Merge branch 'main' of https://dd3skj.picp.vip/JyuHsin/zhgh_cug_v4
# Conflicts: # src/main/resources/application-dev.yaml
This commit is contained in:
@@ -310,7 +310,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
|||||||
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
|
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
|
||||||
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
||||||
// if (Globals.sso) {
|
// if (Globals.sso) {
|
||||||
throw new BaseException("用户名或者密码不正确");
|
// throw new BaseException("用户名或者密码不正确");
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
user = this.fetchLinks(user, "unit");
|
user = this.fetchLinks(user, "unit");
|
||||||
|
|||||||
+16
-10
@@ -59,18 +59,12 @@ public class QsvActivityController {
|
|||||||
//保存问卷基础信息
|
//保存问卷基础信息
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.activity")
|
@SaCheckPermission("qsv.activity")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("保存问卷基础信息")
|
@ApiOperation("保存问卷基础信息")
|
||||||
@SLog(type = "qsv.activity", tag = "保存问卷基础信息", msg = "保存问卷基础信息")
|
@SLog(type = "qsv.activity", tag = "保存问卷基础信息", msg = "保存问卷基础信息")
|
||||||
public Result save(QsvActivity qsvActivity) {
|
public Result save(QsvActivity qsvActivity) {
|
||||||
if (qsvActivity.getCategory().equals("QUIZ")) {
|
QsvActivity activity = qsvActivityService.saveActivity(qsvActivity);
|
||||||
if (qsvActivity.getMode().equals("SCHEDULED")) {
|
return Result.success(activity);
|
||||||
qsvActivity.setRepeatMode("DAILY");
|
|
||||||
} else if (qsvActivity.getMode().equals("REGULAR")) {
|
|
||||||
qsvActivity.setRepeatMode("TOTAL");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dao.insertOrUpdate(qsvActivity);
|
|
||||||
return Result.success();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@@ -83,13 +77,25 @@ public class QsvActivityController {
|
|||||||
// 删除问卷
|
// 删除问卷
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.activity")
|
@SaCheckPermission("qsv.activity")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@ApiOperation("删除问卷")
|
@ApiOperation("删除问卷")
|
||||||
@SLog(type = "qsv.activity", tag = "删除问卷", msg = "删除问卷")
|
@SLog(type = "qsv.activity", tag = "删除问卷", msg = "删除问卷")
|
||||||
public Result delete(@Valid String id) {
|
public Result delete(@Valid String id) {
|
||||||
dao.delete(QsvActivity.class, id);
|
qsvActivityService.deleteActivity(id);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新活动开启状态
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.activity")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("更新活动开启状态")
|
||||||
|
@SLog(type = "qsv.activity", tag = "更新活动开启状态", msg = "更新活动开启状态")
|
||||||
|
public Result updateEnabled(@Valid String id, Boolean enabled) {
|
||||||
|
qsvActivityService.updateEnabled(id, enabled);
|
||||||
|
return Result.success(enabled ? "开启成功" : "关闭成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 保存问卷题目
|
// 保存问卷题目
|
||||||
@At
|
@At
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/qsv")
|
||||||
|
@Ok("json:full")
|
||||||
|
@ApiOperation("问卷调查新建")
|
||||||
|
public class QsvNewController {
|
||||||
|
|
||||||
|
@At("/new")
|
||||||
|
@Ok("beetl:/platform/zhgh/dayofficework/qsv/activity/new.html")
|
||||||
|
@SaCheckPermission("qsv.activity")
|
||||||
|
public void newPage() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+562
@@ -0,0 +1,562 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.lang.Dict;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import com.budwk.app.base.page.Pagination;
|
||||||
|
import com.budwk.app.base.param.PageForm;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvQuizService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/qsv/online")
|
||||||
|
@Ok("json:full")
|
||||||
|
@ApiOperation("在线答题")
|
||||||
|
public class QsvOnlineController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
||||||
|
@Inject
|
||||||
|
private QsvQuizService qsvQuizService;
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhgh/dayofficework/qsv/online/index.html")
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
public void index() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@ApiOperation("在线答题任务分页")
|
||||||
|
public Result pageData(@Valid PageForm pageForm, Integer year, String title, Boolean isAnswered) {
|
||||||
|
List<Integer> groupIds = dao.query(ActivityUserScope.class, Cnd.where("userId", "=", SecurityUtil.getUserId()))
|
||||||
|
.stream().map(ActivityUserScope::getGroupId).collect(Collectors.toList());
|
||||||
|
if (groupIds.isEmpty()) {
|
||||||
|
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Cnd cnd = Cnd.where("enabled", "=", true);
|
||||||
|
cnd.and("groupId", "in", groupIds);
|
||||||
|
cnd.andEX("YEAR(startTime)", "=", year);
|
||||||
|
cnd.and(Cnd.likeEX("title", title));
|
||||||
|
cnd.desc("startTime");
|
||||||
|
|
||||||
|
int totalCount = dao.count(QsvActivity.class, cnd);
|
||||||
|
if (totalCount == 0) {
|
||||||
|
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>()));
|
||||||
|
}
|
||||||
|
List<QsvActivity> activities = dao.query(QsvActivity.class, cnd);
|
||||||
|
List<String> activityIds = activities.stream().map(QsvActivity::getId).collect(Collectors.toList());
|
||||||
|
Map<String, List<QsvUserAnswerRecord>> recordGroup = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId())
|
||||||
|
.and("activityId", "in", activityIds))
|
||||||
|
.stream().collect(Collectors.groupingBy(QsvUserAnswerRecord::getActivityId));
|
||||||
|
List<NutMap> rows = activities.stream()
|
||||||
|
.map((activity) -> buildOnlineRow(activity, recordGroup.getOrDefault(activity.getId(), new ArrayList<>())))
|
||||||
|
.filter((row) -> {
|
||||||
|
if (Boolean.TRUE.equals(isAnswered)) {
|
||||||
|
return row.getBoolean("isAnswered");
|
||||||
|
}
|
||||||
|
return !row.getBoolean("isAnswered") || row.getBoolean("canAnswer");
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
int filteredTotal = rows.size();
|
||||||
|
int fromIndex = Math.min((pageForm.getPageNumber() - 1) * pageForm.getPageSize(), filteredTotal);
|
||||||
|
int toIndex = Math.min(fromIndex + pageForm.getPageSize(), filteredTotal);
|
||||||
|
return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), filteredTotal, rows.subList(fromIndex, toIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("在线答题题目")
|
||||||
|
public Result subjects(@Valid String activityId, Boolean viewOnly) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("调查不存在");
|
||||||
|
}
|
||||||
|
QsvUserAnswerRecord answerRecord = findCurrentAnswerRecord(activityId, Boolean.TRUE.equals(viewOnly));
|
||||||
|
if (Boolean.TRUE.equals(viewOnly) && answerRecord == null) {
|
||||||
|
return Result.error("未找到已完成的答题记录");
|
||||||
|
}
|
||||||
|
boolean finishedView = Boolean.TRUE.equals(viewOnly) && answerRecord != null && Boolean.TRUE.equals(answerRecord.getIsFinish());
|
||||||
|
if (!finishedView) {
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result scopeResult = checkUserScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
answerRecord = prepareAnswerRecord(activity, answerRecord);
|
||||||
|
List<QsvSubject> activitySubjects = queryActivitySubjectsForSync(activity);
|
||||||
|
syncAnswerRecordSubjects(answerRecord, activity, activitySubjects);
|
||||||
|
|
||||||
|
List<QsvSubject> subjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds());
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
subjects.forEach((subject) -> {
|
||||||
|
if (subject.getUserSelectOptionIds() == null) {
|
||||||
|
subject.setUserSelectOptionIds(new ArrayList<>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result.success(NutMap.NEW()
|
||||||
|
.addv("activity", activity)
|
||||||
|
.addv("subjects", subjects)
|
||||||
|
.addv("answerRecordId", answerRecord.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@ApiOperation("在线答题记录")
|
||||||
|
public Result answerRecord(@Valid String answerRecordId) {
|
||||||
|
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
||||||
|
return Result.success(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@ApiOperation("在线投票统计")
|
||||||
|
public Result voteStats(@Valid String activityId) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
if (activity == null || !"VOTE".equals(activity.getCategory())) {
|
||||||
|
return Result.error("投票不存在");
|
||||||
|
}
|
||||||
|
Result scopeResult = checkUserScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
return Result.success(buildVoteStats(activityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.online")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("提交在线答题")
|
||||||
|
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
Result scopeResult = checkUserScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
||||||
|
if (answerRecord == null || !SecurityUtil.getUserId().equals(answerRecord.getUserId())) {
|
||||||
|
return Result.error("答题记录不存在");
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return Result.error("您已完成该调查");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
float totalScore = 0;
|
||||||
|
boolean isQuiz = "QUIZ".equals(activity.getCategory());
|
||||||
|
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
||||||
|
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(entries)) {
|
||||||
|
entries = new JSONObject();
|
||||||
|
}
|
||||||
|
entries.set("optionIds", subject.getUserSelectOptionIds());
|
||||||
|
entries.set("text", subject.getUserFillContent());
|
||||||
|
entries.set("optionFillContents", subject.getOptionFillContents());
|
||||||
|
if (isQuiz) {
|
||||||
|
QsvCheckAnswerResult answerResult = qsvQuizService.calcScore(subject.getId(), subject.getUserSelectOptionIds());
|
||||||
|
entries.set("score", answerResult.getScore());
|
||||||
|
entries.set("isCorrect", answerResult.isCorrect());
|
||||||
|
totalScore += answerResult.getScore();
|
||||||
|
}
|
||||||
|
extJson.set(subject.getId(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
if (isQuiz) {
|
||||||
|
answerRecord.setTotalScore(totalScore);
|
||||||
|
answerRecord.setAnswerTime(qsvAnswerParam.getAnswerTime());
|
||||||
|
}
|
||||||
|
answerRecord.setAttemptDate(new Date());
|
||||||
|
answerRecord.setSubmitTime(new Date());
|
||||||
|
answerRecord.setIsFinish(true);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
if (isQuiz) {
|
||||||
|
qsvUserAnswerRecordService.calcByScoreMode(qsvAnswerParam.getActivityId(), SecurityUtil.getUserId());
|
||||||
|
}
|
||||||
|
return Result.success("提交成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkActivityStatus(QsvActivity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("调查不存在");
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(activity.getEnabled())) {
|
||||||
|
return Result.error("活动已关闭,暂不能参与");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
|
||||||
|
return Result.error("调查尚未开始,请在开始后再参与");
|
||||||
|
}
|
||||||
|
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
|
||||||
|
return Result.error("调查已结束");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkUserScope(QsvActivity activity) {
|
||||||
|
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (count != 1) {
|
||||||
|
return Result.error("您无需参加此次调查,感谢您的关注!");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildOnlineRow(QsvActivity activity, List<QsvUserAnswerRecord> records) {
|
||||||
|
boolean isAnswered = records.stream().anyMatch((record) -> Boolean.TRUE.equals(record.getIsFinish()));
|
||||||
|
boolean hasUnfinished = records.stream().anyMatch((record) -> !Boolean.TRUE.equals(record.getIsFinish()));
|
||||||
|
boolean canRepeat = "QUIZ".equals(activity.getCategory())
|
||||||
|
&& Boolean.TRUE.equals(activity.getRepeatable())
|
||||||
|
&& ("SCHEDULED".equals(activity.getMode()) || ("REGULAR".equals(activity.getMode()) && "ALL".equals(activity.getDisplayMode())));
|
||||||
|
boolean randomDisplayMode = "QUIZ".equals(activity.getCategory())
|
||||||
|
&& "REGULAR".equals(activity.getMode())
|
||||||
|
&& "RANDOM".equals(activity.getDisplayMode());
|
||||||
|
int maxAttempts = activity.getMaxAttempts() == null || activity.getMaxAttempts() < 1 ? 1 : activity.getMaxAttempts();
|
||||||
|
int totalRandom = activity.getTotalRandom() == null || activity.getTotalRandom() < 1 ? 1 : activity.getTotalRandom();
|
||||||
|
long attemptCount = records.size();
|
||||||
|
if ("SCHEDULED".equals(activity.getMode())) {
|
||||||
|
attemptCount = records.stream()
|
||||||
|
.filter((record) -> record.getAttemptDate() != null && DateUtil.isSameDay(record.getAttemptDate(), DateUtil.date()))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
boolean notStarted = activity.getStartTime() != null && activity.getStartTime().after(now);
|
||||||
|
boolean ended = activity.getEndTime() != null && activity.getEndTime().before(now);
|
||||||
|
boolean activityAvailable = Boolean.TRUE.equals(activity.getEnabled()) && !notStarted && !ended;
|
||||||
|
boolean canAnswerByAttempt = hasUnfinished || !isAnswered || (canRepeat && attemptCount < maxAttempts) || (randomDisplayMode && attemptCount < totalRandom);
|
||||||
|
boolean canAnswer = activityAvailable && canAnswerByAttempt;
|
||||||
|
boolean isVote = "VOTE".equals(activity.getCategory());
|
||||||
|
String actionText;
|
||||||
|
String actionType;
|
||||||
|
String statusText;
|
||||||
|
String statusType;
|
||||||
|
if (!activityAvailable && !isAnswered) {
|
||||||
|
if (!Boolean.TRUE.equals(activity.getEnabled())) {
|
||||||
|
actionText = "已关闭";
|
||||||
|
statusText = "已关闭";
|
||||||
|
} else if (notStarted) {
|
||||||
|
actionText = isVote ? "未开始投票" : "未开始答题";
|
||||||
|
statusText = "未开始";
|
||||||
|
} else {
|
||||||
|
actionText = isVote ? "投票已结束" : "答题已结束";
|
||||||
|
statusText = "已结束";
|
||||||
|
}
|
||||||
|
actionType = "unavailable";
|
||||||
|
statusType = "info";
|
||||||
|
} else if (hasUnfinished && canAnswer) {
|
||||||
|
actionText = isVote ? "继续投票" : "继续答题";
|
||||||
|
actionType = "answer";
|
||||||
|
statusText = isVote ? "投票中" : "答题中";
|
||||||
|
statusType = "warning";
|
||||||
|
} else if (!isAnswered && canAnswer) {
|
||||||
|
actionText = isVote ? "投票" : "答题";
|
||||||
|
actionType = "answer";
|
||||||
|
statusText = isVote ? "未投票" : "未答题";
|
||||||
|
statusType = "warning";
|
||||||
|
} else if (canAnswer) {
|
||||||
|
actionText = isVote ? "再次投票" : "再次答题";
|
||||||
|
actionType = "answer";
|
||||||
|
statusText = isVote ? "可再次投票" : "可再次答题";
|
||||||
|
statusType = "warning";
|
||||||
|
} else {
|
||||||
|
actionText = isVote ? "查看投票" : "查看答题";
|
||||||
|
actionType = "view";
|
||||||
|
statusText = isVote ? "已投票" : "已答题";
|
||||||
|
statusType = "success";
|
||||||
|
}
|
||||||
|
Optional<QsvUserAnswerRecord> latestFinishedRecord = records.stream()
|
||||||
|
.filter((record) -> Boolean.TRUE.equals(record.getIsFinish()))
|
||||||
|
.max((first, second) -> {
|
||||||
|
long firstTime = first.getSubmitTime() == null ? 0 : first.getSubmitTime().getTime();
|
||||||
|
long secondTime = second.getSubmitTime() == null ? 0 : second.getSubmitTime().getTime();
|
||||||
|
return Long.compare(firstTime, secondTime);
|
||||||
|
});
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("id", activity.getId())
|
||||||
|
.addv("title", activity.getTitle())
|
||||||
|
.addv("category", activity.getCategory())
|
||||||
|
.addv("startTime", activity.getStartTime())
|
||||||
|
.addv("endTime", activity.getEndTime())
|
||||||
|
.addv("enabled", activity.getEnabled())
|
||||||
|
.addv("isAnswered", isAnswered)
|
||||||
|
.addv("hasUnfinished", hasUnfinished)
|
||||||
|
.addv("canAnswer", canAnswer)
|
||||||
|
.addv("notStarted", notStarted)
|
||||||
|
.addv("ended", ended)
|
||||||
|
.addv("canRepeat", canRepeat)
|
||||||
|
.addv("answeredCount", records.stream().filter((record) -> Boolean.TRUE.equals(record.getIsFinish())).count())
|
||||||
|
.addv("remainingAttempts", canRepeat ? Math.max(maxAttempts - attemptCount, 0) : (randomDisplayMode ? Math.max(totalRandom - attemptCount, 0) : 0))
|
||||||
|
.addv("actionText", actionText)
|
||||||
|
.addv("actionType", actionType)
|
||||||
|
.addv("statusText", statusText)
|
||||||
|
.addv("statusType", statusType)
|
||||||
|
.addv("totalScore", latestFinishedRecord.map(QsvUserAnswerRecord::getTotalScore).orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private QsvUserAnswerRecord findCurrentAnswerRecord(String activityId, boolean viewOnly) {
|
||||||
|
Cnd cnd = Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId());
|
||||||
|
if (viewOnly) {
|
||||||
|
cnd.and("isFinish", "=", true).desc("submitTime").desc("createdAt");
|
||||||
|
} else {
|
||||||
|
cnd.and(Cnd.exps("isFinish", "=", false).or("isFinish", "is", null)).desc("createdAt");
|
||||||
|
}
|
||||||
|
return dao.fetch(QsvUserAnswerRecord.class, cnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private QsvUserAnswerRecord prepareAnswerRecord(QsvActivity activity, QsvUserAnswerRecord answerRecord) {
|
||||||
|
if (answerRecord != null) {
|
||||||
|
return answerRecord;
|
||||||
|
}
|
||||||
|
if ("QUIZ".equals(activity.getCategory())
|
||||||
|
&& "REGULAR".equals(activity.getMode())
|
||||||
|
&& "RANDOM".equals(activity.getDisplayMode())) {
|
||||||
|
return createRandomAnswerRecord(activity);
|
||||||
|
}
|
||||||
|
List<QsvSubject> activitySubjects = queryActivitySubjectsForSync(activity);
|
||||||
|
return qsvUserAnswerRecordService.insertRecord(activity.getId(), activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private QsvUserAnswerRecord createRandomAnswerRecord(QsvActivity activity) {
|
||||||
|
Integer randomCount = activity.getRandomCount();
|
||||||
|
Integer totalRandom = activity.getTotalRandom();
|
||||||
|
if (randomCount == null || randomCount < 1) {
|
||||||
|
return qsvUserAnswerRecordService.insertRecord(activity.getId(), new ArrayList<>());
|
||||||
|
}
|
||||||
|
if (totalRandom == null || totalRandom < 1) {
|
||||||
|
totalRandom = 1;
|
||||||
|
}
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activity.getId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (answerRecords.size() >= totalRandom) {
|
||||||
|
return answerRecords.stream()
|
||||||
|
.max((first, second) -> Integer.compare(first.getRandomNumber() == null ? 0 : first.getRandomNumber(), second.getRandomNumber() == null ? 0 : second.getRandomNumber()))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
List<String> usedSubjectIds = answerRecords.stream()
|
||||||
|
.filter((record) -> record.getSubjectIds() != null)
|
||||||
|
.flatMap((record) -> record.getSubjectIds().stream())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Cnd subjectCnd = Cnd.where("activityId", "=", activity.getId());
|
||||||
|
if (!usedSubjectIds.isEmpty()) {
|
||||||
|
subjectCnd.and("id", "not in", usedSubjectIds);
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, subjectCnd.asc("sortNum"));
|
||||||
|
if (subjects.size() < randomCount) {
|
||||||
|
throw new RuntimeException("题目数不够,无法生成题目");
|
||||||
|
}
|
||||||
|
Collections.shuffle(subjects);
|
||||||
|
List<String> randomSubjectIds = subjects.subList(0, randomCount).stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
return qsvUserAnswerRecordService.insertRecord(activity.getId(), randomSubjectIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> queryActivitySubjectsForSync(QsvActivity activity) {
|
||||||
|
Cnd cnd = Cnd.where("activityId", "=", activity.getId());
|
||||||
|
if ("QUIZ".equals(activity.getCategory())
|
||||||
|
&& "SCHEDULED".equals(activity.getMode())) {
|
||||||
|
cnd.and("displayDate", "=", DateUtil.today());
|
||||||
|
}
|
||||||
|
return dao.query(QsvSubject.class, cnd.asc("sortNum"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, QsvActivity activity, List<QsvSubject> activitySubjects) {
|
||||||
|
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
List<String> recordSubjectIds = answerRecord.getSubjectIds();
|
||||||
|
if (recordSubjectIds == null) {
|
||||||
|
recordSubjectIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean randomDisplayMode = "QUIZ".equals(activity.getCategory())
|
||||||
|
&& "REGULAR".equals(activity.getMode())
|
||||||
|
&& "RANDOM".equals(activity.getDisplayMode());
|
||||||
|
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
|
||||||
|
List<String> normalizedSubjectIds = recordSubjectIds.stream()
|
||||||
|
.filter(currentSubjectIdSet::contains)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (randomDisplayMode) {
|
||||||
|
currentSubjectIds = normalizedSubjectIds;
|
||||||
|
} else {
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (!normalizedSubjectIds.contains(subjectId)) {
|
||||||
|
normalizedSubjectIds.add(subjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean changed = !recordSubjectIds.equals(normalizedSubjectIds);
|
||||||
|
boolean subjectOrderLocked = Boolean.TRUE.equals(extJson.get("_subjectOrderLocked"));
|
||||||
|
if ("QUIZ".equals(activity.getCategory())
|
||||||
|
&& Boolean.TRUE.equals(activity.getShuffleSubject())
|
||||||
|
&& !subjectOrderLocked
|
||||||
|
&& isAnswerRecordEmpty(extJson, currentSubjectIds)) {
|
||||||
|
Collections.shuffle(normalizedSubjectIds);
|
||||||
|
extJson.set("_subjectOrderLocked", true);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) {
|
||||||
|
extJson.set(subjectId, Dict.create()
|
||||||
|
.set("optionIds", new ArrayList<>())
|
||||||
|
.set("text", null)
|
||||||
|
.set("optionFillContents", new JSONObject()));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
answerRecord.setSubjectIds(normalizedSubjectIds);
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAnswerRecordEmpty(JSONObject extJson, List<String> subjectIds) {
|
||||||
|
for (String subjectId : subjectIds) {
|
||||||
|
JSONObject answer = extJson.get(subjectId, JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(answer)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isNotEmpty(answer.get("optionIds"))
|
||||||
|
|| ObjectUtil.isNotEmpty(answer.get("text"))
|
||||||
|
|| ObjectUtil.isNotEmpty(answer.get("optionFillContents"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildVoteStats(String activityId) {
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
|
||||||
|
Map<String, NutMap> optionStats = new LinkedHashMap<>();
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
if (subject.getOptions() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvOption option : subject.getOptions()) {
|
||||||
|
optionStats.put(option.getId(), NutMap.NEW()
|
||||||
|
.addv("optionId", option.getId())
|
||||||
|
.addv("subjectId", subject.getId())
|
||||||
|
.addv("text", option.getText())
|
||||||
|
.addv("imgUrl", option.getImgUrl())
|
||||||
|
.addv("votes", 0)
|
||||||
|
.addv("percent", 0D));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QsvUserAnswerRecord> records = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
for (QsvUserAnswerRecord record : records) {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
JSONObject entry = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> optionIds = entry.getBeanList("optionIds", String.class);
|
||||||
|
if (optionIds == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String optionId : optionIds) {
|
||||||
|
NutMap stat = optionStats.get(optionId);
|
||||||
|
if (stat != null) {
|
||||||
|
stat.put("votes", stat.getInt("votes", 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalParticipants = records.size();
|
||||||
|
List<NutMap> rankList = new ArrayList<>(optionStats.values());
|
||||||
|
for (NutMap stat : rankList) {
|
||||||
|
int votes = stat.getInt("votes", 0);
|
||||||
|
double percent = totalParticipants == 0 ? 0D : votes * 100D / totalParticipants;
|
||||||
|
stat.put("percent", percent);
|
||||||
|
}
|
||||||
|
rankList.sort((left, right) -> Integer.compare(right.getInt("votes", 0), left.getInt("votes", 0)));
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("totalParticipants", totalParticipants)
|
||||||
|
.addv("optionCount", optionStats.size())
|
||||||
|
.addv("optionStats", optionStats)
|
||||||
|
.addv("rankList", rankList);
|
||||||
|
}
|
||||||
|
}
|
||||||
+287
-5
@@ -1,16 +1,27 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
package com.budwk.app.zhgh.dayofficework.qsv.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONArray;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvSurveyService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvSurveyService;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
@@ -21,6 +32,10 @@ import org.nutz.mvc.annotation.Ok;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@@ -43,12 +58,19 @@ public class QsvSurveyController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At("/reportPage")
|
||||||
|
@Ok("beetl:/platform/zhgh/dayofficework/qsv/survey/reportPage.html")
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
public void reportPage() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.survey")
|
@SaCheckPermission("qsv.survey")
|
||||||
@ApiOperation("分页查询")
|
@ApiOperation("分页查询")
|
||||||
public Result pageData(@Valid PageForm pageForm, Integer year) {
|
public Result pageData(@Valid PageForm pageForm, Integer year) {
|
||||||
Cnd cnd = Cnd.where("category", "=", "SURVEY");
|
Cnd cnd = Cnd.where("category", "in", new String[]{"SURVEY", "VOTE"});
|
||||||
cnd.andEX("YEAR(startTime)", "=", year);
|
cnd.andEX("YEAR(startTime)", "=", year);
|
||||||
cnd.desc("category");
|
cnd.desc("category");
|
||||||
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
||||||
@@ -63,6 +85,43 @@ public class QsvSurveyController {
|
|||||||
return Result.success(report);
|
return Result.success(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("查询调查信息")
|
||||||
|
public Result activityInfo(@Valid String activityId) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
NutMap data = NutMap.NEW();
|
||||||
|
if (activity != null) {
|
||||||
|
data.setv("id", activity.getId());
|
||||||
|
data.setv("title", activity.getTitle());
|
||||||
|
}
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("生成分类报告")
|
||||||
|
public Result categoryReport(@Valid String activityId, String conditions) {
|
||||||
|
List<NutMap> report = qsvSurveyService.categoryReport(activityId, conditions);
|
||||||
|
return Result.success(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("生成交叉分析报告")
|
||||||
|
public Result crossReport(@Valid String activityId, @Valid String xSubjectIds, @Valid String ySubjectIds, String conditions) {
|
||||||
|
NutMap report = qsvSurveyService.crossReport(activityId, xSubjectIds, ySubjectIds, conditions);
|
||||||
|
return Result.success(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("生成对比分析报告")
|
||||||
|
public Result compareReport(@Valid String activityId, @Valid String subjectId, @Valid String optionId) {
|
||||||
|
NutMap report = qsvSurveyService.compareReport(activityId, subjectId, optionId);
|
||||||
|
return Result.success(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
@@ -72,13 +131,236 @@ public class QsvSurveyController {
|
|||||||
qsvSurveyService.exportReportXlsx(activityId, response);
|
qsvSurveyService.exportReportXlsx(activityId, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckPermission("qsv.survey")
|
||||||
|
@ApiOperation("下载PDF报告")
|
||||||
|
public void exportReportPdf(String reportData, HttpServletResponse response) {
|
||||||
|
if (StrUtil.isBlank(reportData)) {
|
||||||
|
throw new IllegalArgumentException("报告数据不能为空");
|
||||||
|
}
|
||||||
|
JSONObject report = JSONUtil.parseObj(reportData);
|
||||||
|
String title = StrUtil.blankToDefault(report.getStr("title"), "分析报告");
|
||||||
|
String tabLabel = StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告");
|
||||||
|
try {
|
||||||
|
CommonDownloadUtil.download(safeFileName(title + "-" + tabLabel) + ".pdf", buildReportPdf(report), response);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("生成PDF报告失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildReportPdf(JSONObject report) throws IOException {
|
||||||
|
try (PDDocument document = new PDDocument(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||||
|
PdfReportWriter writer = new PdfReportWriter(document);
|
||||||
|
writer.title(StrUtil.blankToDefault(report.getStr("title"), "分析报告"));
|
||||||
|
writer.text("报告类型:" + StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告"), 10);
|
||||||
|
writer.text("生成时间:" + DateUtil.now(), 10);
|
||||||
|
writer.gap(8);
|
||||||
|
|
||||||
|
JSONArray sections = report.getJSONArray("sections");
|
||||||
|
if (sections == null || sections.isEmpty()) {
|
||||||
|
writer.text("暂无可下载内容", 12);
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < sections.size(); i++) {
|
||||||
|
JSONObject section = JSONUtil.parseObj(sections.get(i));
|
||||||
|
writer.section(StrUtil.blankToDefault(section.getStr("title"), "统计项"));
|
||||||
|
String description = section.getStr("description");
|
||||||
|
if (StrUtil.isNotBlank(description)) {
|
||||||
|
writer.text(description, 10);
|
||||||
|
}
|
||||||
|
String image = section.getStr("image");
|
||||||
|
if (StrUtil.isNotBlank(image)) {
|
||||||
|
writer.image(image);
|
||||||
|
}
|
||||||
|
writer.table(section.getJSONArray("rows"));
|
||||||
|
writer.gap(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
document.save(out);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeFileName(String fileName) {
|
||||||
|
return StrUtil.blankToDefault(fileName, "分析报告").replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException {
|
||||||
|
String[] paths = bold ? new String[]{
|
||||||
|
"C:/Windows/Fonts/simhei.ttf",
|
||||||
|
"C:/Windows/Fonts/simsunb.ttf",
|
||||||
|
"C:/Windows/Fonts/msyhbd.ttf"
|
||||||
|
} : new String[]{
|
||||||
|
"C:/Windows/Fonts/simfang.ttf",
|
||||||
|
"C:/Windows/Fonts/simsun.ttf",
|
||||||
|
"C:/Windows/Fonts/msyh.ttf"
|
||||||
|
};
|
||||||
|
for (String path : paths) {
|
||||||
|
File file = new File(path);
|
||||||
|
if (file.exists()) {
|
||||||
|
return PDType0Font.load(document, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IOException("未找到可用的中文字体");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PdfReportWriter {
|
||||||
|
private final PDDocument document;
|
||||||
|
private final PDType0Font regularFont;
|
||||||
|
private final PDType0Font boldFont;
|
||||||
|
private final float margin = 42F;
|
||||||
|
private final float pageWidth = PDRectangle.A4.getWidth();
|
||||||
|
private final float pageHeight = PDRectangle.A4.getHeight();
|
||||||
|
private final float contentWidth = pageWidth - margin * 2;
|
||||||
|
private PDPageContentStream content;
|
||||||
|
private float y;
|
||||||
|
|
||||||
|
PdfReportWriter(PDDocument document) throws IOException {
|
||||||
|
this.document = document;
|
||||||
|
this.regularFont = loadChineseFont(document, false);
|
||||||
|
this.boldFont = loadChineseFont(document, true);
|
||||||
|
newPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void newPage() throws IOException {
|
||||||
|
if (content != null) {
|
||||||
|
content.close();
|
||||||
|
}
|
||||||
|
PDPage page = new PDPage(PDRectangle.A4);
|
||||||
|
document.addPage(page);
|
||||||
|
content = new PDPageContentStream(document, page);
|
||||||
|
y = pageHeight - margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
if (content != null) {
|
||||||
|
content.close();
|
||||||
|
content = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensure(float height) throws IOException {
|
||||||
|
if (y - height < margin) {
|
||||||
|
newPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void gap(float height) throws IOException {
|
||||||
|
ensure(height);
|
||||||
|
y -= height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void title(String text) throws IOException {
|
||||||
|
ensure(36);
|
||||||
|
drawText(text, margin, y, 18, boldFont);
|
||||||
|
y -= 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
void section(String text) throws IOException {
|
||||||
|
ensure(30);
|
||||||
|
drawText(text, margin, y, 13, boldFont);
|
||||||
|
y -= 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
void text(String text, float fontSize) throws IOException {
|
||||||
|
List<String> lines = wrap(StrUtil.blankToDefault(text, ""), fontSize, contentWidth, regularFont);
|
||||||
|
for (String line : lines) {
|
||||||
|
ensure(fontSize + 8);
|
||||||
|
drawText(line, margin, y, fontSize, regularFont);
|
||||||
|
y -= fontSize + 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void image(String dataUrl) throws IOException {
|
||||||
|
int commaIndex = dataUrl.indexOf(",");
|
||||||
|
if (commaIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] bytes = Base64.getDecoder().decode(dataUrl.substring(commaIndex + 1));
|
||||||
|
PDImageXObject image = PDImageXObject.createFromByteArray(document, bytes, "chart");
|
||||||
|
float width = contentWidth;
|
||||||
|
float height = width * image.getHeight() / image.getWidth();
|
||||||
|
if (height > 260) {
|
||||||
|
height = 260;
|
||||||
|
width = height * image.getWidth() / image.getHeight();
|
||||||
|
}
|
||||||
|
ensure(height + 12);
|
||||||
|
content.drawImage(image, margin, y - height, width, height);
|
||||||
|
y -= height + 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
void table(JSONArray rows) throws IOException {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
text("暂无统计数据", 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float[] widths = new float[]{contentWidth * 0.46F, contentWidth * 0.15F, contentWidth * 0.17F, contentWidth * 0.22F};
|
||||||
|
drawTableRow(new String[]{"选项/内容", "数量", "占比", "备注"}, widths, true);
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
JSONObject row = JSONUtil.parseObj(rows.get(i));
|
||||||
|
drawTableRow(new String[]{
|
||||||
|
StrUtil.blankToDefault(row.getStr("name"), ""),
|
||||||
|
StrUtil.blankToDefault(row.getStr("count"), ""),
|
||||||
|
StrUtil.blankToDefault(row.getStr("percent"), ""),
|
||||||
|
StrUtil.blankToDefault(row.getStr("remark"), "")
|
||||||
|
}, widths, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawTableRow(String[] values, float[] widths, boolean header) throws IOException {
|
||||||
|
float rowHeight = 24F;
|
||||||
|
ensure(rowHeight);
|
||||||
|
float x = margin;
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
content.addRect(x, y - rowHeight, widths[i], rowHeight);
|
||||||
|
content.stroke();
|
||||||
|
drawText(clip(values[i], header ? 16 : 28), x + 5, y - 16, 9, header ? boldFont : regularFont);
|
||||||
|
x += widths[i];
|
||||||
|
}
|
||||||
|
y -= rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawText(String text, float x, float y, float fontSize, PDType0Font font) throws IOException {
|
||||||
|
content.beginText();
|
||||||
|
content.setFont(font, fontSize);
|
||||||
|
content.newLineAtOffset(x, y);
|
||||||
|
content.showText(StrUtil.blankToDefault(text, "").replaceAll("[\\r\\n\\t]", " "));
|
||||||
|
content.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> wrap(String text, float fontSize, float maxWidth, PDType0Font font) throws IOException {
|
||||||
|
List<String> lines = new java.util.ArrayList<>();
|
||||||
|
StringBuilder line = new StringBuilder();
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
String next = line.toString() + ch;
|
||||||
|
if (font.getStringWidth(next) / 1000 * fontSize > maxWidth && line.length() > 0) {
|
||||||
|
lines.add(line.toString());
|
||||||
|
line.setLength(0);
|
||||||
|
}
|
||||||
|
line.append(ch);
|
||||||
|
}
|
||||||
|
if (line.length() > 0 || lines.isEmpty()) {
|
||||||
|
lines.add(line.toString());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
String clip(String text, int maxLength) {
|
||||||
|
if (text == null || text.length() <= maxLength) {
|
||||||
|
return StrUtil.blankToDefault(text, "");
|
||||||
|
}
|
||||||
|
return text.substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("qsv.survey")
|
@SaCheckPermission("qsv.survey")
|
||||||
@ApiOperation("选项选择详情")
|
@ApiOperation("选项选择详情")
|
||||||
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId) {
|
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId, String conditions) {
|
||||||
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
|
List<NutMap> selectOptionUsers = qsvSurveyService.selectOptionUsers(activityId, subjectId, optionId, conditions);
|
||||||
List<QsvUserAnswerRecord> selectOptionUsers = answerRecords.stream().filter(ext -> ObjectUtil.isNotNull(ext.getExtJson().get(subjectId, JSONObject.class)) && ext.getExtJson().get(subjectId, JSONObject.class).getJSONArray("optionIds").contains(optionId)).toList();
|
|
||||||
return Result.success(selectOptionUsers);
|
return Result.success(selectOptionUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+70
-2
@@ -1,19 +1,29 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@At("/platform/h5/qsv")
|
@At("/platform/h5/qsv")
|
||||||
@@ -23,6 +33,8 @@ public class H5QsvController {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private QsvActivityService qsvActivityService;
|
private QsvActivityService qsvActivityService;
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/index.html")
|
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/index.html")
|
||||||
@@ -33,13 +45,69 @@ public class H5QsvController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("h5.qsv")
|
@SaCheckPermission("h5.qsv")
|
||||||
public Result pageData(@Valid PageForm pageForm, @Valid String category) {
|
public Result pageData(@Valid PageForm pageForm, @Valid String category, @Param("title") String title) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.andEX("category", "=", category);
|
cnd.andEX("category", "=", category);
|
||||||
|
cnd.and(Cnd.likeEX("title", title));
|
||||||
|
cnd.and(Cnd.exps("enabled", "=", true).or("enabled", "is", null));
|
||||||
|
LocalDate currentYearFirstDay = LocalDate.now().withDayOfYear(1);
|
||||||
|
Date currentYearStart = Date.from(currentYearFirstDay.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
Date nextYearStart = Date.from(currentYearFirstDay.plusYears(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||||
|
cnd.and("startTime", ">=", currentYearStart);
|
||||||
|
cnd.and("startTime", "<", nextYearStart);
|
||||||
cnd.desc("startTime");
|
cnd.desc("startTime");
|
||||||
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
||||||
|
fillAnsweredStatus(pagination);
|
||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fillAnsweredStatus(Pagination pagination) {
|
||||||
|
List<NutMap> rows = pagination.getList();
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> activityIds = rows.stream()
|
||||||
|
.map(this::getRowActivityId)
|
||||||
|
.filter(id -> id != null && !id.isBlank())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (activityIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> answeredActivityIds = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId())
|
||||||
|
.and("activityId", "in", activityIds)
|
||||||
|
.and("isFinish", "=", true))
|
||||||
|
.stream()
|
||||||
|
.map(QsvUserAnswerRecord::getActivityId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
rows.forEach(row -> {
|
||||||
|
boolean isAnswered = answeredActivityIds.contains(getRowActivityId(row));
|
||||||
|
row.put("isAnswered", isAnswered);
|
||||||
|
row.put("answeredText", isAnswered ? getAnsweredText(row.getString("category")) : "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRowActivityId(NutMap row) {
|
||||||
|
String id = row.getString("id");
|
||||||
|
if (id == null || id.isBlank()) {
|
||||||
|
id = row.getString("ID");
|
||||||
|
}
|
||||||
|
if (id == null || id.isBlank()) {
|
||||||
|
id = row.getString("activityId");
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAnsweredText(String category) {
|
||||||
|
if ("QUIZ".equals(category)) {
|
||||||
|
return "已答题";
|
||||||
|
}
|
||||||
|
if ("SURVEY".equals(category)) {
|
||||||
|
return "已填写";
|
||||||
|
}
|
||||||
|
if ("VOTE".equals(category)) {
|
||||||
|
return "已投票";
|
||||||
|
}
|
||||||
|
return "已完成";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-28
@@ -1,7 +1,6 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||||
import cn.hutool.core.collection.CollectionUtil;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
@@ -9,7 +8,7 @@ import cn.hutool.json.JSONObject;
|
|||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
|
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult;
|
import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
@@ -44,8 +43,6 @@ public class H5QsvQuizController {
|
|||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private Dao dao;
|
||||||
@Inject
|
@Inject
|
||||||
private ActivityBasicScopeService activityBasicScopeService;
|
|
||||||
@Inject
|
|
||||||
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
||||||
@Inject
|
@Inject
|
||||||
private QsvQuizService qsvQuizService;
|
private QsvQuizService qsvQuizService;
|
||||||
@@ -71,7 +68,9 @@ public class H5QsvQuizController {
|
|||||||
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
|
||||||
if (activity.getGroupId() != null) {
|
if (activity.getGroupId() != null) {
|
||||||
if (!activityBasicScopeService.isUserInGroup(activity.getGroupId(), SecurityUtil.getUserId())) {
|
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (count != 1) {
|
||||||
return Result.error("您无需参加此次答题,感谢您的关注!");
|
return Result.error("您无需参加此次答题,感谢您的关注!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +102,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = lastRecordOptional.get().getSubjectIds();
|
List<String> subjectIds = lastRecordOptional.get().getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
answerRecordId = lastRecordOptional.get().getId();
|
answerRecordId = lastRecordOptional.get().getId();
|
||||||
} else {
|
} else {
|
||||||
@@ -115,7 +114,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = answerRecord.getSubjectIds();
|
List<String> subjectIds = answerRecord.getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -125,7 +124,9 @@ public class H5QsvQuizController {
|
|||||||
//首次进来生成答题记录
|
//首次进来生成答题记录
|
||||||
if ("ALL".equals(displayMode)) {
|
if ("ALL".equals(displayMode)) {
|
||||||
resultSubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
resultSubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
answerRecordId = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())).getId();
|
QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
|
||||||
|
answerRecordId = answerRecord.getId();
|
||||||
|
resultSubjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds());
|
||||||
} else if ("RANDOM".equals(displayMode)) {
|
} else if ("RANDOM".equals(displayMode)) {
|
||||||
//单次随机抽取题目数量
|
//单次随机抽取题目数量
|
||||||
Integer randomCount = activity.getRandomCount();
|
Integer randomCount = activity.getRandomCount();
|
||||||
@@ -144,7 +145,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = answerRecord.getSubjectIds();
|
List<String> subjectIds = answerRecord.getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -170,7 +171,7 @@ public class H5QsvQuizController {
|
|||||||
}
|
}
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
repeatTips = true;
|
repeatTips = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -180,7 +181,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
|
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
answerRecordId = maxAnswerRecordOptional.get().getId();
|
answerRecordId = maxAnswerRecordOptional.get().getId();
|
||||||
} else {
|
} else {
|
||||||
@@ -193,7 +194,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
|
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
answerRecordId = maxAnswerRecordOptional.get().getId();
|
answerRecordId = maxAnswerRecordOptional.get().getId();
|
||||||
} else {
|
} else {
|
||||||
@@ -207,7 +208,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = notFinishRecordOptional.get().getSubjectIds();
|
List<String> subjectIds = notFinishRecordOptional.get().getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
answerRecordId = notFinishRecordOptional.get().getId();
|
answerRecordId = notFinishRecordOptional.get().getId();
|
||||||
} else {
|
} else {
|
||||||
@@ -245,7 +246,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
|
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
answerRecordId = maxAnswerRecordOptional.get().getId();
|
answerRecordId = maxAnswerRecordOptional.get().getId();
|
||||||
} else {
|
} else {
|
||||||
@@ -281,7 +282,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = answerRecord.getSubjectIds();
|
List<String> subjectIds = answerRecord.getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
} else {
|
} else {
|
||||||
//今天最大那次的记录
|
//今天最大那次的记录
|
||||||
@@ -292,7 +293,7 @@ public class H5QsvQuizController {
|
|||||||
List<String> subjectIds = maxTodayRecord.get().getSubjectIds();
|
List<String> subjectIds = maxTodayRecord.get().getSubjectIds();
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("业务异常");
|
throw new RuntimeException("业务异常");
|
||||||
@@ -306,12 +307,9 @@ public class H5QsvQuizController {
|
|||||||
answerRecordId = answerRecord.getId();
|
answerRecordId = answerRecord.getId();
|
||||||
|
|
||||||
List<String> subjectIds = answerRecord.getSubjectIds();
|
List<String> subjectIds = answerRecord.getSubjectIds();
|
||||||
if(CollectionUtil.isEmpty(subjectIds)){
|
|
||||||
throw new RuntimeException("题目列表为空!请检查答题显示日期");
|
|
||||||
}
|
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
resultSubjects = dao.query(QsvSubject.class, cnd);
|
resultSubjects = dao.query(QsvSubject.class, cnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,13 +342,6 @@ public class H5QsvQuizController {
|
|||||||
float totalScore = 0;
|
float totalScore = 0;
|
||||||
|
|
||||||
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
||||||
QsvSubject dbSubject = dao.fetch(QsvSubject.class, subject.getId());
|
|
||||||
if (ObjectUtil.isNotEmpty(dbSubject) && "checkbox".equals(dbSubject.getType())
|
|
||||||
&& ObjectUtil.isNotEmpty(dbSubject.getMaxMulti()) && dbSubject.getMaxMulti() > 0
|
|
||||||
&& CollectionUtil.size(subject.getUserSelectOptionIds()) > dbSubject.getMaxMulti()) {
|
|
||||||
// 后端兜底校验最大可选数,避免绕过前端直接提交超限答案。
|
|
||||||
return Result.error("题目【" + dbSubject.getTitle() + "】最多只能选择" + dbSubject.getMaxMulti() + "项");
|
|
||||||
}
|
|
||||||
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
|
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
|
||||||
if (ObjectUtil.isNotEmpty(entries)) {
|
if (ObjectUtil.isNotEmpty(entries)) {
|
||||||
entries.set("optionIds", subject.getUserSelectOptionIds());
|
entries.set("optionIds", subject.getUserSelectOptionIds());
|
||||||
@@ -395,7 +386,7 @@ public class H5QsvQuizController {
|
|||||||
|
|
||||||
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
Cnd cnd = Cnd.where("id", "in", subjectIds);
|
||||||
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
|
||||||
cnd.orderBy("sortNum","asc");
|
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
|
||||||
|
|
||||||
List<QsvSubject> subjects = dao.query(QsvSubject.class, cnd);
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, cnd);
|
||||||
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
@@ -407,4 +398,17 @@ public class H5QsvQuizController {
|
|||||||
return Result.success(result);
|
return Result.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, java.util.function.Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-7
@@ -4,7 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
|
|||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
|
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
@@ -12,6 +12,8 @@ import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam;
|
|||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -22,7 +24,11 @@ import org.nutz.mvc.annotation.Param;
|
|||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@At("/platform/h5/qsv/survey")
|
@At("/platform/h5/qsv/survey")
|
||||||
@@ -32,8 +38,6 @@ public class H5QsvSurveyController {
|
|||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private Dao dao;
|
||||||
@Inject
|
@Inject
|
||||||
private ActivityBasicScopeService activityBasicScopeService;
|
|
||||||
@Inject
|
|
||||||
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@@ -42,29 +46,37 @@ public class H5QsvSurveyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result subjects(String activityId) {
|
public Result subjects(String activityId) {
|
||||||
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (!activityBasicScopeService.isUserInGroup(activity.getGroupId(), SecurityUtil.getUserId())) {
|
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (count != 1) {
|
||||||
return Result.error("您无需参加此次投票,感谢您的关注!");
|
return Result.error("您无需参加此次投票,感谢您的关注!");
|
||||||
}
|
}
|
||||||
|
|
||||||
String answerRecordId = null;
|
String answerRecordId = null;
|
||||||
|
|
||||||
|
List<QsvSubject> activitySubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
.and("userId", "=", SecurityUtil.getUserId()));
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
if (answerRecord == null) {
|
if (answerRecord == null) {
|
||||||
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
qsvUserAnswerRecordService.insertRecord(activityId, activitySubjects.stream().map(QsvSubject::getId).toList());
|
||||||
qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class,
|
QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class,
|
||||||
Cnd.where("activityId", "=", activityId)
|
Cnd.where("activityId", "=", activityId)
|
||||||
.and("userId", "=", SecurityUtil.getUserId()));
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
syncAnswerRecordSubjects(answerRecord2, activitySubjects);
|
||||||
|
|
||||||
answerRecordId = answerRecord2.getId();
|
answerRecordId = answerRecord2.getId();
|
||||||
|
|
||||||
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", answerRecord2.getSubjectIds()).asc("sortNum"));
|
List<QsvSubject> subjects = querySubjectsByRecordOrder(answerRecord2.getSubjectIds());
|
||||||
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
|
||||||
for (QsvSubject subject : subjects) {
|
for (QsvSubject subject : subjects) {
|
||||||
@@ -77,6 +89,95 @@ public class H5QsvSurveyController {
|
|||||||
return Result.success(result);
|
return Result.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验调查活动是否处于可答题状态。activity 为空表示活动不存在;
|
||||||
|
* enabled 为 false 表示活动已关闭,当前时间早于 startTime 表示未开始,当前时间晚于 endTime 表示已结束。
|
||||||
|
*
|
||||||
|
* @param activity 当前调查活动,包含开启状态、开始时间和结束时间
|
||||||
|
* @return 状态或时间不允许答题时返回错误结果,允许答题时返回 null
|
||||||
|
*/
|
||||||
|
private Result checkActivityStatus(QsvActivity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("调查不存在");
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(activity.getEnabled())) {
|
||||||
|
return Result.error("活动已关闭,暂不能参与");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
|
||||||
|
return Result.error("调查尚未开始,请在开始后再参与");
|
||||||
|
}
|
||||||
|
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
|
||||||
|
return Result.error("调查已结束");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步问卷当前题目到未完成答题记录。后台新增题目后,已进入过问卷的用户记录里没有新题ID,
|
||||||
|
* 这里补齐 subjectIds 和 extJson,保证手机端能拿到最新题目并正常提交答案。
|
||||||
|
*
|
||||||
|
* @param answerRecord 用户当前问卷答题记录
|
||||||
|
* @param activitySubjects 当前活动下按排序查询出的最新题目列表
|
||||||
|
*/
|
||||||
|
private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, List<QsvSubject> activitySubjects) {
|
||||||
|
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
List<String> recordSubjectIds = answerRecord.getSubjectIds();
|
||||||
|
if (recordSubjectIds == null) {
|
||||||
|
recordSubjectIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
|
||||||
|
List<String> normalizedSubjectIds = recordSubjectIds.stream()
|
||||||
|
.filter(currentSubjectIdSet::contains)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (!normalizedSubjectIds.contains(subjectId)) {
|
||||||
|
normalizedSubjectIds.add(subjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = !recordSubjectIds.equals(normalizedSubjectIds);
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) {
|
||||||
|
JSONObject entry = new JSONObject();
|
||||||
|
entry.set("optionIds", new ArrayList<>());
|
||||||
|
entry.set("text", null);
|
||||||
|
entry.set("optionFillContents", new JSONObject());
|
||||||
|
extJson.set(subjectId, entry);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
answerRecord.setSubjectIds(normalizedSubjectIds);
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
public Result answerRecord(@Valid String answerRecordId) {
|
public Result answerRecord(@Valid String answerRecordId) {
|
||||||
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
||||||
@@ -84,7 +185,14 @@ public class H5QsvSurveyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
|
||||||
|
Result activityStatusResult = checkActivityStatus(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
||||||
JSONObject extJson = answerRecord.getExtJson();
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
|
||||||
@@ -93,6 +201,7 @@ public class H5QsvSurveyController {
|
|||||||
if (ObjectUtil.isNotEmpty(entries)) {
|
if (ObjectUtil.isNotEmpty(entries)) {
|
||||||
entries.set("optionIds", subject.getUserSelectOptionIds());
|
entries.set("optionIds", subject.getUserSelectOptionIds());
|
||||||
entries.set("text", subject.getUserFillContent());
|
entries.set("text", subject.getUserFillContent());
|
||||||
|
entries.set("optionFillContents", subject.getOptionFillContents());
|
||||||
}
|
}
|
||||||
extJson.set(subject.getId(), entries);
|
extJson.set(subject.getId(), entries);
|
||||||
}
|
}
|
||||||
|
|||||||
+301
@@ -0,0 +1,301 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.qsv.h5controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/h5/qsv/vote")
|
||||||
|
@Ok("json:full")
|
||||||
|
public class H5QsvVoteController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
@Inject
|
||||||
|
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhghh5/dayofficework/qsv/vote/index.html")
|
||||||
|
public void index() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result subjects(String activityId) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
|
||||||
|
Result activityStatusResult = checkActivityReadable(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result scopeResult = checkScope(activity);
|
||||||
|
if (scopeResult != null) {
|
||||||
|
return scopeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QsvSubject> activitySubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (answerRecord == null) {
|
||||||
|
List<String> subjectIds = activitySubjects.stream()
|
||||||
|
.map(QsvSubject::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, subjectIds);
|
||||||
|
}
|
||||||
|
syncAnswerRecordSubjects(answerRecord, activitySubjects);
|
||||||
|
|
||||||
|
List<QsvSubject> subjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds());
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
if (subject.getUserSelectOptionIds() == null) {
|
||||||
|
subject.setUserSelectOptionIds(new ArrayList<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NutMap result = NutMap.NEW()
|
||||||
|
.addv("subjects", subjects)
|
||||||
|
.addv("answerRecordId", answerRecord.getId())
|
||||||
|
.addv("answerRecord", answerRecord)
|
||||||
|
.addv("activity", activity)
|
||||||
|
.addv("voteStats", buildVoteStats(activityId));
|
||||||
|
return Result.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
public Result answerRecord(@Valid String answerRecordId) {
|
||||||
|
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
|
||||||
|
return Result.success(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
public Result rank(@Valid String activityId) {
|
||||||
|
return Result.success(buildVoteStats(activityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
|
||||||
|
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
|
||||||
|
Result activityStatusResult = checkActivityWritable(activity);
|
||||||
|
if (activityStatusResult != null) {
|
||||||
|
return activityStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
|
||||||
|
if (answerRecord == null || !SecurityUtil.getUserId().equals(answerRecord.getUserId())) {
|
||||||
|
return Result.error("\u6295\u7968\u8bb0\u5f55\u4e0d\u5b58\u5728");
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return Result.error("\u60a8\u5df2\u5b8c\u6210\u6295\u7968");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
|
||||||
|
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (entries == null) {
|
||||||
|
entries = new JSONObject();
|
||||||
|
}
|
||||||
|
entries.set("optionIds", subject.getUserSelectOptionIds());
|
||||||
|
entries.set("text", subject.getUserFillContent());
|
||||||
|
entries.set("optionFillContents", subject.getOptionFillContents());
|
||||||
|
extJson.set(subject.getId(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
answerRecord.setAttemptDate(new Date());
|
||||||
|
answerRecord.setSubmitTime(new Date());
|
||||||
|
answerRecord.setIsFinish(true);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
|
||||||
|
return Result.success("\u6295\u7968\u6210\u529f");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkActivityReadable(QsvActivity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
return Result.error("\u6295\u7968\u4e0d\u5b58\u5728");
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(activity.getEnabled())) {
|
||||||
|
return Result.error("\u6d3b\u52a8\u5df2\u5173\u95ed\uff0c\u6682\u4e0d\u80fd\u53c2\u4e0e");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
|
||||||
|
return Result.error("\u6295\u7968\u5c1a\u672a\u5f00\u59cb\uff0c\u8bf7\u5728\u5f00\u59cb\u540e\u518d\u53c2\u4e0e");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkActivityWritable(QsvActivity activity) {
|
||||||
|
Result readableResult = checkActivityReadable(activity);
|
||||||
|
if (readableResult != null) {
|
||||||
|
return readableResult;
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
|
||||||
|
return Result.error("\u6295\u7968\u5df2\u7ed3\u675f");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result checkScope(QsvActivity activity) {
|
||||||
|
if (activity.getGroupId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
|
||||||
|
.and("userId", "=", SecurityUtil.getUserId()));
|
||||||
|
if (count != 1) {
|
||||||
|
return Result.error("\u60a8\u65e0\u9700\u53c2\u52a0\u6b64\u6b21\u6295\u7968\uff0c\u611f\u8c22\u60a8\u7684\u5173\u6ce8\uff01");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, List<QsvSubject> activitySubjects) {
|
||||||
|
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> currentSubjectIds = activitySubjects.stream()
|
||||||
|
.map(QsvSubject::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<String> recordSubjectIds = answerRecord.getSubjectIds();
|
||||||
|
if (recordSubjectIds == null) {
|
||||||
|
recordSubjectIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
extJson = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
|
||||||
|
List<String> normalizedSubjectIds = recordSubjectIds.stream()
|
||||||
|
.filter(currentSubjectIdSet::contains)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (!normalizedSubjectIds.contains(subjectId)) {
|
||||||
|
normalizedSubjectIds.add(subjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = !recordSubjectIds.equals(normalizedSubjectIds);
|
||||||
|
for (String subjectId : currentSubjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) {
|
||||||
|
JSONObject entry = new JSONObject();
|
||||||
|
entry.set("optionIds", new ArrayList<>());
|
||||||
|
entry.set("text", null);
|
||||||
|
entry.set("optionFillContents", new JSONObject());
|
||||||
|
extJson.set(subjectId, entry);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
answerRecord.setSubjectIds(normalizedSubjectIds);
|
||||||
|
answerRecord.setExtJson(extJson);
|
||||||
|
dao.update(answerRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
|
||||||
|
if (subjectIds == null || subjectIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
Map<String, QsvSubject> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue));
|
||||||
|
return subjectIds.stream()
|
||||||
|
.map(subjectMap::get)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildVoteStats(String activityId) {
|
||||||
|
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
|
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
|
||||||
|
|
||||||
|
Map<String, NutMap> optionStats = new LinkedHashMap<>();
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
if (subject.getOptions() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvOption option : subject.getOptions()) {
|
||||||
|
optionStats.put(option.getId(), NutMap.NEW()
|
||||||
|
.addv("optionId", option.getId())
|
||||||
|
.addv("subjectId", subject.getId())
|
||||||
|
.addv("text", option.getText())
|
||||||
|
.addv("imgUrl", option.getImgUrl())
|
||||||
|
.addv("votes", 0)
|
||||||
|
.addv("percent", 0D));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QsvUserAnswerRecord> records = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
for (QsvUserAnswerRecord record : records) {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (extJson == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (QsvSubject subject : subjects) {
|
||||||
|
JSONObject entry = extJson.get(subject.getId(), JSONObject.class);
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> optionIds = entry.getBeanList("optionIds", String.class);
|
||||||
|
if (optionIds == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String optionId : optionIds) {
|
||||||
|
NutMap stat = optionStats.get(optionId);
|
||||||
|
if (stat != null) {
|
||||||
|
stat.put("votes", stat.getInt("votes", 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalParticipants = records.size();
|
||||||
|
List<NutMap> rankList = new ArrayList<>(optionStats.values());
|
||||||
|
for (NutMap stat : rankList) {
|
||||||
|
int votes = stat.getInt("votes", 0);
|
||||||
|
double percent = totalParticipants == 0 ? 0D : votes * 100D / totalParticipants;
|
||||||
|
stat.put("percent", percent);
|
||||||
|
}
|
||||||
|
rankList.sort((left, right) -> Integer.compare(right.getInt("votes", 0), left.getInt("votes", 0)));
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("totalParticipants", totalParticipants)
|
||||||
|
.addv("optionCount", optionStats.size())
|
||||||
|
.addv("optionStats", optionStats)
|
||||||
|
.addv("rankList", rankList);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.models;
|
package com.budwk.app.zhgh.dayofficework.qsv.models;
|
||||||
|
|
||||||
import com.budwk.app.base.model.BaseModel;
|
import com.budwk.app.base.model.BaseModel;
|
||||||
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
|
import com.budwk.app.sys.services.SysHomeConvert;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.nutz.dao.entity.annotation.*;
|
import org.nutz.dao.entity.annotation.*;
|
||||||
@@ -14,7 +16,7 @@ import java.util.Date;
|
|||||||
@Table("qsv_activity")
|
@Table("qsv_activity")
|
||||||
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||||
@Comment("问卷调查投票活动表")
|
@Comment("问卷调查投票活动表")
|
||||||
public class QsvActivity extends BaseModel implements Serializable {
|
public class QsvActivity extends BaseModel implements Serializable, SysHomeConvert {
|
||||||
@Name
|
@Name
|
||||||
@Comment("ID")
|
@Comment("ID")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
@@ -46,6 +48,12 @@ public class QsvActivity extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.DATETIME)
|
@ColDefine(type = ColType.DATETIME)
|
||||||
private Date endTime;
|
private Date endTime;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("是否开启")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
@Default("1")
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("活动分组ID")
|
@Comment("活动分组ID")
|
||||||
@ColDefine(type = ColType.INT)
|
@ColDefine(type = ColType.INT)
|
||||||
@@ -107,4 +115,35 @@ public class QsvActivity extends BaseModel implements Serializable {
|
|||||||
@Comment("封面图片")
|
@Comment("封面图片")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 255)
|
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||||
private String cover;
|
private String cover;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Sys_home_activity covertToSysHomeActivity() {
|
||||||
|
Sys_home_activity sysHomeActivity = new Sys_home_activity();
|
||||||
|
sysHomeActivity.setId(this.getId());
|
||||||
|
sysHomeActivity.setName(this.getTitle());
|
||||||
|
sysHomeActivity.setCover(this.getCover());
|
||||||
|
sysHomeActivity.setContent(this.getDescription());
|
||||||
|
sysHomeActivity.setUrl("/platform/qsv/activity");
|
||||||
|
sysHomeActivity.setH5Url(getH5Url());
|
||||||
|
sysHomeActivity.setStartDate(this.getStartTime());
|
||||||
|
sysHomeActivity.setEndDate(this.getEndTime());
|
||||||
|
sysHomeActivity.setAllowUserGroupId(this.getGroupId());
|
||||||
|
sysHomeActivity.setEnable(!Boolean.FALSE.equals(this.getEnabled()));
|
||||||
|
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
|
||||||
|
return sysHomeActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据活动类型生成移动端首页跳转地址。
|
||||||
|
* QUIZ 跳转答题页,SURVEY 跳转调查页,VOTE 预留投票页地址,返回值为移动端路由字符串。
|
||||||
|
*/
|
||||||
|
private String getH5Url() {
|
||||||
|
if ("SURVEY".equals(this.getCategory())) {
|
||||||
|
return "/platform/h5/qsv/survey?id=" + this.getId();
|
||||||
|
}
|
||||||
|
if ("VOTE".equals(this.getCategory())) {
|
||||||
|
return "/platform/h5/qsv/vote?id=" + this.getId();
|
||||||
|
}
|
||||||
|
return "/platform/h5/qsv/quiz?id=" + this.getId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ public class QsvOption extends BaseModel implements Serializable {
|
|||||||
@ColDefine(customType = "longtext")
|
@ColDefine(customType = "longtext")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("选中后是否需要填写补充内容")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
private Boolean fillRequired;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("补充内容输入提示")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||||
|
private String fillPlaceholder;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("排序")
|
@Comment("排序")
|
||||||
@ColDefine(type = ColType.INT)
|
@ColDefine(type = ColType.INT)
|
||||||
|
|||||||
@@ -58,11 +58,41 @@ public class QsvSubject extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.INT, width = 1)
|
@ColDefine(type = ColType.INT, width = 1)
|
||||||
private Integer maxMulti;
|
private Integer maxMulti;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("投票选项排列方式")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
private String optionLayout;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("投票选项横向列数")
|
||||||
|
@ColDefine(type = ColType.INT, width = 1)
|
||||||
|
private Integer optionColumns;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("排序")
|
@Comment("排序")
|
||||||
@ColDefine(type = ColType.INT, width = 1)
|
@ColDefine(type = ColType.INT, width = 1)
|
||||||
private Integer sortNum;
|
private Integer sortNum;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("是否启用题目隐显逻辑")
|
||||||
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
private Boolean visibleRuleEnabled;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("隐显逻辑处理方式(show显示,hide隐藏)")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
private String visibleRuleAction;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("隐显逻辑条件关系(AND并且,OR或者)")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
private String visibleRuleLogic;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("隐显逻辑条件列表")
|
||||||
|
@ColDefine(type = ColType.MYSQL_JSON)
|
||||||
|
private List<VisibleRuleCondition> visibleRuleConditions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选项
|
* 选项
|
||||||
*/
|
*/
|
||||||
@@ -79,4 +109,52 @@ public class QsvSubject extends BaseModel implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String userFillContent;
|
private String userFillContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目隐显逻辑条件,一条条件表示选择指定题目的指定选项后参与显示或隐藏判断。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class VisibleRuleCondition implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作为触发条件的题目ID。
|
||||||
|
*/
|
||||||
|
private String subjectId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作为触发条件的选项ID。
|
||||||
|
*/
|
||||||
|
private String optionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件节点类型:group 分组,item 条件项。
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组内条件关系:and 并且,or 或者。
|
||||||
|
*/
|
||||||
|
private String logic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件项引用的题目ID。
|
||||||
|
*/
|
||||||
|
private String field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较符:==、!=、contains、>、<、>=、<=。
|
||||||
|
*/
|
||||||
|
private String operator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较值,选择题为选项ID,填空题可为文本或数字。
|
||||||
|
*/
|
||||||
|
private Object value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子条件节点,支持条件分组嵌套。
|
||||||
|
*/
|
||||||
|
private List<VisibleRuleCondition> children;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.dayofficework.qsv.param;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
//答题参数
|
//答题参数
|
||||||
@@ -21,6 +22,7 @@ public class QsvAnswerParam {
|
|||||||
private String id;
|
private String id;
|
||||||
private List<String> userSelectOptionIds;
|
private List<String> userSelectOptionIds;
|
||||||
private String userFillContent;
|
private String userFillContent;
|
||||||
|
private Map<String, String> optionFillContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,10 @@ import com.budwk.app.base.service.BaseService;
|
|||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
|
||||||
public interface QsvActivityService extends BaseService<QsvActivity> {
|
public interface QsvActivityService extends BaseService<QsvActivity> {
|
||||||
|
|
||||||
|
QsvActivity saveActivity(QsvActivity qsvActivity);
|
||||||
|
|
||||||
|
void deleteActivity(String id);
|
||||||
|
|
||||||
|
void updateEnabled(String id, Boolean enabled);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ public interface QsvSurveyService extends BaseService<QsvUserAnswerRecord> {
|
|||||||
|
|
||||||
List<NutMap> report(String activityId);
|
List<NutMap> report(String activityId);
|
||||||
|
|
||||||
|
List<NutMap> categoryReport(String activityId, String conditions);
|
||||||
|
|
||||||
|
NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions);
|
||||||
|
|
||||||
|
NutMap compareReport(String activityId, String subjectId, String optionId);
|
||||||
|
|
||||||
|
List<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions);
|
||||||
|
|
||||||
void exportReportXlsx(String activityId, HttpServletResponse response);
|
void exportReportXlsx(String activityId, HttpServletResponse response);
|
||||||
|
|
||||||
void exportUserAnswerXlsx(String activityId, HttpServletResponse response);
|
void exportUserAnswerXlsx(String activityId, HttpServletResponse response);
|
||||||
|
|||||||
+67
@@ -1,15 +1,82 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.qsv.service.impl;
|
package com.budwk.app.zhgh.dayofficework.qsv.service.impl;
|
||||||
|
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
|
||||||
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> implements QsvActivityService {
|
public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> implements QsvActivityService {
|
||||||
|
|
||||||
public QsvActivityServiceImpl(Dao dao) {
|
public QsvActivityServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public QsvActivity saveActivity(QsvActivity qsvActivity) {
|
||||||
|
fillQuizRepeatMode(qsvActivity);
|
||||||
|
dao().insertOrUpdate(qsvActivity);
|
||||||
|
syncHomeActivity(qsvActivity);
|
||||||
|
return qsvActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void deleteActivity(String id) {
|
||||||
|
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", id));
|
||||||
|
List<String> subjectIds = subjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
|
||||||
|
if (!subjectIds.isEmpty()) {
|
||||||
|
dao().clear(QsvOption.class, Cnd.where("subjectId", "in", subjectIds));
|
||||||
|
dao().clear(QsvSubject.class, Cnd.where("id", "in", subjectIds));
|
||||||
|
}
|
||||||
|
dao().clear(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().delete(QsvActivity.class, id);
|
||||||
|
dao().delete(Sys_home_activity.class, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void updateEnabled(String id, Boolean enabled) {
|
||||||
|
QsvActivity activity = dao().fetch(QsvActivity.class, id);
|
||||||
|
if (activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activity.setEnabled(enabled);
|
||||||
|
dao().update(activity);
|
||||||
|
syncHomeActivity(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 答题活动的重复模式由出题模式决定。定时定题按天统计,常规模式按活动总次数统计。
|
||||||
|
*/
|
||||||
|
private void fillQuizRepeatMode(QsvActivity qsvActivity) {
|
||||||
|
if (!"QUIZ".equals(qsvActivity.getCategory())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("SCHEDULED".equals(qsvActivity.getMode())) {
|
||||||
|
qsvActivity.setRepeatMode("DAILY");
|
||||||
|
} else if ("REGULAR".equals(qsvActivity.getMode())) {
|
||||||
|
qsvActivity.setRepeatMode("TOTAL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步移动端首页活动。问卷活动保存或启停后,都以活动自身 enabled、时间、人员范围生成首页记录。
|
||||||
|
*/
|
||||||
|
private void syncHomeActivity(QsvActivity activity) {
|
||||||
|
Sys_home_activity sysHomeActivity = activity.covertToSysHomeActivity();
|
||||||
|
dao().insertOrUpdate(sysHomeActivity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-11
@@ -119,17 +119,11 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
|
|||||||
cnd.andEX("t1.unionId","=",pageForm.getUnionId());
|
cnd.andEX("t1.unionId","=",pageForm.getUnionId());
|
||||||
cnd.andEX("t1.unitId","=",pageForm.getUnitId());
|
cnd.andEX("t1.unitId","=",pageForm.getUnitId());
|
||||||
|
|
||||||
if ("SCHEDULED".equals(mode)) {
|
if ("SCHEDULED".equals(mode) || "REGULAR".equals(mode)) {
|
||||||
if ("HIGH".equals(scoreStatisticsMode)) {
|
if ("HIGH".equals(scoreStatisticsMode) || "HIGHEST".equals(scoreStatisticsMode)) {
|
||||||
cnd.and("t1.isHighestScore","=",1);
|
cnd.and("t1.isHighestScore", "=", 1);
|
||||||
} else if ("LAST".equals(scoreStatisticsMode)) {
|
} else {
|
||||||
cnd.and("t1.isLatestScore","=",1);
|
cnd.and("t1.isLatestScore", "=", 1);
|
||||||
}
|
|
||||||
} else if ("REGULAR".equals(mode)) {
|
|
||||||
if ("HIGH".equals(scoreStatisticsMode)) {
|
|
||||||
cnd.and("t1.isHighestScore","=",1);
|
|
||||||
} else if ("LAST".equals(scoreStatisticsMode)) {
|
|
||||||
cnd.and("t1.isLatestScore","=",1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+493
-39
@@ -8,6 +8,7 @@ import cn.afterturn.easypoi.excel.export.ExcelExportService;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.json.JSONArray;
|
import cn.hutool.json.JSONArray;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
@@ -68,47 +69,47 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
// 处理每个题目
|
// 处理每个题目
|
||||||
for (NutMap subject : subjects) {
|
for (NutMap subject : subjects) {
|
||||||
// 获取当前题目的选项列表
|
// 获取当前题目的选项列表
|
||||||
List<NutMap> subjectOptions = Lang.collection2list(optionsGroup.get(subject.getString("id")), NutMap.class);
|
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
|
||||||
|
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
|
||||||
|
|
||||||
// 获取题目类型
|
// 获取题目类型
|
||||||
String subjectType = subject.getString("type");
|
String subjectType = subject.getString("type");
|
||||||
|
|
||||||
// 处理文本类型题目
|
// 处理文本类型题目
|
||||||
if ("text".equals(subjectType)) {
|
if ("text".equals(subjectType)) {
|
||||||
List<String> texts = answerExtList.stream()
|
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
|
||||||
.filter(ext -> ObjectUtil.isNull(ext.get(subject.getString("id"), JSONObject.class)))
|
List<String> texts = textAnswers.stream()
|
||||||
.map(ext -> ext.get(subject.getString("id"), JSONObject.class).getStr("text"))
|
.map(textAnswer -> textAnswer.getString("text"))
|
||||||
.toList();
|
.toList();
|
||||||
subject.put("texts", texts);
|
subject.put("texts", texts);
|
||||||
|
subject.put("textAnswers", textAnswers);
|
||||||
}
|
}
|
||||||
// 处理单选类型题目 处理多选类型题目
|
// 处理单选类型题目 处理多选类型题目
|
||||||
else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
|
else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
|
||||||
long selectTotal = answerExtList.stream()
|
long selectTotal = answerExtList.stream()
|
||||||
.filter(ext -> ext.containsKey(subject.getString("id")))
|
.filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id")))
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
subjectOptions.forEach(subjectOption -> {
|
subjectOptions.forEach(subjectOption -> {
|
||||||
long selectCount = answerExtList.stream()
|
long selectCount = answerExtList.stream()
|
||||||
.filter(ext ->
|
.filter(ext ->
|
||||||
{
|
{
|
||||||
|
if (ObjectUtil.isEmpty(ext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
|
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
|
||||||
return jsonObject != null
|
return jsonObject != null
|
||||||
&& jsonObject.getJSONArray("optionIds") != null
|
&& jsonObject.getJSONArray("optionIds") != null
|
||||||
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
|
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
|
||||||
})
|
})
|
||||||
.count();
|
.count();
|
||||||
long round = Math.round((double) selectCount / selectTotal * 100);
|
long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100);
|
||||||
subjectOption.put("selectPercent", round + "%");
|
subjectOption.put("selectPercent", round + "%");
|
||||||
subjectOption.put("selectCount", selectCount);
|
subjectOption.put("selectCount", selectCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
subjectOptions.sort((o1, o2) -> {
|
subjectOptions.sort((o1, o2) -> {
|
||||||
int count1 = o1.getInt("selectCount");
|
return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum"));
|
||||||
int count2 = o2.getInt("selectCount");
|
|
||||||
if (count1 == count2) {
|
|
||||||
return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum"));
|
|
||||||
}
|
|
||||||
return Integer.compare(count2, count1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
subject.put("selectTotal", selectTotal);
|
subject.put("selectTotal", selectTotal);
|
||||||
@@ -125,6 +126,228 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NutMap> categoryReport(String activityId, String conditions) {
|
||||||
|
if (activityId == null || activityId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("activityId不能为空");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<NutMap> conditionList = parseCategoryConditions(conditions);
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
return buildReport(activityId, filterAnswerRecords(answerRecords, conditionList));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("分类报告生成失败", e);
|
||||||
|
throw new RuntimeException("分类报告生成失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions) {
|
||||||
|
if (activityId == null || activityId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("activityId不能为空");
|
||||||
|
}
|
||||||
|
List<String> xSubjectIdList = parseSubjectIds(xSubjectIds);
|
||||||
|
List<String> ySubjectIdList = parseSubjectIds(ySubjectIds);
|
||||||
|
if (ObjectUtil.isEmpty(xSubjectIdList) || ObjectUtil.isEmpty(ySubjectIdList)) {
|
||||||
|
throw new IllegalArgumentException("请选择自变量X和因变量Y");
|
||||||
|
}
|
||||||
|
if (xSubjectIdList.stream().anyMatch(ySubjectIdList::contains)) {
|
||||||
|
throw new IllegalArgumentException("自变量X和因变量Y不能选择相同变量");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<NutMap> subjects = querySubjects(activityId);
|
||||||
|
Map<String, NutMap> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject));
|
||||||
|
if (!subjectMap.keySet().containsAll(xSubjectIdList) || !subjectMap.keySet().containsAll(ySubjectIdList)) {
|
||||||
|
throw new IllegalArgumentException("题目不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> allSubjectIds = new ArrayList<>();
|
||||||
|
allSubjectIds.addAll(xSubjectIdList);
|
||||||
|
allSubjectIds.addAll(ySubjectIdList);
|
||||||
|
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", allSubjectIds).asc("sortNum"));
|
||||||
|
Map<String, List<QsvOption>> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
|
||||||
|
List<NutMap> xOptionMaps = buildVariableOptionCombinations(xSubjectIdList, subjectMap, optionGroup);
|
||||||
|
List<NutMap> conditionList = parseCategoryConditions(conditions);
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, conditionList);
|
||||||
|
List<NutMap> reports = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String ySubjectId : ySubjectIdList) {
|
||||||
|
NutMap ySubject = subjectMap.get(ySubjectId);
|
||||||
|
List<QsvOption> yOptions = optionGroup.get(ySubjectId);
|
||||||
|
if (ObjectUtil.isEmpty(yOptions)) {
|
||||||
|
yOptions = new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<NutMap> yOptionMaps = ObjectUtil.isEmpty(yOptions) ? new ArrayList<>() : Lang.collection2list(yOptions, NutMap.class);
|
||||||
|
List<NutMap> rows = new ArrayList<>();
|
||||||
|
|
||||||
|
for (NutMap xOption : xOptionMaps) {
|
||||||
|
List<NutMap> xConditions = xOption.getList("conditions", NutMap.class);
|
||||||
|
List<QsvUserAnswerRecord> xMatchedRecords = filteredRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, xConditions))
|
||||||
|
.toList();
|
||||||
|
long rowTotal = xMatchedRecords.size();
|
||||||
|
List<NutMap> cells = new ArrayList<>();
|
||||||
|
for (QsvOption yOption : yOptions) {
|
||||||
|
List<NutMap> yCondition = List.of(NutMap.NEW()
|
||||||
|
.addv("subjectId", ySubjectId)
|
||||||
|
.addv("optionId", yOption.getId()));
|
||||||
|
long count = xMatchedRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, yCondition))
|
||||||
|
.count();
|
||||||
|
double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0;
|
||||||
|
cells.add(NutMap.NEW()
|
||||||
|
.addv("yOptionId", yOption.getId())
|
||||||
|
.addv("yOptionText", yOption.getText())
|
||||||
|
.addv("count", count)
|
||||||
|
.addv("percent", percent));
|
||||||
|
}
|
||||||
|
rows.add(NutMap.NEW()
|
||||||
|
.addv("xOptionId", xOption.getString("id"))
|
||||||
|
.addv("xOptionText", xOption.getString("text"))
|
||||||
|
.addv("cells", cells)
|
||||||
|
.addv("total", rowTotal));
|
||||||
|
}
|
||||||
|
|
||||||
|
reports.add(NutMap.NEW()
|
||||||
|
.addv("ySubject", ySubject)
|
||||||
|
.addv("yOptions", yOptionMaps)
|
||||||
|
.addv("rows", rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("xSubject", buildVariableSubject(xSubjectIdList, subjectMap))
|
||||||
|
.addv("xOptions", xOptionMaps)
|
||||||
|
.addv("reports", reports)
|
||||||
|
.addv("total", filteredRecords.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("交叉分析报告生成失败", e);
|
||||||
|
throw new RuntimeException("交叉分析报告生成失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap compareReport(String activityId, String subjectId, String optionId) {
|
||||||
|
if (activityId == null || activityId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("activityId不能为空");
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isEmpty(subjectId) || ObjectUtil.isEmpty(optionId)) {
|
||||||
|
throw new IllegalArgumentException("请选择对比变量和选项");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<NutMap> subjects = querySubjects(activityId);
|
||||||
|
Map<String, NutMap> subjectMap = subjects.stream()
|
||||||
|
.collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject));
|
||||||
|
NutMap compareSubject = subjectMap.get(subjectId);
|
||||||
|
if (compareSubject == null) {
|
||||||
|
throw new IllegalArgumentException("对比变量不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> subjectIds = subjects.stream().map(subject -> subject.getString("id")).toList();
|
||||||
|
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
|
||||||
|
Map<String, List<QsvOption>> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
|
||||||
|
QsvOption compareOption = optionGroup.getOrDefault(subjectId, new ArrayList<>()).stream()
|
||||||
|
.filter(option -> optionId.equals(option.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (compareOption == null) {
|
||||||
|
throw new IllegalArgumentException("对比选项不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<NutMap> compareCondition = List.of(NutMap.NEW()
|
||||||
|
.addv("subjectId", subjectId)
|
||||||
|
.addv("optionId", optionId));
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, compareCondition);
|
||||||
|
List<NutMap> reports = new ArrayList<>();
|
||||||
|
|
||||||
|
for (NutMap subject : subjects) {
|
||||||
|
String currentSubjectId = subject.getString("id");
|
||||||
|
String subjectType = subject.getString("type");
|
||||||
|
if (subjectId.equals(currentSubjectId) || (!"radio".equals(subjectType) && !"checkbox".equals(subjectType))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<QsvOption> currentOptions = optionGroup.get(currentSubjectId);
|
||||||
|
if (ObjectUtil.isEmpty(currentOptions)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<NutMap> cells = new ArrayList<>();
|
||||||
|
long rowTotal = filteredRecords.size();
|
||||||
|
for (QsvOption option : currentOptions) {
|
||||||
|
List<NutMap> optionCondition = List.of(NutMap.NEW()
|
||||||
|
.addv("subjectId", currentSubjectId)
|
||||||
|
.addv("optionId", option.getId()));
|
||||||
|
long count = filteredRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, optionCondition))
|
||||||
|
.count();
|
||||||
|
double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0;
|
||||||
|
cells.add(NutMap.NEW()
|
||||||
|
.addv("yOptionId", option.getId())
|
||||||
|
.addv("yOptionText", option.getText())
|
||||||
|
.addv("count", count)
|
||||||
|
.addv("percent", percent));
|
||||||
|
}
|
||||||
|
reports.add(NutMap.NEW()
|
||||||
|
.addv("ySubject", subject)
|
||||||
|
.addv("yOptions", Lang.collection2list(currentOptions, NutMap.class))
|
||||||
|
.addv("rows", List.of(NutMap.NEW()
|
||||||
|
.addv("xOptionId", optionId)
|
||||||
|
.addv("xOptionText", compareOption.getText())
|
||||||
|
.addv("cells", cells)
|
||||||
|
.addv("total", rowTotal))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("compareSubject", compareSubject)
|
||||||
|
.addv("compareOption", Lang.obj2nutmap(compareOption))
|
||||||
|
.addv("reports", reports)
|
||||||
|
.addv("total", filteredRecords.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("对比分析报告生成失败", e);
|
||||||
|
throw new RuntimeException("对比分析报告生成失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions) {
|
||||||
|
List<NutMap> conditionList = parseCategoryConditions(conditions);
|
||||||
|
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
|
||||||
|
.and("isFinish", "=", true));
|
||||||
|
return filterAnswerRecords(answerRecords, conditionList).stream().map(record -> {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(subjectAnswer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONArray optionIds = subjectAnswer.getJSONArray("optionIds");
|
||||||
|
if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(optionId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject optionFillContents = subjectAnswer.getJSONObject("optionFillContents");
|
||||||
|
String fillContent = ObjectUtil.isEmpty(optionFillContents) ? "" : optionFillContents.getStr(optionId);
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("userName", record.getUserName())
|
||||||
|
.addv("loginName", record.getLoginName())
|
||||||
|
.addv("unionName", record.getUnionName())
|
||||||
|
.addv("unitName", record.getUnitName())
|
||||||
|
.addv("attemptDate", record.getAttemptDate())
|
||||||
|
.addv("submitTime", record.getSubmitTime())
|
||||||
|
.addv("fillContent", fillContent);
|
||||||
|
})
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exportReportXlsx(String activityId, HttpServletResponse response) {
|
public void exportReportXlsx(String activityId, HttpServletResponse response) {
|
||||||
List<NutMap> report = report(activityId);
|
List<NutMap> report = report(activityId);
|
||||||
@@ -134,10 +357,8 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20));
|
exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20));
|
||||||
exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20));
|
exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20));
|
||||||
|
|
||||||
Map<String, List<NutMap>> listMap = report.stream().collect(Collectors.groupingBy(v -> v.getString("id")));
|
|
||||||
Workbook workbook = new XSSFWorkbook();
|
Workbook workbook = new XSSFWorkbook();
|
||||||
listMap.forEach((k, v) -> {
|
report.forEach(nutMap -> {
|
||||||
NutMap nutMap = v.get(0);
|
|
||||||
ExcelExportService service = new ExcelExportService();
|
ExcelExportService service = new ExcelExportService();
|
||||||
ExportParams exportParams = new ExportParams();
|
ExportParams exportParams = new ExportParams();
|
||||||
exportParams.setTitle(nutMap.getString("title"));
|
exportParams.setTitle(nutMap.getString("title"));
|
||||||
@@ -171,7 +392,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
||||||
|
|
||||||
// 查询活动题目列表
|
// 查询活动题目列表
|
||||||
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
|
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
// 将题目列表转换为Map
|
// 将题目列表转换为Map
|
||||||
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
||||||
// 获取题目ID列表
|
// 获取题目ID列表
|
||||||
@@ -185,7 +406,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
// 构建Excel导出实体
|
// 构建Excel导出实体
|
||||||
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
|
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
|
||||||
// 构建答题记录列表
|
// 构建答题记录列表
|
||||||
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, options);
|
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, optionMap);
|
||||||
|
|
||||||
// 设置导出参数
|
// 设置导出参数
|
||||||
ExportParams exportParams = new ExportParams();
|
ExportParams exportParams = new ExportParams();
|
||||||
@@ -223,7 +444,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
// List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
// List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
||||||
|
|
||||||
// 查询活动题目列表
|
// 查询活动题目列表
|
||||||
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
|
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
|
||||||
// 将题目列表转换为Map
|
// 将题目列表转换为Map
|
||||||
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
|
||||||
// 获取题目ID列表
|
// 获取题目ID列表
|
||||||
@@ -245,18 +466,15 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
.addv("unitName", record.getString("unitName"))
|
.addv("unitName", record.getString("unitName"))
|
||||||
.addv("unionName", record.getString("unionName"));
|
.addv("unionName", record.getString("unionName"));
|
||||||
JSONObject extJson = record.getAs("extJson", JSONObject.class);
|
JSONObject extJson = record.getAs("extJson", JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
extJson.forEach((k, v) -> {
|
extJson.forEach((k, v) -> {
|
||||||
JSONObject jsonVal = (JSONObject) v;
|
QsvSubject subject = subjectMap.get(k);
|
||||||
|
if (subject == null || !(v instanceof JSONObject jsonVal)) {
|
||||||
String type = subjectMap.get(k).getType();
|
return;
|
||||||
if (type.equals("text")) {
|
|
||||||
map.addv(k, jsonVal.getStr("text"));
|
|
||||||
} else if (type.equals("radio") || type.equals("checkbox")) {
|
|
||||||
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
|
|
||||||
String selectOptionTexts = options.stream().filter(option -> optionIds.contains(option.getId()))
|
|
||||||
.map(QsvOption::getText).collect(Collectors.joining(";"));
|
|
||||||
map.addv(k, selectOptionTexts);
|
|
||||||
}
|
}
|
||||||
|
map.addv(k, buildAnswerText(jsonVal, subject, optionMap));
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -278,6 +496,171 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<NutMap> buildReport(String activityId, List<QsvUserAnswerRecord> answerRecords) {
|
||||||
|
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
|
||||||
|
List<NutMap> subjects = querySubjects(activityId);
|
||||||
|
List<String> subjectIds = subjects.stream().map(v -> v.getString("id")).toList();
|
||||||
|
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
|
||||||
|
Map<String, List<QsvOption>> optionsGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
|
||||||
|
|
||||||
|
for (NutMap subject : subjects) {
|
||||||
|
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
|
||||||
|
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
|
||||||
|
String subjectType = subject.getString("type");
|
||||||
|
if ("text".equals(subjectType)) {
|
||||||
|
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
|
||||||
|
List<String> texts = textAnswers.stream()
|
||||||
|
.map(textAnswer -> textAnswer.getString("text"))
|
||||||
|
.toList();
|
||||||
|
subject.put("texts", texts);
|
||||||
|
subject.put("textAnswers", textAnswers);
|
||||||
|
} else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
|
||||||
|
long selectTotal = answerExtList.stream()
|
||||||
|
.filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id")))
|
||||||
|
.count();
|
||||||
|
subjectOptions.forEach(subjectOption -> {
|
||||||
|
long selectCount = answerExtList.stream()
|
||||||
|
.filter(ext -> {
|
||||||
|
if (ObjectUtil.isEmpty(ext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
|
||||||
|
return jsonObject != null
|
||||||
|
&& jsonObject.getJSONArray("optionIds") != null
|
||||||
|
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100);
|
||||||
|
subjectOption.put("selectPercent", round + "%");
|
||||||
|
subjectOption.put("selectCount", selectCount);
|
||||||
|
});
|
||||||
|
subjectOptions.sort((o1, o2) -> Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum")));
|
||||||
|
subject.put("selectTotal", selectTotal);
|
||||||
|
}
|
||||||
|
subject.addv("options", subjectOptions);
|
||||||
|
}
|
||||||
|
return subjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NutMap> parseCategoryConditions(String conditions) {
|
||||||
|
if (ObjectUtil.isEmpty(conditions)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return JSONUtil.parseArray(conditions).toList(NutMap.class).stream()
|
||||||
|
.filter(condition -> ObjectUtil.isNotEmpty(condition.getString("subjectId"))
|
||||||
|
&& ObjectUtil.isNotEmpty(condition.getString("optionId")))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseSubjectIds(String subjectIds) {
|
||||||
|
if (ObjectUtil.isEmpty(subjectIds)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
String value = subjectIds.trim();
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (value.startsWith("[")) {
|
||||||
|
for (Object subjectId : JSONUtil.parseArray(value)) {
|
||||||
|
if (ObjectUtil.isNotEmpty(subjectId)) {
|
||||||
|
result.add(String.valueOf(subjectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.stream().distinct().toList();
|
||||||
|
}
|
||||||
|
for (String subjectId : value.split(",")) {
|
||||||
|
if (ObjectUtil.isNotEmpty(subjectId)) {
|
||||||
|
result.add(subjectId.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.stream().distinct().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NutMap buildVariableSubject(List<String> subjectIds, Map<String, NutMap> subjectMap) {
|
||||||
|
String title = subjectIds.stream()
|
||||||
|
.map(subjectId -> subjectMap.get(subjectId).getString("title"))
|
||||||
|
.collect(Collectors.joining(" / "));
|
||||||
|
return NutMap.NEW()
|
||||||
|
.addv("id", String.join("|", subjectIds))
|
||||||
|
.addv("title", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NutMap> buildVariableOptionCombinations(List<String> subjectIds, Map<String, NutMap> subjectMap, Map<String, List<QsvOption>> optionGroup) {
|
||||||
|
List<List<NutMap>> subjectOptionGroups = new ArrayList<>();
|
||||||
|
for (String subjectId : subjectIds) {
|
||||||
|
NutMap subject = subjectMap.get(subjectId);
|
||||||
|
List<QsvOption> options = optionGroup.get(subjectId);
|
||||||
|
if (ObjectUtil.isEmpty(options)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<NutMap> optionMaps = options.stream()
|
||||||
|
.map(option -> NutMap.NEW()
|
||||||
|
.addv("subjectId", subjectId)
|
||||||
|
.addv("subjectTitle", subject.getString("title"))
|
||||||
|
.addv("optionId", option.getId())
|
||||||
|
.addv("optionText", option.getText()))
|
||||||
|
.toList();
|
||||||
|
subjectOptionGroups.add(optionMaps);
|
||||||
|
}
|
||||||
|
if (subjectOptionGroups.size() != subjectIds.size()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<NutMap> result = new ArrayList<>();
|
||||||
|
buildVariableOptionCombinations(subjectOptionGroups, 0, new ArrayList<>(), result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildVariableOptionCombinations(List<List<NutMap>> subjectOptionGroups, int index, List<NutMap> current, List<NutMap> result) {
|
||||||
|
if (index >= subjectOptionGroups.size()) {
|
||||||
|
String id = current.stream()
|
||||||
|
.map(option -> option.getString("subjectId") + ":" + option.getString("optionId"))
|
||||||
|
.collect(Collectors.joining("|"));
|
||||||
|
String text = current.stream()
|
||||||
|
.map(option -> option.getString("optionText"))
|
||||||
|
.collect(Collectors.joining(" / "));
|
||||||
|
List<NutMap> conditions = current.stream()
|
||||||
|
.map(option -> NutMap.NEW()
|
||||||
|
.addv("subjectId", option.getString("subjectId"))
|
||||||
|
.addv("optionId", option.getString("optionId")))
|
||||||
|
.toList();
|
||||||
|
result.add(NutMap.NEW()
|
||||||
|
.addv("id", id)
|
||||||
|
.addv("text", text)
|
||||||
|
.addv("conditions", conditions));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (NutMap option : subjectOptionGroups.get(index)) {
|
||||||
|
current.add(option);
|
||||||
|
buildVariableOptionCombinations(subjectOptionGroups, index + 1, current, result);
|
||||||
|
current.remove(current.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QsvUserAnswerRecord> filterAnswerRecords(List<QsvUserAnswerRecord> answerRecords, List<NutMap> conditions) {
|
||||||
|
if (ObjectUtil.isEmpty(conditions)) {
|
||||||
|
return answerRecords;
|
||||||
|
}
|
||||||
|
return answerRecords.stream()
|
||||||
|
.filter(record -> isRecordMatched(record, conditions))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRecordMatched(QsvUserAnswerRecord record, List<NutMap> conditions) {
|
||||||
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (NutMap condition : conditions) {
|
||||||
|
JSONObject subjectAnswer = extJson.get(condition.getString("subjectId"), JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(subjectAnswer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JSONArray optionIds = subjectAnswer.getJSONArray("optionIds");
|
||||||
|
if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(condition.getString("optionId"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private List<NutMap> querySubjects(String activityId) {
|
private List<NutMap> querySubjects(String activityId) {
|
||||||
Sql subjectSql = Sqls.create("""
|
Sql subjectSql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -309,7 +692,37 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
return excelExportEntities;
|
return excelExportEntities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, List<QsvOption> options) {
|
/**
|
||||||
|
* 构建填空题填写详情。分析弹窗需要同时展示填写内容和答题人信息,
|
||||||
|
* 因此这里从已完成答题记录中提取当前题目的文本答案,并携带姓名、工号、所属工会和单位。
|
||||||
|
*
|
||||||
|
* @param answerRecords 当前活动已完成的答题记录
|
||||||
|
* @param subjectId 当前填空题ID,用于从 extJson 中取出该题填写内容
|
||||||
|
* @return 填空题填写详情列表,每条包含 loginName、userName、unionName、unitName、submitTime、text
|
||||||
|
*/
|
||||||
|
private List<NutMap> buildTextAnswerDetails(List<QsvUserAnswerRecord> answerRecords, String subjectId) {
|
||||||
|
List<NutMap> textAnswers = new ArrayList<>();
|
||||||
|
for (QsvUserAnswerRecord answerRecord : answerRecords) {
|
||||||
|
JSONObject extJson = answerRecord.getExtJson();
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class);
|
||||||
|
if (ObjectUtil.isEmpty(subjectAnswer) || ObjectUtil.isEmpty(subjectAnswer.getStr("text"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
textAnswers.add(NutMap.NEW()
|
||||||
|
.addv("loginName", answerRecord.getLoginName())
|
||||||
|
.addv("userName", answerRecord.getUserName())
|
||||||
|
.addv("unionName", answerRecord.getUnionName())
|
||||||
|
.addv("unitName", answerRecord.getUnitName())
|
||||||
|
.addv("submitTime", answerRecord.getSubmitTime())
|
||||||
|
.addv("text", subjectAnswer.getStr("text")));
|
||||||
|
}
|
||||||
|
return textAnswers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, Map<String, QsvOption> optionMap) {
|
||||||
return answerRecords.stream().map(record -> {
|
return answerRecords.stream().map(record -> {
|
||||||
NutMap map = NutMap.NEW()
|
NutMap map = NutMap.NEW()
|
||||||
.addv("id", record.getId())
|
.addv("id", record.getId())
|
||||||
@@ -318,20 +731,61 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
|
|||||||
.addv("unitName", record.getUnitName())
|
.addv("unitName", record.getUnitName())
|
||||||
.addv("unionName", record.getUnionName());
|
.addv("unionName", record.getUnionName());
|
||||||
JSONObject extJson = record.getExtJson();
|
JSONObject extJson = record.getExtJson();
|
||||||
|
if (ObjectUtil.isEmpty(extJson)) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
extJson.forEach((k, v) -> {
|
extJson.forEach((k, v) -> {
|
||||||
JSONObject jsonVal = (JSONObject) v;
|
QsvSubject subject = subjectMap.get(k);
|
||||||
|
if (subject == null || !(v instanceof JSONObject jsonVal)) {
|
||||||
String type = subjectMap.get(k).getType();
|
return;
|
||||||
if (type.equals("text")) {
|
|
||||||
map.addv(k, jsonVal.getStr("text"));
|
|
||||||
} else if (type.equals("radio") || type.equals("checkbox")) {
|
|
||||||
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
|
|
||||||
String selectOptionTexts = options.stream().filter(option -> optionIds.contains(option.getId()))
|
|
||||||
.map(QsvOption::getText).collect(Collectors.joining(";"));
|
|
||||||
map.addv(k, selectOptionTexts);
|
|
||||||
}
|
}
|
||||||
|
map.addv(k, buildAnswerText(jsonVal, subject, optionMap));
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装单题答案展示内容。填空题返回填写文本;选择题返回选项文本,若选项配置了补充填写,
|
||||||
|
* 则追加对应补充内容,保证后台查看答卷和导出能看到用户填写的说明。
|
||||||
|
*
|
||||||
|
* @param jsonVal 答题记录 extJson 中当前题目的答案对象,包含 optionIds、text、optionFillContents
|
||||||
|
* @param subject 当前题目,用于判断题目类型
|
||||||
|
* @param optionMap 当前活动所有选项,key 为选项ID,value 为选项实体
|
||||||
|
* @return 当前题目的答案展示文本
|
||||||
|
*/
|
||||||
|
private String buildAnswerText(JSONObject jsonVal, QsvSubject subject, Map<String, QsvOption> optionMap) {
|
||||||
|
String type = subject.getType();
|
||||||
|
if ("text".equals(type)) {
|
||||||
|
return jsonVal.getStr("text");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"radio".equals(type) && !"checkbox".equals(type)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
|
||||||
|
if (ObjectUtil.isEmpty(optionIds)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject optionFillContents = jsonVal.getJSONObject("optionFillContents");
|
||||||
|
List<String> answerTexts = new ArrayList<>();
|
||||||
|
for (Object optionIdObj : optionIds) {
|
||||||
|
String optionId = String.valueOf(optionIdObj);
|
||||||
|
QsvOption option = optionMap.get(optionId);
|
||||||
|
if (option == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String answerText = option.getText();
|
||||||
|
if (ObjectUtil.isNotEmpty(optionFillContents)) {
|
||||||
|
String fillContent = optionFillContents.getStr(optionId);
|
||||||
|
if (ObjectUtil.isNotEmpty(fillContent)) {
|
||||||
|
answerText = answerText + ":" + fillContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
answerTexts.add(answerText);
|
||||||
|
}
|
||||||
|
return String.join(";", answerTexts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -121,11 +121,12 @@ public class QsvUserAnswerRecordServiceImpl extends BaseServiceImpl<QsvUserAnswe
|
|||||||
|
|
||||||
String mode = activity.getMode();
|
String mode = activity.getMode();
|
||||||
String scoreMode = activity.getScoreMode();
|
String scoreMode = activity.getScoreMode();
|
||||||
|
boolean highestScoreMode = "HIGH".equals(scoreMode) || "HIGHEST".equals(scoreMode);
|
||||||
|
|
||||||
if (mode.equals("SCHEDULED")) {
|
if (mode.equals("SCHEDULED")) {
|
||||||
processScoreRecords(activityId, SecurityUtil.getUserId(), true, scoreMode.equals("HIGH"));
|
processScoreRecords(activityId, SecurityUtil.getUserId(), true, highestScoreMode);
|
||||||
} else if (mode.equals("REGULAR")) {
|
} else if (mode.equals("REGULAR")) {
|
||||||
processScoreRecords(activityId, SecurityUtil.getUserId(), false, scoreMode.equals("HIGH"));
|
processScoreRecords(activityId, SecurityUtil.getUserId(), false, highestScoreMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+56
@@ -7,6 +7,8 @@ import com.alibaba.excel.EasyExcel;
|
|||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.sys.models.Sys_union;
|
||||||
|
import com.budwk.app.sys.models.Sys_union_cadre;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.models.MemberHistory;
|
import com.budwk.app.zhgh.staffmanage.member.models.MemberHistory;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm;
|
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService;
|
import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService;
|
||||||
@@ -14,6 +16,7 @@ import com.budwk.app.zhgh.staffmanage.member.vo.MemberImportVo;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
@@ -30,6 +33,7 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @version 1.0
|
* @version 1.0
|
||||||
@@ -72,6 +76,58 @@ public class MemberInfoGroupController {
|
|||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("member.info.group")
|
||||||
|
public Result unionOverview(String unionId) {
|
||||||
|
Sys_union union = memberInfoService.dao().fetch(Sys_union.class, unionId);
|
||||||
|
if (union == null) {
|
||||||
|
return Result.success(NutMap.NEW());
|
||||||
|
}
|
||||||
|
|
||||||
|
int staffTotal = memberInfoService.count(Sqls.create("""
|
||||||
|
select count(1)
|
||||||
|
from vw_user u
|
||||||
|
where u.unionId = @unionId
|
||||||
|
and u.personType in (
|
||||||
|
select dict.code
|
||||||
|
from sys_dict dict
|
||||||
|
left join sys_dict parent on parent.id = dict.parentId
|
||||||
|
where parent.code = 'USER_PERSON_TYPE'
|
||||||
|
and dict.name = '教职工'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
.setParam("unionId", unionId));
|
||||||
|
int memberTotal = memberInfoService.count(Sqls.create("select count(1) from vw_user where unionId = @unionId and member = 1")
|
||||||
|
.setParam("unionId", unionId));
|
||||||
|
int maleTotal = memberInfoService.count(Sqls.create("select count(1) from vw_user where unionId = @unionId and member = 1 and (sex = '男' or sex = '男性')")
|
||||||
|
.setParam("unionId", unionId));
|
||||||
|
int femaleTotal = memberInfoService.count(Sqls.create("select count(1) from vw_user where unionId = @unionId and member = 1 and (sex = '女' or sex = '女性')")
|
||||||
|
.setParam("unionId", unionId));
|
||||||
|
|
||||||
|
java.util.List<Sys_union_cadre> chairmen = memberInfoService.dao().query(Sys_union_cadre.class,
|
||||||
|
Cnd.where("unionId", "=", unionId)
|
||||||
|
.and("roleCode", "=", "BRANCH_UNION_CHAIRMAN")
|
||||||
|
.and(Cnd.exps("isServing", "=", true).or("isServing", "is", null)));
|
||||||
|
String chairman = chairmen.stream()
|
||||||
|
.map(item -> item.getUserName() + "(" + item.getLoginName() + ")")
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
String chairmanMobile = chairmen.stream()
|
||||||
|
.map(Sys_union_cadre::getMobile)
|
||||||
|
.filter(mobile -> mobile != null && !mobile.isBlank())
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
|
||||||
|
return Result.success(NutMap.NEW()
|
||||||
|
.addv("unionName", union.getName())
|
||||||
|
.addv("unionCode", union.getUnionCode())
|
||||||
|
.addv("chairman", chairman)
|
||||||
|
.addv("telephone", union.getTelephone())
|
||||||
|
.addv("chairmanMobile", chairmanMobile)
|
||||||
|
.addv("memberTotal", memberTotal)
|
||||||
|
.addv("staffTotal", staffTotal)
|
||||||
|
.addv("maleTotal", maleTotal)
|
||||||
|
.addv("femaleTotal", femaleTotal));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("member.info.group")
|
@SaCheckPermission("member.info.group")
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
-- 问卷服务菜单初始化
|
||||||
|
-- 使用 permission 做幂等判断,避免同一环境重复执行时插入重复菜单。
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT
|
||||||
|
'f6c0c37d2d7a4a22b5c7b1a1qsv0001',
|
||||||
|
'',
|
||||||
|
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||||
|
'日常办公',
|
||||||
|
'Day Office Work',
|
||||||
|
'menu',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'ti-briefcase',
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
'dayofficework',
|
||||||
|
NULL,
|
||||||
|
600,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
0,
|
||||||
|
'PC',
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
'r',
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
FROM sys_menu
|
||||||
|
WHERE (parentId = '' OR parentId IS NULL)
|
||||||
|
AND CHAR_LENGTH(path) = 4
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'dayofficework') t);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT
|
||||||
|
'f6c0c37d2d7a4a22b5c7b1a1qsv0002',
|
||||||
|
p.id,
|
||||||
|
CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')),
|
||||||
|
'问卷服务',
|
||||||
|
'Questionnaire Service',
|
||||||
|
'menu',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'fa fa-list-alt',
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
'qsv',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
0,
|
||||||
|
'PC',
|
||||||
|
p.moduleId,
|
||||||
|
NULL,
|
||||||
|
'w',
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
FROM sys_menu p
|
||||||
|
LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4
|
||||||
|
WHERE p.permission = 'dayofficework'
|
||||||
|
GROUP BY p.id, p.path, p.moduleId
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv') t);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0003', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '问卷管理', 'Questionnaire Manage', 'menu', '/platform/qsv/activity', 'data-pjax', '', 1, 0, 'qsv.activity', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'w', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
GROUP BY p.id, p.path, p.moduleId
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.activity') t);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0004', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '题库管理', 'Question Bank', 'menu', '/platform/qsv/bank', 'data-pjax', '', 1, 0, 'qsv.bank', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 't', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
GROUP BY p.id, p.path, p.moduleId
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.bank') t);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0005', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '调查统计', 'Survey Statistics', 'menu', '/platform/qsv/survey', 'data-pjax', '', 1, 0, 'qsv.survey', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'd', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
GROUP BY p.id, p.path, p.moduleId
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.survey') t);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0006', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '答题排行', 'Quiz Rank', 'menu', '/platform/qsv/quizRank', 'data-pjax', '', 1, 0, 'qsv.quiz.rank', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'd', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
GROUP BY p.id, p.path, p.moduleId
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.quiz.rank') t);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0007', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '在线答题', 'Online Quiz', 'menu', '/platform/qsv/online', 'data-pjax', '', 1, 0, 'qsv.online', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'z', 0, 0
|
||||||
|
FROM sys_menu p
|
||||||
|
LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4
|
||||||
|
WHERE p.permission = 'qsv'
|
||||||
|
GROUP BY p.id, p.path, p.moduleId
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.online') t);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
|
||||||
|
SELECT
|
||||||
|
'f6c0c37d2d7a4a22b5c7b1a1qsv0008',
|
||||||
|
'',
|
||||||
|
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
|
||||||
|
'问卷服务',
|
||||||
|
'Questionnaire Service',
|
||||||
|
'menu',
|
||||||
|
'/platform/h5/qsv',
|
||||||
|
'data-pjax',
|
||||||
|
'',
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
'h5.qsv',
|
||||||
|
NULL,
|
||||||
|
601,
|
||||||
|
0,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
'',
|
||||||
|
UNIX_TIMESTAMP(NOW()) * 1000,
|
||||||
|
0,
|
||||||
|
'H5',
|
||||||
|
NULL,
|
||||||
|
'/assets/mobile/svg/qsv/icon.svg',
|
||||||
|
'w',
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
FROM sys_menu
|
||||||
|
WHERE (parentId = '' OR parentId IS NULL)
|
||||||
|
AND CHAR_LENGTH(path) = 4
|
||||||
|
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'h5.qsv') t);
|
||||||
|
|
||||||
|
-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。
|
||||||
|
INSERT INTO sys_role_menu (roleId, menuId)
|
||||||
|
SELECT r.id, m.id
|
||||||
|
FROM sys_role r
|
||||||
|
JOIN sys_menu m ON m.permission IN (
|
||||||
|
'dayofficework',
|
||||||
|
'qsv',
|
||||||
|
'qsv.activity',
|
||||||
|
'qsv.bank',
|
||||||
|
'qsv.survey',
|
||||||
|
'qsv.quiz.rank',
|
||||||
|
'qsv.online',
|
||||||
|
'h5.qsv'
|
||||||
|
)
|
||||||
|
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
|
||||||
|
WHERE r.code = 'SYSADMIN'
|
||||||
|
AND rm.roleId IS NULL;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE qsv_subject
|
||||||
|
ADD COLUMN option_layout VARCHAR(20) DEFAULT 'VERTICAL' COMMENT '投票选项排列方式',
|
||||||
|
ADD COLUMN option_columns INT DEFAULT 1 COMMENT '投票选项横向列数';
|
||||||
@@ -365,6 +365,7 @@ layout("/layouts/v4/baseLayout.html"){
|
|||||||
|
|
||||||
// 地址选中
|
// 地址选中
|
||||||
hrefSelect() {
|
hrefSelect() {
|
||||||
|
const targetHref = this.getMenuOwnerPath()
|
||||||
function findMenuPath(menus, targetHref) {
|
function findMenuPath(menus, targetHref) {
|
||||||
function search(items, path) {
|
function search(items, path) {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
@@ -383,7 +384,7 @@ layout("/layouts/v4/baseLayout.html"){
|
|||||||
return search(menus, []);
|
return search(menus, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = findMenuPath(this.menus, window.location.pathname)
|
const result = findMenuPath(this.menus, targetHref)
|
||||||
if(result){
|
if(result){
|
||||||
this.activeMenuIndex = result[result.length-1]['id']
|
this.activeMenuIndex = result[result.length-1]['id']
|
||||||
this.openedMenus = result.map(v=>v.id)
|
this.openedMenus = result.map(v=>v.id)
|
||||||
@@ -396,6 +397,27 @@ layout("/layouts/v4/baseLayout.html"){
|
|||||||
|
|
||||||
|
|
||||||
// 默认选中
|
// 默认选中
|
||||||
|
getMenuOwnerPath() {
|
||||||
|
const pathname = window.location.pathname
|
||||||
|
if (pathname === "/platform/qsv/survey/reportPage") {
|
||||||
|
return "/platform/qsv/survey"
|
||||||
|
}
|
||||||
|
if (pathname === "/platform/qsv/new") {
|
||||||
|
return "/platform/qsv/activity"
|
||||||
|
}
|
||||||
|
return pathname
|
||||||
|
},
|
||||||
|
|
||||||
|
getMenuOwnerFullPath() {
|
||||||
|
if (window.location.pathname === "/platform/qsv/survey/reportPage") {
|
||||||
|
return "/platform/qsv/survey"
|
||||||
|
}
|
||||||
|
if (window.location.pathname === "/platform/qsv/new") {
|
||||||
|
return "/platform/qsv/activity"
|
||||||
|
}
|
||||||
|
return getFullSubAppPath()
|
||||||
|
},
|
||||||
|
|
||||||
defaultSelect() {
|
defaultSelect() {
|
||||||
// 查找第一个有href的子菜单
|
// 查找第一个有href的子菜单
|
||||||
function findFirstChildWithHref(menus) {
|
function findFirstChildWithHref(menus) {
|
||||||
@@ -445,7 +467,7 @@ layout("/layouts/v4/baseLayout.html"){
|
|||||||
// 页面加载时获取到正确的菜单
|
// 页面加载时获取到正确的菜单
|
||||||
const pathname = window.location.pathname
|
const pathname = window.location.pathname
|
||||||
if (!pathname.startsWith("/platform/v4/subApp")) {
|
if (!pathname.startsWith("/platform/v4/subApp")) {
|
||||||
const path = getFullSubAppPath()
|
const path = this.getMenuOwnerFullPath()
|
||||||
this.$axios.post("/platform/sys/user/rootMenuByPath", {pathname: path}).then((res) => {
|
this.$axios.post("/platform/sys/user/rootMenuByPath", {pathname: path}).then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.setAppInfo(res.data)
|
this.setAppInfo(res.data)
|
||||||
|
|||||||
@@ -58,6 +58,17 @@ const basicForm = {
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="封面" prop="cover">
|
||||||
|
<file-upload
|
||||||
|
:upload_number="1"
|
||||||
|
:value.sync="formData.cover"
|
||||||
|
accept=".jpg,.jpeg,.png"
|
||||||
|
upload_mode="image"
|
||||||
|
upload_result_category="interval"
|
||||||
|
upload_result_type="url"
|
||||||
|
></file-upload>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<template v-if="formData.category==='QUIZ'">
|
<template v-if="formData.category==='QUIZ'">
|
||||||
<el-form-item label="出题模式" prop="mode">
|
<el-form-item label="出题模式" prop="mode">
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
@@ -171,17 +182,6 @@ const basicForm = {
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- <el-form-item label="封面" prop="cover">-->
|
|
||||||
<!-- <file-upload-->
|
|
||||||
<!-- :upload_number="1"-->
|
|
||||||
<!-- :value.sync="formData.cover"-->
|
|
||||||
<!-- accept=".jpg,.jpeg,.png"-->
|
|
||||||
<!-- complete_result-->
|
|
||||||
<!-- upload_mode="image"-->
|
|
||||||
<!-- upload_result_category="interval"-->
|
|
||||||
<!-- ></file-upload>-->
|
|
||||||
<!-- </el-form-item>-->
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,7 +204,8 @@ const basicForm = {
|
|||||||
return {
|
return {
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
formData: {
|
formData: {
|
||||||
category: ''
|
category: '',
|
||||||
|
cover: ''
|
||||||
},
|
},
|
||||||
formRules: {
|
formRules: {
|
||||||
category: [{ required: true, message: "请选择类型", trigger: "change" }],
|
category: [{ required: true, message: "请选择类型", trigger: "change" }],
|
||||||
@@ -229,7 +230,7 @@ const basicForm = {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.formData = {
|
this.formData = {
|
||||||
category: 'QUIZ'
|
cover: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,382 @@
|
|||||||
layout("/layouts/platform.html"){
|
layout("/layouts/platform.html"){
|
||||||
#-->
|
#-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.qsv-logic-drawer .el-drawer__header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 18px 24px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-drawer {
|
||||||
|
top: 64px !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: auto !important;
|
||||||
|
width: calc(100% - 250px) !important;
|
||||||
|
height: calc(100vh - 64px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-drawer .el-drawer__body {
|
||||||
|
height: calc(100% - 58px);
|
||||||
|
background: #f0f2f5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-workspace {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(520px, 46%) minmax(0, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-section {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-section-header {
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-section-header .el-button--success {
|
||||||
|
min-width: 88px;
|
||||||
|
height: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-question-list,
|
||||||
|
.qsv-logic-preview-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-question-card {
|
||||||
|
position: relative;
|
||||||
|
min-height: 66px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 16px 20px 16px 54px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: grab;
|
||||||
|
transition: all .2s ease-in-out;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-question-card:hover,
|
||||||
|
.qsv-logic-question-card.active {
|
||||||
|
border-color: #409eff;
|
||||||
|
box-shadow: 0 6px 16px rgba(64, 158, 255, .12);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-question-card:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-card-main {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-index {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
color: #909399;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-type {
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
min-width: 70px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-type.single {
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-type.multiple {
|
||||||
|
background: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-type.text {
|
||||||
|
background: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-title {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-title.has-visible-rule {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(.9);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all .2s cubic-bezier(.175, .885, .32, 1.275);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-logic-question-card:hover .qsv-question-actions,
|
||||||
|
.qsv-logic-question-card.active .qsv-question-actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-question-actions .el-button-group .el-button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-question {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-question.active {
|
||||||
|
background: #f8fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-index {
|
||||||
|
min-width: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-title {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-options .el-radio,
|
||||||
|
.qsv-preview-options .el-checkbox {
|
||||||
|
margin: 0 32px 12px 0;
|
||||||
|
color: #3f4a5a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-preview-options .el-radio__label,
|
||||||
|
.qsv-preview-options .el-checkbox__label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-dialog .el-dialog__body {
|
||||||
|
padding: 18px 20px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-group {
|
||||||
|
background-color: #f9f9f5;
|
||||||
|
border: 1px solid #e8e8e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-group.is-root {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-root-title {
|
||||||
|
margin-bottom: -4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px dashed #dcdcdc;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-group-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-item-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all .2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-item-card:hover {
|
||||||
|
border-color: #dcdfe6;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, .03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-item-index {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
color: #909399;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-item-content {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-nested-group-wrapper {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-nested-group-label {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-empty-tip {
|
||||||
|
padding: 28px;
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-condition-group-footer {
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px dashed #dcdcdc;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-add-group-text {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.qsv-logic-workspace {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<guava ref="guava">
|
<guava ref="guava">
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
@@ -10,32 +386,54 @@ layout("/layouts/platform.html"){
|
|||||||
<el-date-picker placeholder="请选择年度" style="width: 100%" type="year" v-model="pageForm.year"
|
<el-date-picker placeholder="请选择年度" style="width: 100%" type="year" v-model="pageForm.year"
|
||||||
value-format="yyyy"></el-date-picker>
|
value-format="yyyy"></el-date-picker>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="标题">
|
<search-item label="标题名称">
|
||||||
<el-input placeholder="请输入标题" v-model="pageForm.title" clearable></el-input>
|
<el-input placeholder="请输入标题名称" v-model="pageForm.title" clearable></el-input>
|
||||||
</search-item>
|
</search-item>
|
||||||
</search>
|
</search>
|
||||||
|
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card shadow="never" class="mt10">
|
<el-card shadow="never" class="mt10">
|
||||||
<table-tool label="管理列表">
|
<table-tool label="标题列表">
|
||||||
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
|
|
||||||
</table-tool>
|
</table-tool>
|
||||||
|
|
||||||
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
|
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
|
||||||
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
|
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
|
||||||
<el-table-column label="标题" prop="title" sortable></el-table-column>
|
<el-table-column label="标题名称" prop="title" sortable></el-table-column>
|
||||||
<el-table-column label="类型" prop="category" sortable width="200">
|
<el-table-column label="类型" prop="category" sortable width="200">
|
||||||
<template scope="{row}">
|
<template scope="{row}">
|
||||||
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
|
<el-tag size="mini" :type="getCategoryTagType(row.category)">
|
||||||
|
{{getCategoryText(row.category)}}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
|
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
|
||||||
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
|
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
|
||||||
<el-table-column label="操作" fixed="right" width="450px">
|
<el-table-column label="状态" width="100">
|
||||||
|
<template scope="{row}">
|
||||||
|
<el-tag v-if="row.enabled !== false" type="success">开启</el-tag>
|
||||||
|
<el-tag v-else type="info">关闭</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" fixed="right" width="580px">
|
||||||
<template scope="{row}">
|
<template scope="{row}">
|
||||||
<el-button size="mini" type="primary" @click="openQrCode(row)">二维码</el-button>
|
<el-button size="mini" type="primary" @click="openQrCode(row)">二维码</el-button>
|
||||||
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
|
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.category === 'SURVEY'"
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
@click="openLogic(row.id)">逻辑</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.enabled === false"
|
||||||
|
size="mini"
|
||||||
|
type="success"
|
||||||
|
@click="updateEnabled(row, true)">开启</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
size="mini"
|
||||||
|
type="warning"
|
||||||
|
@click="updateEnabled(row, false)">关闭</el-button>
|
||||||
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
|
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -48,22 +446,22 @@ layout("/layouts/platform.html"){
|
|||||||
</template>
|
</template>
|
||||||
</guava>
|
</guava>
|
||||||
|
|
||||||
<basic-form ref="basicFormRef" @refresh="pageData"></basic-form>
|
<logic-form ref="logicFormRef" @refresh="pageData"></logic-form>
|
||||||
|
|
||||||
<qr-code-plus :url="signatureAddress" ref="qrCodePlusRef"></qr-code-plus>
|
<qr-code-plus :url="signatureAddress" ref="qrCodePlusRef"></qr-code-plus>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="${cspNonce!}">
|
<script nonce="${cspNonce!}">
|
||||||
<!--#include('basicForm.js'){}#-->
|
|
||||||
<!--#include('subjectForm.js'){}#-->
|
<!--#include('subjectForm.js'){}#-->
|
||||||
|
<!--#include('logicForm.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
dicts: ["ACTIVITY_QSV_CATEGORY"],
|
dicts: ["ACTIVITY_QSV_CATEGORY"],
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
components: {
|
components: {
|
||||||
"basic-form": basicForm,
|
"subject-form": subjectForm,
|
||||||
"subject-form": subjectForm
|
"logic-form": logicForm
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -71,9 +469,24 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getCategoryText(category) {
|
||||||
|
const options = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_CATEGORY || []) : []
|
||||||
|
const item = options.find(item => item.code === category)
|
||||||
|
return item ? item.name : category
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategoryTagType(category) {
|
||||||
|
const map = {
|
||||||
|
SURVEY: "success",
|
||||||
|
QUIZ: "primary",
|
||||||
|
VOTE: "danger"
|
||||||
|
}
|
||||||
|
return map[category] || "info"
|
||||||
|
},
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
openEdit(row) {
|
openEdit(row) {
|
||||||
this.$refs.basicFormRef.onOpen(row.id)
|
location.href = "/platform/qsv/new?id=" + row.id
|
||||||
},
|
},
|
||||||
|
|
||||||
// 打开二维码
|
// 打开二维码
|
||||||
@@ -94,9 +507,34 @@ layout("/layouts/platform.html"){
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 调查类型独立维护题目之间的显隐逻辑
|
||||||
|
openLogic(id) {
|
||||||
|
this.$refs.logicFormRef.onOpen(id)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 开启或关闭活动,关闭后移动端首页不展示,直达手机端问卷也不能答题
|
||||||
|
updateEnabled(row, enabled) {
|
||||||
|
const actionText = enabled ? "开启" : "关闭"
|
||||||
|
this.$confirm("您确认" + actionText + "该活动吗?", "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning"
|
||||||
|
}).then(() => {
|
||||||
|
$.post("/platform/qsv/activity/updateEnabled", {
|
||||||
|
id: row.id,
|
||||||
|
enabled
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success(res.msg)
|
||||||
|
this.pageData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
onDelete(id) {
|
onDelete(id) {
|
||||||
this.$confirm("您确认删除吗, 是否继续?", "提示", {
|
this.$confirm("删除后将同步删除该任务的题目、选项、用户答题记录、统计分析和成绩数据,且不可恢复。确定删除吗?", "提示", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
type: "warning"
|
type: "warning"
|
||||||
|
|||||||
@@ -0,0 +1,744 @@
|
|||||||
|
const conditionGroupEditor = {
|
||||||
|
name: "condition-group-editor",
|
||||||
|
props: {
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
questions: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isRoot: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: /*language=HTML*/ `
|
||||||
|
<div :class="['qsv-condition-group', {'is-root': isRoot}]">
|
||||||
|
<div v-if="isRoot" class="qsv-condition-root-title">条件规则</div>
|
||||||
|
<div class="qsv-condition-group-header">
|
||||||
|
<el-select v-model="group.logic" size="small" style="width: 120px;" @change="emitChange">
|
||||||
|
<el-option label="并且 (AND)" value="and"></el-option>
|
||||||
|
<el-option label="或者 (OR)" value="or"></el-option>
|
||||||
|
</el-select>
|
||||||
|
<div class="qsv-condition-group-actions">
|
||||||
|
<el-button type="primary" plain size="small" icon="el-icon-plus" @click="addItem">添加条件</el-button>
|
||||||
|
<el-button type="success" plain size="small" icon="el-icon-folder-add" @click="addGroup">添加分组</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="!isRoot"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
circle
|
||||||
|
@click="$emit('remove')">
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qsv-condition-children">
|
||||||
|
<div v-for="(child, index) in group.children" :key="index" class="qsv-condition-row">
|
||||||
|
<template v-if="child.type === 'item'">
|
||||||
|
<div class="qsv-condition-item-card">
|
||||||
|
<span class="qsv-condition-item-index">{{index + 1}}</span>
|
||||||
|
<div class="qsv-condition-item-content">
|
||||||
|
<el-select
|
||||||
|
v-model="child.field"
|
||||||
|
placeholder="选择题目"
|
||||||
|
size="small"
|
||||||
|
filterable
|
||||||
|
style="flex: 1; min-width: 180px;"
|
||||||
|
@change="onFieldChange(child)">
|
||||||
|
<el-option
|
||||||
|
v-for="question in questions"
|
||||||
|
:key="question.id"
|
||||||
|
:label="question.title || '未填写题目'"
|
||||||
|
:value="question.id">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-select
|
||||||
|
v-model="child.operator"
|
||||||
|
size="small"
|
||||||
|
style="width: 110px;"
|
||||||
|
@change="emitChange">
|
||||||
|
<el-option label="等于" :value="'=='"></el-option>
|
||||||
|
<el-option label="不等于" :value="'!='"></el-option>
|
||||||
|
<el-option label="包含" :value="'contains'"></el-option>
|
||||||
|
<el-option label="大于" :value="'>'"></el-option>
|
||||||
|
<el-option label="小于" :value="'<'"></el-option>
|
||||||
|
<el-option label="大于等于" :value="'>='"></el-option>
|
||||||
|
<el-option label="小于等于" :value="'<='"></el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-select
|
||||||
|
v-if="isChoiceQuestion(child.field)"
|
||||||
|
v-model="child.value"
|
||||||
|
placeholder="选择值"
|
||||||
|
size="small"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
style="width: 180px;"
|
||||||
|
@change="emitChange">
|
||||||
|
<el-option
|
||||||
|
v-for="option in getQuestionOptions(child.field)"
|
||||||
|
:key="option.id"
|
||||||
|
:label="option.text || '未填写选项'"
|
||||||
|
:value="option.id">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-input-number
|
||||||
|
v-else-if="isNumericOperator(child.operator)"
|
||||||
|
v-model="child.value"
|
||||||
|
size="small"
|
||||||
|
:controls="false"
|
||||||
|
style="width: 180px;"
|
||||||
|
@change="emitChange">
|
||||||
|
</el-input-number>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="child.value"
|
||||||
|
placeholder="输入值"
|
||||||
|
size="small"
|
||||||
|
style="width: 180px;"
|
||||||
|
@change="emitChange">
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
circle
|
||||||
|
@click="removeChild(index)">
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="child.type === 'group'">
|
||||||
|
<div class="qsv-nested-group-wrapper">
|
||||||
|
<div class="qsv-nested-group-label">
|
||||||
|
<i class="el-icon-folder"></i>
|
||||||
|
<span>嵌套分组</span>
|
||||||
|
</div>
|
||||||
|
<condition-group-editor
|
||||||
|
:group="child"
|
||||||
|
:questions="questions"
|
||||||
|
:is-root="false"
|
||||||
|
@change="emitChange"
|
||||||
|
@remove="removeChild(index)">
|
||||||
|
</condition-group-editor>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!group.children.length" class="qsv-condition-empty-tip">
|
||||||
|
暂无条件,请点击上方按钮添加
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qsv-condition-group-footer">
|
||||||
|
<el-button type="text" icon="el-icon-plus" @click="addItem">添加条件</el-button>
|
||||||
|
<el-button type="text" icon="el-icon-folder-add" class="qsv-add-group-text" @click="addGroup">添加分组</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
methods: {
|
||||||
|
addItem() {
|
||||||
|
this.group.children.push({
|
||||||
|
type: "item",
|
||||||
|
field: "",
|
||||||
|
operator: "==",
|
||||||
|
value: ""
|
||||||
|
})
|
||||||
|
this.emitChange()
|
||||||
|
},
|
||||||
|
addGroup() {
|
||||||
|
this.group.children.push({
|
||||||
|
type: "group",
|
||||||
|
logic: "and",
|
||||||
|
children: []
|
||||||
|
})
|
||||||
|
this.emitChange()
|
||||||
|
},
|
||||||
|
removeChild(index) {
|
||||||
|
this.group.children.splice(index, 1)
|
||||||
|
this.emitChange()
|
||||||
|
},
|
||||||
|
onFieldChange(condition) {
|
||||||
|
condition.value = ""
|
||||||
|
const question = this.getQuestion(condition.field)
|
||||||
|
if (question && question.type === "checkbox" && condition.operator === "==") {
|
||||||
|
condition.operator = "contains"
|
||||||
|
}
|
||||||
|
this.emitChange()
|
||||||
|
},
|
||||||
|
emitChange() {
|
||||||
|
this.$emit("change")
|
||||||
|
},
|
||||||
|
getQuestion(questionId) {
|
||||||
|
return this.questions.find((question) => {
|
||||||
|
return question.id === questionId
|
||||||
|
})
|
||||||
|
},
|
||||||
|
isChoiceQuestion(questionId) {
|
||||||
|
const question = this.getQuestion(questionId)
|
||||||
|
return question && ["radio", "checkbox"].includes(question.type)
|
||||||
|
},
|
||||||
|
getQuestionOptions(questionId) {
|
||||||
|
const question = this.getQuestion(questionId)
|
||||||
|
return question && question.options ? question.options : []
|
||||||
|
},
|
||||||
|
isNumericOperator(operator) {
|
||||||
|
return [">", "<", ">=", "<="].includes(operator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logicForm = {
|
||||||
|
components: {
|
||||||
|
"condition-group-editor": conditionGroupEditor
|
||||||
|
},
|
||||||
|
template: /*language=HTML*/ `
|
||||||
|
<el-drawer
|
||||||
|
:visible.sync="visible"
|
||||||
|
:title="activity.title ? '题目逻辑 - ' + activity.title : '题目逻辑'"
|
||||||
|
direction="rtl"
|
||||||
|
size="calc(100% - 250px)"
|
||||||
|
:modal="false"
|
||||||
|
append-to-body
|
||||||
|
custom-class="qsv-logic-drawer">
|
||||||
|
<div class="qsv-logic-workspace" v-loading="loading">
|
||||||
|
<section class="qsv-logic-section qsv-logic-question-section">
|
||||||
|
<div class="qsv-logic-section-header">
|
||||||
|
<span class="qsv-logic-section-title">题目列表</span>
|
||||||
|
<el-button type="success" size="small" :loading="saveLoading" @click="saveLogic">
|
||||||
|
保存问卷
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="qsv-logic-question-list">
|
||||||
|
<div
|
||||||
|
v-for="(subject, index) in subjects"
|
||||||
|
:key="subject.id"
|
||||||
|
:class="['qsv-logic-question-card', {active: activeSubjectId === subject.id}]"
|
||||||
|
@click="selectSubject(subject)">
|
||||||
|
<div class="qsv-question-card-main">
|
||||||
|
<span class="qsv-question-index">{{index + 1}}</span>
|
||||||
|
<span :class="['qsv-question-type', getSubjectTypeClass(subject)]">
|
||||||
|
{{getSubjectTypeText(subject)}}
|
||||||
|
</span>
|
||||||
|
<span :class="['qsv-question-title', {'has-visible-rule': subject.visibleRuleEnabled}]">
|
||||||
|
{{subject.title || '未填写题目'}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="qsv-question-actions" v-if="index > 0">
|
||||||
|
<el-button-group>
|
||||||
|
<el-tooltip content="设置显隐逻辑" placement="top">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-share"
|
||||||
|
@click.stop="openSubjectVisibleSetting(index, subject)">
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除逻辑" placement="top">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
:disabled="!subject.visibleRuleEnabled"
|
||||||
|
@click.stop="clearSubjectVisibleRule(index)">
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="subjects.length === 0" description="暂无题目"></el-empty>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="qsv-logic-section qsv-logic-preview-section">
|
||||||
|
<div class="qsv-logic-section-header">
|
||||||
|
<span class="qsv-logic-section-title">实时预览</span>
|
||||||
|
<div class="qsv-preview-controls">
|
||||||
|
<el-checkbox v-model="showPreviewIndex" size="small">显示序号</el-checkbox>
|
||||||
|
<el-tag type="info" size="small">配置即所得</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="qsv-logic-preview-list">
|
||||||
|
<div
|
||||||
|
v-for="(subject, index) in visiblePreviewSubjects"
|
||||||
|
:key="subject.id"
|
||||||
|
:data-preview-subject="subject.id"
|
||||||
|
:class="['qsv-preview-question', {active: activeSubjectId === subject.id}]">
|
||||||
|
<span v-if="showPreviewIndex" class="qsv-preview-index">{{index + 1}}.</span>
|
||||||
|
<div class="qsv-preview-content">
|
||||||
|
<div class="qsv-preview-title">{{subject.title || '未填写题目'}}</div>
|
||||||
|
|
||||||
|
<template v-if="subject.type === 'radio'">
|
||||||
|
<el-radio-group
|
||||||
|
class="qsv-preview-options"
|
||||||
|
:value="getPreviewRadioValue(subject)"
|
||||||
|
@input="setPreviewRadioValue(subject, $event)">
|
||||||
|
<el-radio
|
||||||
|
v-for="option in subject.options"
|
||||||
|
:key="option.id"
|
||||||
|
:label="option.id">
|
||||||
|
{{option.text || '未填写选项'}}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="subject.type === 'checkbox'">
|
||||||
|
<el-checkbox-group
|
||||||
|
class="qsv-preview-options"
|
||||||
|
v-model="previewAnswers[subject.id]"
|
||||||
|
@change="refreshPreview">
|
||||||
|
<el-checkbox
|
||||||
|
v-for="option in subject.options"
|
||||||
|
:key="option.id"
|
||||||
|
:label="option.id">
|
||||||
|
{{option.text || '未填写选项'}}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="previewTextAnswers[subject.id]"
|
||||||
|
placeholder="请输入"
|
||||||
|
size="small"
|
||||||
|
@input="refreshPreview">
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="visiblePreviewSubjects.length === 0" description="暂无可见题目"></el-empty>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
:title="subjectVisibleSetting.subjectTitle ? '配置题目逻辑 - ' + subjectVisibleSetting.subjectTitle : '配置题目逻辑'"
|
||||||
|
:visible.sync="subjectVisibleSetting.visible"
|
||||||
|
append-to-body
|
||||||
|
custom-class="qsv-condition-dialog"
|
||||||
|
width="800px">
|
||||||
|
<el-form :model="subjectVisibleSetting.formData" label-width="88px">
|
||||||
|
<el-form-item label="启用逻辑">
|
||||||
|
<el-switch
|
||||||
|
v-model="subjectVisibleSetting.formData.enabled"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="关闭">
|
||||||
|
</el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<template v-if="subjectVisibleSetting.formData.enabled">
|
||||||
|
<el-form-item label="处理方式">
|
||||||
|
<el-radio-group v-model="subjectVisibleSetting.formData.action">
|
||||||
|
<el-radio label="show">条件满足后显示本题</el-radio>
|
||||||
|
<el-radio label="hide">条件满足后隐藏本题</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<condition-group-editor
|
||||||
|
:group="subjectVisibleSetting.formData.rootGroup"
|
||||||
|
:questions="subjectVisibleRuleSubjects"
|
||||||
|
:is-root="true"
|
||||||
|
@change="refreshConditionDialog">
|
||||||
|
</condition-group-editor>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="subjectVisibleSetting.visible = false">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="onSubjectVisibleSettingConfirm">保存逻辑</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</el-drawer>
|
||||||
|
`,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
saveLoading: false,
|
||||||
|
id: null,
|
||||||
|
activity: {},
|
||||||
|
subjects: [],
|
||||||
|
activeSubjectId: null,
|
||||||
|
previewAnswers: {},
|
||||||
|
previewTextAnswers: {},
|
||||||
|
showPreviewIndex: true,
|
||||||
|
subjectVisibleSetting: {
|
||||||
|
visible: false,
|
||||||
|
subjectIndex: null,
|
||||||
|
subjectTitle: "",
|
||||||
|
formData: {
|
||||||
|
enabled: false,
|
||||||
|
action: "hide",
|
||||||
|
rootGroup: {
|
||||||
|
type: "group",
|
||||||
|
logic: "and",
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visiblePreviewSubjects() {
|
||||||
|
return this.subjects.filter((subject) => {
|
||||||
|
return this.isSubjectVisible(subject)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
subjectVisibleRuleSubjects() {
|
||||||
|
if (this.subjectVisibleSetting.subjectIndex === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return this.subjects.filter((item, index) => {
|
||||||
|
return index < this.subjectVisibleSetting.subjectIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onOpen(id) {
|
||||||
|
this.visible = true
|
||||||
|
this.loading = true
|
||||||
|
this.id = id
|
||||||
|
this.activity = {}
|
||||||
|
this.subjects = []
|
||||||
|
this.activeSubjectId = null
|
||||||
|
$.post("/platform/qsv/activity/findOne", {id}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.activity = res.data || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$.post("/platform/qsv/activity/listSubjects", {activityId: id}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.subjects = (res.data || []).map((subject) => {
|
||||||
|
return this.normalizeSubject(subject)
|
||||||
|
})
|
||||||
|
this.initPreviewAnswers()
|
||||||
|
if (this.subjects.length > 0) {
|
||||||
|
this.activeSubjectId = this.subjects[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).always(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
normalizeSubject(subject) {
|
||||||
|
return {
|
||||||
|
...subject,
|
||||||
|
visibleRuleEnabled: !!subject.visibleRuleEnabled,
|
||||||
|
visibleRuleAction: subject.visibleRuleEnabled ? (subject.visibleRuleAction || "hide") : "",
|
||||||
|
visibleRuleLogic: subject.visibleRuleLogic || "AND",
|
||||||
|
visibleRuleConditions: this.normalizeVisibleRuleConditions(subject.visibleRuleConditions, subject.visibleRuleLogic)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeVisibleRuleConditions(conditions, legacyLogic) {
|
||||||
|
let parsedConditions = conditions || []
|
||||||
|
if (typeof parsedConditions === "string") {
|
||||||
|
try {
|
||||||
|
parsedConditions = JSON.parse(parsedConditions)
|
||||||
|
} catch (e) {
|
||||||
|
parsedConditions = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Array.isArray(parsedConditions) || parsedConditions.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (parsedConditions.length === 1 && parsedConditions[0].type === "group") {
|
||||||
|
return [this.normalizeConditionGroup(parsedConditions[0])]
|
||||||
|
}
|
||||||
|
return [this.normalizeLegacyConditionGroup(parsedConditions, legacyLogic)]
|
||||||
|
},
|
||||||
|
normalizeConditionGroup(group) {
|
||||||
|
return {
|
||||||
|
type: "group",
|
||||||
|
logic: (group.logic || "and").toLowerCase() === "or" ? "or" : "and",
|
||||||
|
children: Array.isArray(group.children) ? group.children.map((child) => {
|
||||||
|
if (child.type === "group") {
|
||||||
|
return this.normalizeConditionGroup(child)
|
||||||
|
}
|
||||||
|
return this.normalizeConditionItem(child)
|
||||||
|
}) : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeConditionItem(item) {
|
||||||
|
return {
|
||||||
|
type: "item",
|
||||||
|
field: item.field || item.subjectId || "",
|
||||||
|
operator: item.operator || (item.optionId ? "contains" : "=="),
|
||||||
|
value: item.value !== undefined && item.value !== null ? item.value : (item.optionId || "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeLegacyConditionGroup(conditions, legacyLogic) {
|
||||||
|
return {
|
||||||
|
type: "group",
|
||||||
|
logic: legacyLogic === "OR" ? "or" : "and",
|
||||||
|
children: conditions.map((condition) => {
|
||||||
|
return {
|
||||||
|
type: "item",
|
||||||
|
field: condition.subjectId || condition.field || "",
|
||||||
|
operator: condition.operator || "contains",
|
||||||
|
value: condition.optionId || condition.value || ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createDefaultConditionGroup() {
|
||||||
|
return {
|
||||||
|
type: "group",
|
||||||
|
logic: "and",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "item",
|
||||||
|
field: "",
|
||||||
|
operator: "==",
|
||||||
|
value: ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initPreviewAnswers() {
|
||||||
|
const previewAnswers = {}
|
||||||
|
const previewTextAnswers = {}
|
||||||
|
this.subjects.forEach((subject) => {
|
||||||
|
if (["radio", "checkbox"].includes(subject.type)) {
|
||||||
|
previewAnswers[subject.id] = []
|
||||||
|
} else {
|
||||||
|
previewTextAnswers[subject.id] = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.previewAnswers = previewAnswers
|
||||||
|
this.previewTextAnswers = previewTextAnswers
|
||||||
|
},
|
||||||
|
selectSubject(subject) {
|
||||||
|
this.activeSubjectId = subject.id
|
||||||
|
this.scrollPreviewToSubject(subject.id)
|
||||||
|
},
|
||||||
|
scrollPreviewToSubject(subjectId) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const node = this.$el.querySelector('[data-preview-subject="' + subjectId + '"]')
|
||||||
|
if (node && node.scrollIntoView) {
|
||||||
|
node.scrollIntoView({behavior: "smooth", block: "center"})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getSubjectTypeText(subject) {
|
||||||
|
const typeMap = {
|
||||||
|
radio: "单选题",
|
||||||
|
checkbox: "多选题",
|
||||||
|
text: "填空题"
|
||||||
|
}
|
||||||
|
return typeMap[subject.type] || "题目"
|
||||||
|
},
|
||||||
|
getSubjectTypeClass(subject) {
|
||||||
|
if (subject.type === "checkbox") {
|
||||||
|
return "multiple"
|
||||||
|
}
|
||||||
|
if (subject.type === "text") {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
return "single"
|
||||||
|
},
|
||||||
|
getPreviewRadioValue(subject) {
|
||||||
|
const value = this.previewAnswers[subject.id]
|
||||||
|
return value && value.length > 0 ? value[0] : ""
|
||||||
|
},
|
||||||
|
setPreviewRadioValue(subject, optionId) {
|
||||||
|
this.$set(this.previewAnswers, subject.id, optionId ? [optionId] : [])
|
||||||
|
this.refreshPreview()
|
||||||
|
},
|
||||||
|
refreshPreview() {
|
||||||
|
this.clearHiddenPreviewAnswers()
|
||||||
|
},
|
||||||
|
refreshConditionDialog() {
|
||||||
|
this.$forceUpdate()
|
||||||
|
},
|
||||||
|
openSubjectVisibleSetting(subjectIndex, subject) {
|
||||||
|
const rootGroup = subject.visibleRuleConditions && subject.visibleRuleConditions.length > 0
|
||||||
|
? this.cloneConditionGroup(subject.visibleRuleConditions[0])
|
||||||
|
: this.createDefaultConditionGroup()
|
||||||
|
this.activeSubjectId = subject.id
|
||||||
|
this.subjectVisibleSetting.subjectIndex = subjectIndex
|
||||||
|
this.subjectVisibleSetting.subjectTitle = subject.title || "未填写题目"
|
||||||
|
this.subjectVisibleSetting.formData = {
|
||||||
|
enabled: !!subject.visibleRuleEnabled,
|
||||||
|
action: subject.visibleRuleEnabled ? (subject.visibleRuleAction || "hide") : "hide",
|
||||||
|
rootGroup
|
||||||
|
}
|
||||||
|
this.subjectVisibleSetting.visible = true
|
||||||
|
},
|
||||||
|
cloneConditionGroup(group) {
|
||||||
|
return JSON.parse(JSON.stringify(group))
|
||||||
|
},
|
||||||
|
clearSubjectVisibleRule(subjectIndex) {
|
||||||
|
const subject = this.subjects[subjectIndex]
|
||||||
|
if (!subject || !subject.visibleRuleEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(subject, "visibleRuleEnabled", false)
|
||||||
|
this.$set(subject, "visibleRuleAction", "show")
|
||||||
|
this.$set(subject, "visibleRuleLogic", "AND")
|
||||||
|
this.$set(subject, "visibleRuleConditions", [])
|
||||||
|
this.activeSubjectId = subject.id
|
||||||
|
this.refreshPreview()
|
||||||
|
this.scrollPreviewToSubject(subject.id)
|
||||||
|
},
|
||||||
|
onSubjectVisibleSettingConfirm() {
|
||||||
|
const subjectIndex = this.subjectVisibleSetting.subjectIndex
|
||||||
|
if (subjectIndex === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const formData = this.subjectVisibleSetting.formData
|
||||||
|
if (formData.enabled && !this.validateConditionGroup(formData.rootGroup)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const normalizedGroup = this.normalizeConditionGroup(formData.rootGroup)
|
||||||
|
this.$set(this.subjects[subjectIndex], "visibleRuleEnabled", formData.enabled)
|
||||||
|
this.$set(this.subjects[subjectIndex], "visibleRuleAction", formData.enabled ? formData.action : "show")
|
||||||
|
this.$set(this.subjects[subjectIndex], "visibleRuleLogic", normalizedGroup.logic === "or" ? "OR" : "AND")
|
||||||
|
this.$set(this.subjects[subjectIndex], "visibleRuleConditions", formData.enabled ? [normalizedGroup] : [])
|
||||||
|
this.subjectVisibleSetting.visible = false
|
||||||
|
this.refreshPreview()
|
||||||
|
this.scrollPreviewToSubject(this.subjects[subjectIndex].id)
|
||||||
|
},
|
||||||
|
validateConditionGroup(group) {
|
||||||
|
if (!group.children || group.children.length === 0) {
|
||||||
|
this.$message.warning("请至少添加一条条件")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < group.children.length; i++) {
|
||||||
|
const child = group.children[i]
|
||||||
|
if (child.type === "group") {
|
||||||
|
if (!this.validateConditionGroup(child)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (!child.field || !child.operator || child.value === "" || child.value === null || child.value === undefined) {
|
||||||
|
this.$message.warning("请完整配置条件" + (i + 1))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
isSubjectVisible(subject) {
|
||||||
|
if (!subject.visibleRuleEnabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const rootGroup = this.getVisibleRuleRootGroup(subject)
|
||||||
|
if (!rootGroup) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const matched = this.evaluateConditionGroup(rootGroup)
|
||||||
|
return subject.visibleRuleAction === "hide" ? !matched : matched
|
||||||
|
},
|
||||||
|
getVisibleRuleRootGroup(subject) {
|
||||||
|
const conditions = this.normalizeVisibleRuleConditions(subject.visibleRuleConditions, subject.visibleRuleLogic)
|
||||||
|
return conditions.length > 0 ? conditions[0] : null
|
||||||
|
},
|
||||||
|
evaluateConditionGroup(group) {
|
||||||
|
const children = (group.children || []).filter((child) => {
|
||||||
|
return child.type === "group" || (child.field && child.operator)
|
||||||
|
})
|
||||||
|
if (children.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const results = children.map((child) => {
|
||||||
|
if (child.type === "group") {
|
||||||
|
return this.evaluateConditionGroup(child)
|
||||||
|
}
|
||||||
|
return this.evaluateConditionItem(child)
|
||||||
|
})
|
||||||
|
return group.logic === "or"
|
||||||
|
? results.some((item) => {
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
: results.every((item) => {
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
},
|
||||||
|
evaluateConditionItem(condition) {
|
||||||
|
const subject = this.subjects.find((item) => {
|
||||||
|
return item.id === condition.field
|
||||||
|
})
|
||||||
|
if (!subject) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const answer = ["radio", "checkbox"].includes(subject.type)
|
||||||
|
? (this.previewAnswers[subject.id] || [])
|
||||||
|
: this.previewTextAnswers[subject.id]
|
||||||
|
return this.compareRuleValue(answer, condition.operator, condition.value)
|
||||||
|
},
|
||||||
|
compareRuleValue(answer, operator, value) {
|
||||||
|
if (Array.isArray(answer)) {
|
||||||
|
if (operator === "!=") {
|
||||||
|
return !answer.includes(value)
|
||||||
|
}
|
||||||
|
return answer.includes(value)
|
||||||
|
}
|
||||||
|
if ([">", "<", ">=", "<="].includes(operator)) {
|
||||||
|
const answerNumber = Number(answer)
|
||||||
|
const valueNumber = Number(value)
|
||||||
|
if (isNaN(answerNumber) || isNaN(valueNumber)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (operator === ">") {
|
||||||
|
return answerNumber > valueNumber
|
||||||
|
}
|
||||||
|
if (operator === "<") {
|
||||||
|
return answerNumber < valueNumber
|
||||||
|
}
|
||||||
|
if (operator === ">=") {
|
||||||
|
return answerNumber >= valueNumber
|
||||||
|
}
|
||||||
|
return answerNumber <= valueNumber
|
||||||
|
}
|
||||||
|
const answerText = answer === undefined || answer === null ? "" : String(answer)
|
||||||
|
const valueText = value === undefined || value === null ? "" : String(value)
|
||||||
|
if (operator === "!=") {
|
||||||
|
return answerText !== valueText
|
||||||
|
}
|
||||||
|
if (operator === "contains") {
|
||||||
|
return answerText.indexOf(valueText) !== -1
|
||||||
|
}
|
||||||
|
return answerText === valueText
|
||||||
|
},
|
||||||
|
clearHiddenPreviewAnswers() {
|
||||||
|
this.subjects.forEach((subject) => {
|
||||||
|
if (this.isSubjectVisible(subject)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (["radio", "checkbox"].includes(subject.type)) {
|
||||||
|
this.$set(this.previewAnswers, subject.id, [])
|
||||||
|
} else {
|
||||||
|
this.$set(this.previewTextAnswers, subject.id, "")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
buildSubmitSubjects() {
|
||||||
|
return this.subjects.map((subject) => {
|
||||||
|
const conditions = this.normalizeVisibleRuleConditions(subject.visibleRuleConditions, subject.visibleRuleLogic)
|
||||||
|
return {
|
||||||
|
...subject,
|
||||||
|
visibleRuleEnabled: !!subject.visibleRuleEnabled,
|
||||||
|
visibleRuleAction: subject.visibleRuleEnabled ? subject.visibleRuleAction : "show",
|
||||||
|
visibleRuleLogic: subject.visibleRuleEnabled && conditions[0] && conditions[0].logic === "or" ? "OR" : "AND",
|
||||||
|
visibleRuleConditions: subject.visibleRuleEnabled ? conditions : []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
saveLogic() {
|
||||||
|
this.saveLoading = true
|
||||||
|
$.post("/platform/qsv/activity/saveSubjects", {
|
||||||
|
activityId: this.id,
|
||||||
|
subjects: JSON.stringify(this.buildSubmitSubjects())
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success(res.msg || "保存成功")
|
||||||
|
this.$emit("refresh")
|
||||||
|
}
|
||||||
|
}).always(() => {
|
||||||
|
this.saveLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
<!--#
|
||||||
|
layout("/layouts/platform.html"){
|
||||||
|
#-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.qsv-new-page {
|
||||||
|
padding-bottom: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-new-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-new-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-new-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-new-sub-title {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-new-footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 250px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 900;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-cover-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-cover-tip span {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.qsv-new-footer {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-scope-select {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="app" v-cloak>
|
||||||
|
<div class="qsv-new-page">
|
||||||
|
<div class="qsv-new-header">
|
||||||
|
<h2 class="qsv-new-title">{{pageTitle}}</h2>
|
||||||
|
<el-button icon="el-icon-back" @click="goBack">返回</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="qsv-new-card">
|
||||||
|
<h3 class="qsv-new-sub-title">基础信息</h3>
|
||||||
|
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="150px">
|
||||||
|
<el-form-item label="类型" prop="category">
|
||||||
|
<el-radio-group v-model="formData.category" size="small" @change="onCategoryChange">
|
||||||
|
<el-radio v-for="item in categoryOptions" :label="item.code"
|
||||||
|
:key="item.code"
|
||||||
|
border>
|
||||||
|
{{item.label}}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="标题" prop="title">
|
||||||
|
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
|
||||||
|
show-word-limit></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="说明" prop="description">
|
||||||
|
<text-editor v-model="formData.description" :height="150"></text-editor>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="开始时间" prop="startTime">
|
||||||
|
<el-date-picker v-model="formData.startTime" type="datetime" placeholder="选择日期时间"
|
||||||
|
style="width: 100%"
|
||||||
|
value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="结束时间" prop="endTime">
|
||||||
|
<el-date-picker v-model="formData.endTime" type="datetime" placeholder="选择日期时间"
|
||||||
|
style="width: 100%"
|
||||||
|
value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="参加人员范围" prop="groupId">
|
||||||
|
<div style="display: flex;column-gap: 10px">
|
||||||
|
<el-select class="qsv-scope-select"
|
||||||
|
clearable filterable
|
||||||
|
placeholder="参加人员范围"
|
||||||
|
style="width: 640px; max-width: calc(100% - 82px);"
|
||||||
|
v-model="formData.groupId">
|
||||||
|
<el-option :key="item.groupId"
|
||||||
|
:label="item.groupName"
|
||||||
|
:value="item.groupId"
|
||||||
|
v-for="item in activityGroupOptions"></el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-button
|
||||||
|
@click="$refs.drawerUserScope.userScopeDialog = true"
|
||||||
|
type="primary">设置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<h3 class="qsv-new-sub-title" v-if="formData.category==='QUIZ'">题目信息</h3>
|
||||||
|
|
||||||
|
<template v-if="formData.category==='QUIZ'">
|
||||||
|
<el-form-item label="答题模式" prop="mode">
|
||||||
|
<el-radio-group v-model="formData.mode" size="small" @change="onModeChange">
|
||||||
|
<el-radio v-for="item in modeOptions" :label="item.code"
|
||||||
|
:key="item.code"
|
||||||
|
border>
|
||||||
|
{{item.label}}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="常规题目范围" prop="displayMode"
|
||||||
|
v-if="isRegularMode()">
|
||||||
|
<el-radio-group v-model="formData.displayMode" size="small" @change="normalizeQuizConfig">
|
||||||
|
<el-radio label="ALL" border>显示全部题目</el-radio>
|
||||||
|
<el-radio label="RANDOM" border>随机抽取题目</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="题目展示顺序打乱" prop="shuffleSubject">
|
||||||
|
<el-switch v-model="formData.shuffleSubject" inactive-text="否" active-text="是"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="每次随机题数" prop="randomCount"
|
||||||
|
v-if="isRegularMode() && isRandomDisplayMode()">
|
||||||
|
<el-input-number v-model="formData.randomCount" :min="1" :max="100"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="最多随机答题次数" prop="totalRandom"
|
||||||
|
v-if="isRegularMode() && isRandomDisplayMode()">
|
||||||
|
<el-input-number v-model="formData.totalRandom" :min="1" :max="100"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="允许重复答题" prop="repeatable"
|
||||||
|
v-if="canRepeatAnswer()">
|
||||||
|
<el-switch v-model="formData.repeatable" inactive-text="否" active-text="是"
|
||||||
|
@change="normalizeQuizConfig"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="最多答题次数" prop="maxAttempts"
|
||||||
|
v-if="canRepeatAnswer() && formData.repeatable">
|
||||||
|
<el-input-number v-model="formData.maxAttempts" :min="1" :max="100"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="时间限制(min)" prop="timeLimit">
|
||||||
|
<el-input-number v-model="formData.timeLimit" :min="0" :max="100"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="0表示不限时"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="重复答题计分方式" prop="scoreMode"
|
||||||
|
v-if="canRepeatAnswer() && formData.repeatable">
|
||||||
|
<el-radio-group v-model="formData.scoreMode" size="small">
|
||||||
|
<el-radio v-for="item in dict.type.ACTIVITY_QSV_SCORE_MODE" :label="item.code"
|
||||||
|
:key="item.code" border>
|
||||||
|
{{item.label}}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item label="封面" prop="cover">
|
||||||
|
<file-upload
|
||||||
|
:upload_number="1"
|
||||||
|
:upload_size="1024 * 1024"
|
||||||
|
:value.sync="formData.cover"
|
||||||
|
accept=".jpg,.jpeg,.png"
|
||||||
|
upload_mode="image"
|
||||||
|
upload_result_category="interval"
|
||||||
|
upload_result_type="url"
|
||||||
|
></file-upload>
|
||||||
|
<div class="qsv-cover-tip">
|
||||||
|
只能上传<span>1</span>个文件;只能上传<span>.jpg,.jpeg,.png</span>文件;单个文件大小不能超过<span>1.00</span>M;
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div class="qsv-new-footer">
|
||||||
|
<el-button @click="goBack">取消</el-button>
|
||||||
|
<el-button :loading="saveLoading" @click="saveDraft">保存</el-button>
|
||||||
|
<el-button type="primary" :loading="saveLoading" @click="submitActivity">提交</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<drawer-user-scope
|
||||||
|
@group_change="getActivityGroup"
|
||||||
|
ref="drawerUserScope"
|
||||||
|
:group_id.sync="formData.groupId"
|
||||||
|
></drawer-user-scope>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script nonce="${cspNonce!}">
|
||||||
|
new Vue({
|
||||||
|
el: "#app",
|
||||||
|
dicts: ["ACTIVITY_QSV_CATEGORY", "ACTIVITY_QSV_MODE", "ACTIVITY_QSV_REPEAT_MODE", "ACTIVITY_QSV_SCORE_MODE"],
|
||||||
|
components: {
|
||||||
|
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
saveLoading: false,
|
||||||
|
activityId: "",
|
||||||
|
formData: {
|
||||||
|
category: "QUIZ",
|
||||||
|
cover: "",
|
||||||
|
enabled: false,
|
||||||
|
mode: "REGULAR",
|
||||||
|
shuffleSubject: false,
|
||||||
|
repeatable: false,
|
||||||
|
displayMode: "ALL",
|
||||||
|
timeLimit: 0
|
||||||
|
},
|
||||||
|
formRules: {
|
||||||
|
category: [{ required: true, message: "请选择类型", trigger: "change" }],
|
||||||
|
title: [{ required: true, message: "请输入标题", trigger: "change" }],
|
||||||
|
startTime: [{ required: true, message: "请选择开始时间", trigger: "change" }],
|
||||||
|
endTime: [{ required: true, message: "请选择结束时间", trigger: "change" }],
|
||||||
|
mode: [{ required: true, message: "请选择答题模式", trigger: "change" }],
|
||||||
|
scoreMode: [{ required: true, message: "请选择重复答题计分方式", trigger: "change" }]
|
||||||
|
},
|
||||||
|
activityGroupOptions: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getQueryString(name) {
|
||||||
|
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)")
|
||||||
|
const result = window.location.search.substr(1).match(reg)
|
||||||
|
return result ? decodeURIComponent(result[2]) : ""
|
||||||
|
},
|
||||||
|
onCategoryChange(category) {
|
||||||
|
this.formData.category = this.normalizeCategoryValue(category)
|
||||||
|
if (this.formData.category === "QUIZ") {
|
||||||
|
this.normalizeQuizConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onModeChange(mode) {
|
||||||
|
this.$set(this.formData, "mode", this.normalizeModeValue(mode))
|
||||||
|
this.normalizeQuizConfig()
|
||||||
|
},
|
||||||
|
getDefaultScoreMode() {
|
||||||
|
const scoreModes = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_SCORE_MODE || []) : []
|
||||||
|
const highMode = scoreModes.find(item => item.code === "HIGH")
|
||||||
|
const lastMode = scoreModes.find(item => item.code === "LAST")
|
||||||
|
return (lastMode || highMode || scoreModes[0] || { code: "LAST" }).code
|
||||||
|
},
|
||||||
|
normalizeCategoryValue(category) {
|
||||||
|
const value = (category || "").toString().trim().toUpperCase()
|
||||||
|
if (value === "QUIZ" || value === "VOTE") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return "SURVEY"
|
||||||
|
},
|
||||||
|
normalizeModeValue(mode) {
|
||||||
|
const value = (mode || "").toString().trim().toUpperCase()
|
||||||
|
if (value.indexOf("SCHEDULE") > -1 || value.indexOf("定时") > -1) {
|
||||||
|
return "SCHEDULED"
|
||||||
|
}
|
||||||
|
return "REGULAR"
|
||||||
|
},
|
||||||
|
normalizeModeOptionValue(item) {
|
||||||
|
const code = item && item.code ? item.code.toString().trim() : ""
|
||||||
|
if (code.toUpperCase().indexOf("SCHEDULE") > -1 || code.indexOf("定时") > -1) {
|
||||||
|
return "SCHEDULED"
|
||||||
|
}
|
||||||
|
if (code.toUpperCase().indexOf("REGULAR") > -1 || code.indexOf("常规") > -1) {
|
||||||
|
return "REGULAR"
|
||||||
|
}
|
||||||
|
return this.normalizeModeValue([item && item.name, item && item.label, item && item.text].join(" "))
|
||||||
|
},
|
||||||
|
getDefaultModeValue() {
|
||||||
|
const regularOption = this.modeOptions.find(item => this.normalizeModeOptionValue(item) === "REGULAR")
|
||||||
|
return regularOption ? regularOption.code : "REGULAR"
|
||||||
|
},
|
||||||
|
normalizeDisplayModeValue(displayMode) {
|
||||||
|
const value = (displayMode || "").toString().trim().toUpperCase()
|
||||||
|
if (value === "RANDOM") {
|
||||||
|
return "RANDOM"
|
||||||
|
}
|
||||||
|
return "ALL"
|
||||||
|
},
|
||||||
|
isRegularMode(data) {
|
||||||
|
const formData = data || this.formData
|
||||||
|
return this.normalizeModeValue(formData.mode) === "REGULAR"
|
||||||
|
},
|
||||||
|
isRandomDisplayMode(data) {
|
||||||
|
const formData = data || this.formData
|
||||||
|
return this.normalizeDisplayModeValue(formData.displayMode) === "RANDOM"
|
||||||
|
},
|
||||||
|
canRepeatAnswer(data) {
|
||||||
|
const formData = data || this.formData
|
||||||
|
const category = this.normalizeCategoryValue(formData.category)
|
||||||
|
const mode = this.normalizeModeValue(formData.mode)
|
||||||
|
const displayMode = this.normalizeDisplayModeValue(formData.displayMode)
|
||||||
|
return category === "QUIZ"
|
||||||
|
&& (mode === "SCHEDULED" || (mode === "REGULAR" && displayMode === "ALL"))
|
||||||
|
},
|
||||||
|
normalizeQuizConfig() {
|
||||||
|
this.formData.category = this.normalizeCategoryValue(this.formData.category)
|
||||||
|
if (this.formData.category !== "QUIZ") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(this.formData, "mode", this.formData.mode ? this.normalizeModeValue(this.formData.mode) : this.getDefaultModeValue())
|
||||||
|
this.$set(this.formData, "displayMode", this.normalizeDisplayModeValue(this.formData.displayMode))
|
||||||
|
this.$set(this.formData, "timeLimit", this.formData.timeLimit || 0)
|
||||||
|
if (!this.isRegularMode()) {
|
||||||
|
this.$set(this.formData, "displayMode", "ALL")
|
||||||
|
this.$set(this.formData, "randomCount", null)
|
||||||
|
this.$set(this.formData, "totalRandom", null)
|
||||||
|
} else if (!this.isRandomDisplayMode()) {
|
||||||
|
this.$set(this.formData, "randomCount", null)
|
||||||
|
this.$set(this.formData, "totalRandom", null)
|
||||||
|
} else {
|
||||||
|
this.$set(this.formData, "repeatable", false)
|
||||||
|
this.$set(this.formData, "randomCount", this.formData.randomCount || 1)
|
||||||
|
this.$set(this.formData, "totalRandom", this.formData.totalRandom || 1)
|
||||||
|
}
|
||||||
|
if (!this.canRepeatAnswer()) {
|
||||||
|
this.$set(this.formData, "repeatable", false)
|
||||||
|
this.$set(this.formData, "maxAttempts", 1)
|
||||||
|
this.$set(this.formData, "scoreMode", this.getDefaultScoreMode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.formData.repeatable) {
|
||||||
|
this.$set(this.formData, "maxAttempts", this.formData.maxAttempts || 1)
|
||||||
|
this.$set(this.formData, "scoreMode", this.formData.scoreMode || this.getDefaultScoreMode())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(this.formData, "maxAttempts", 1)
|
||||||
|
this.$set(this.formData, "scoreMode", this.getDefaultScoreMode())
|
||||||
|
},
|
||||||
|
getActivityGroup() {
|
||||||
|
$.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.activityGroupOptions = res.data || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadActivity() {
|
||||||
|
if (!this.activityId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$.post("/platform/qsv/activity/findOne", { id: this.activityId }).then((res) => {
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
this.formData = Object.assign({}, this.formData, res.data)
|
||||||
|
this.normalizeQuizConfig()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
saveDraft() {
|
||||||
|
const enabled = this.activityId ? this.formData.enabled !== false : false
|
||||||
|
this.saveActivity(enabled, "保存")
|
||||||
|
},
|
||||||
|
submitActivity() {
|
||||||
|
this.saveActivity(true, "提交")
|
||||||
|
},
|
||||||
|
buildSubmitData(enabled) {
|
||||||
|
const formData = Object.assign({}, this.formData, { enabled })
|
||||||
|
formData.category = this.normalizeCategoryValue(formData.category)
|
||||||
|
if (formData.category !== "QUIZ") {
|
||||||
|
delete formData.mode
|
||||||
|
delete formData.repeatable
|
||||||
|
delete formData.repeatMode
|
||||||
|
delete formData.maxAttempts
|
||||||
|
delete formData.displayMode
|
||||||
|
delete formData.randomCount
|
||||||
|
delete formData.totalRandom
|
||||||
|
delete formData.shuffleSubject
|
||||||
|
delete formData.timeLimit
|
||||||
|
delete formData.scoreMode
|
||||||
|
} else {
|
||||||
|
formData.mode = this.normalizeModeValue(formData.mode)
|
||||||
|
formData.displayMode = this.normalizeDisplayModeValue(formData.displayMode)
|
||||||
|
formData.timeLimit = formData.timeLimit || 0
|
||||||
|
if (formData.mode !== "REGULAR") {
|
||||||
|
formData.displayMode = "ALL"
|
||||||
|
delete formData.randomCount
|
||||||
|
delete formData.totalRandom
|
||||||
|
} else if (formData.displayMode !== "RANDOM") {
|
||||||
|
delete formData.randomCount
|
||||||
|
delete formData.totalRandom
|
||||||
|
} else {
|
||||||
|
formData.repeatable = false
|
||||||
|
formData.maxAttempts = 1
|
||||||
|
formData.scoreMode = this.getDefaultScoreMode()
|
||||||
|
}
|
||||||
|
if (!this.canRepeatAnswer(formData) || !formData.repeatable) {
|
||||||
|
formData.repeatable = false
|
||||||
|
formData.maxAttempts = 1
|
||||||
|
formData.scoreMode = this.getDefaultScoreMode()
|
||||||
|
} else {
|
||||||
|
formData.maxAttempts = formData.maxAttempts || 1
|
||||||
|
formData.scoreMode = formData.scoreMode || this.getDefaultScoreMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formData
|
||||||
|
},
|
||||||
|
saveActivity(enabled, actionText) {
|
||||||
|
this.$refs.formRef.validate((valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$confirm("确定" + actionText + "吗?", "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning"
|
||||||
|
}).then(() => {
|
||||||
|
this.saveLoading = true
|
||||||
|
const formData = this.buildSubmitData(enabled)
|
||||||
|
$.post("/platform/qsv/activity/save", formData).then((res) => {
|
||||||
|
if (res.code !== 0) {
|
||||||
|
this.saveLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$message.success(actionText + "成功")
|
||||||
|
location.href = "/platform/qsv/activity"
|
||||||
|
}).fail(() => {
|
||||||
|
this.saveLoading = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
location.href = "/platform/qsv/activity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.activityId = this.getQueryString("id")
|
||||||
|
this.getActivityGroup()
|
||||||
|
this.loadActivity()
|
||||||
|
if (!this.activityId) {
|
||||||
|
this.normalizeQuizConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
categoryOptions() {
|
||||||
|
const categoryList = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_CATEGORY || []) : []
|
||||||
|
return categoryList.map(item => Object.assign({}, item, {
|
||||||
|
code: this.normalizeCategoryValue(item.code)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
modeOptions() {
|
||||||
|
return [
|
||||||
|
{ code: "REGULAR", label: "常规模式" },
|
||||||
|
{ code: "SCHEDULED", label: "定时定题" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
pageTitle() {
|
||||||
|
return this.activityId ? "编辑任务" : "新建任务"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--#
|
||||||
|
}
|
||||||
|
#-->
|
||||||
@@ -8,6 +8,20 @@ const setting = {
|
|||||||
<el-form-item label="详情" prop="description">
|
<el-form-item label="详情" prop="description">
|
||||||
<text-editor v-model="formData.description"></text-editor>
|
<text-editor v-model="formData.description"></text-editor>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="补充填写" prop="fillRequired">
|
||||||
|
<el-switch
|
||||||
|
v-model="formData.fillRequired"
|
||||||
|
active-text="需要填写"
|
||||||
|
inactive-text="无需填写">
|
||||||
|
</el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="填写提示" prop="fillPlaceholder" v-if="formData.fillRequired">
|
||||||
|
<el-input
|
||||||
|
maxlength="255"
|
||||||
|
placeholder="例如:请填写原因"
|
||||||
|
v-model="formData.fillPlaceholder">
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button @click="visible = false">取 消</el-button>
|
<el-button @click="visible = false">取 消</el-button>
|
||||||
@@ -26,7 +40,11 @@ const setting = {
|
|||||||
methods:{
|
methods:{
|
||||||
onOpen(option,ext){
|
onOpen(option,ext){
|
||||||
console.log(option)
|
console.log(option)
|
||||||
this.formData = { ...option }
|
this.formData = {
|
||||||
|
...option,
|
||||||
|
fillRequired: !!option.fillRequired,
|
||||||
|
fillPlaceholder: option.fillPlaceholder || ""
|
||||||
|
}
|
||||||
this.ext = ext
|
this.ext = ext
|
||||||
this.visible = true
|
this.visible = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,20 +5,20 @@
|
|||||||
|
|
||||||
const subjectForm = {
|
const subjectForm = {
|
||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<div class="subject-form-dialog">
|
<div class="subject-form-dialog qsv-subject-editor">
|
||||||
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
|
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
|
||||||
<div style="min-height:50vh;overflow-y: auto">
|
<div style="min-height:50vh;overflow-y: auto">
|
||||||
<draggable v-model="subjects" handle=".drag-handler">
|
<draggable v-model="subjects" handle=".drag-handler">
|
||||||
<transition-group>
|
<transition-group>
|
||||||
<div v-for="(subject, subjectIndex) in subjects" :key="subject.id" class="subject-item">
|
<div v-for="(subject, subjectIndex) in subjects" :key="subject.id" class="subject-item">
|
||||||
<div class="subject-header">
|
<div class="subject-header">
|
||||||
<div style="display: flex; align-items: center;">
|
<div class="subject-title-row">
|
||||||
<i class="el-icon-rank drag-handler"></i>
|
<i class="el-icon-rank drag-handler"></i>
|
||||||
<span style="font-weight: bold; margin-right: 10px;">第 {{subjectIndex + 1}} 题</span>
|
<span style="font-weight: bold; margin-right: 10px;">第 {{subjectIndex + 1}} 题</span>
|
||||||
<el-input
|
<el-input
|
||||||
|
class="subject-title-input"
|
||||||
v-model="subject.title"
|
v-model="subject.title"
|
||||||
placeholder="请输入题目"
|
placeholder="请输入题目">
|
||||||
style="flex: 1">
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,6 +49,27 @@ const subjectForm = {
|
|||||||
最大选择数:
|
最大选择数:
|
||||||
<el-input-number v-model="subject.maxMulti" placeholder="最多可选数量"></el-input-number>
|
<el-input-number v-model="subject.maxMulti" placeholder="最多可选数量"></el-input-number>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="activity.category==='VOTE'">
|
||||||
|
排列方式:
|
||||||
|
<el-select
|
||||||
|
v-model="subject.optionLayout"
|
||||||
|
@change="onVoteLayoutChange(subject)"
|
||||||
|
placeholder="请选择排列方式"
|
||||||
|
style="width: 120px;">
|
||||||
|
<el-option label="竖向排列" value="VERTICAL"></el-option>
|
||||||
|
<el-option label="横向排列" value="HORIZONTAL"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div v-if="activity.category==='VOTE' && subject.optionLayout === 'HORIZONTAL'">
|
||||||
|
横向列数:
|
||||||
|
<el-select
|
||||||
|
v-model="subject.optionColumns"
|
||||||
|
placeholder="请选择列数"
|
||||||
|
style="width: 100px;">
|
||||||
|
<el-option label="横向2列" :value="2"></el-option>
|
||||||
|
<el-option label="横向3列" :value="3"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
<!-- <el-input-->
|
<!-- <el-input-->
|
||||||
<!-- v-model="subject.hint"-->
|
<!-- v-model="subject.hint"-->
|
||||||
<!-- placeholder="题目提示信息"-->
|
<!-- placeholder="题目提示信息"-->
|
||||||
@@ -91,7 +112,7 @@ const subjectForm = {
|
|||||||
<div class="option-content">
|
<div class="option-content">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="option.text"
|
v-model="option.text"
|
||||||
:placeholder="'选项' + (optionIndex + 1)">
|
:placeholder="'选项'+optionIndex + 1">
|
||||||
<template slot="prepend">选项{{optionIndex + 1}}</template>
|
<template slot="prepend">选项{{optionIndex + 1}}</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<!-- <el-input-->
|
<!-- <el-input-->
|
||||||
@@ -119,8 +140,7 @@ const subjectForm = {
|
|||||||
<el-switch
|
<el-switch
|
||||||
v-if="activity.category==='QUIZ'"
|
v-if="activity.category==='QUIZ'"
|
||||||
v-model="option.isCorrect"
|
v-model="option.isCorrect"
|
||||||
active-text="正确答案"
|
active-text="正确答案">
|
||||||
@change="handleCorrectAnswerChange(subject, optionIndex)">
|
|
||||||
</el-switch>
|
</el-switch>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
@@ -193,7 +213,7 @@ const subjectForm = {
|
|||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
|
<div class="qsv-subject-footer">
|
||||||
<!-- <el-button @click="visible = false">取消</el-button>-->
|
<!-- <el-button @click="visible = false">取消</el-button>-->
|
||||||
|
|
||||||
<!--<el-button type="primary" icon="el-icon-plus" @click="openBank">
|
<!--<el-button type="primary" icon="el-icon-plus" @click="openBank">
|
||||||
@@ -201,12 +221,15 @@ const subjectForm = {
|
|||||||
</el-button>-->
|
</el-button>-->
|
||||||
|
|
||||||
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
|
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
|
||||||
从文本导入
|
导入题目
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
|
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
|
||||||
添加题目
|
添加题目
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire">
|
<el-button type="primary" icon="el-icon-collection" @click="openSaveBankDialog">
|
||||||
|
存到题库
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire" v-if="!hideSaveButton">
|
||||||
保存题目
|
保存题目
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,6 +238,22 @@ const subjectForm = {
|
|||||||
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
|
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
|
||||||
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
|
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
|
||||||
<bank ref="bankRef" @confirm="onBankConfirm"></bank>
|
<bank ref="bankRef" @confirm="onBankConfirm"></bank>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="saveBankDialog.visible"
|
||||||
|
title="存到题库"
|
||||||
|
width="420px"
|
||||||
|
append-to-body
|
||||||
|
custom-class="save-bank-dialog">
|
||||||
|
<el-form :model="saveBankDialog.formData" :rules="saveBankDialog.rules" ref="saveBankFormRef" label-width="90px">
|
||||||
|
<el-form-item label="标题名称" prop="title">
|
||||||
|
<el-input v-model="saveBankDialog.formData.title" maxlength="50" show-word-limit placeholder="请输入题库标题"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="saveBankDialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBankDialog.loading" @click="saveToBank">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
components: {
|
components: {
|
||||||
@@ -223,6 +262,12 @@ const subjectForm = {
|
|||||||
'txt-import': txtImport,
|
'txt-import': txtImport,
|
||||||
'bank': subjectBank
|
'bank': subjectBank
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
hideSaveButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
@@ -231,7 +276,20 @@ const subjectForm = {
|
|||||||
subjects: [],
|
subjects: [],
|
||||||
previewDialogVisible: false,
|
previewDialogVisible: false,
|
||||||
activity: {},
|
activity: {},
|
||||||
formRules: {}
|
formRules: {},
|
||||||
|
saveBankDialog: {
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
formData: {
|
||||||
|
title: ""
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
title: [
|
||||||
|
{ required: true, message: "请输入题库标题", trigger: "blur" },
|
||||||
|
{ max: 50, message: "标题名称不能超过50个字", trigger: "blur" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -239,14 +297,19 @@ const subjectForm = {
|
|||||||
this.visible = true
|
this.visible = true
|
||||||
if (id) {
|
if (id) {
|
||||||
this.id = id
|
this.id = id
|
||||||
$.post("/platform/qsv/activity/findOne", {id}).then((res) => {
|
Promise.all([
|
||||||
if (res.code === 0) {
|
$.post("/platform/qsv/activity/findOne", {id}),
|
||||||
this.activity = res.data
|
$.post("/platform/qsv/activity/listSubjects", {activityId: id})
|
||||||
|
]).then(([activityRes, subjectsRes]) => {
|
||||||
|
if (activityRes.code === 0) {
|
||||||
|
this.activity = activityRes.data
|
||||||
}
|
}
|
||||||
})
|
if (subjectsRes.code === 0) {
|
||||||
$.post("/platform/qsv/activity/listSubjects", {activityId: id}).then((res) => {
|
this.subjects = subjectsRes.data || []
|
||||||
if (res.code === 0) {
|
this.normalizeVoteSubjectLayouts()
|
||||||
this.subjects = res.data
|
}
|
||||||
|
if (this.activity.category === "VOTE" && this.subjects.length === 0) {
|
||||||
|
this.addSubject()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -266,6 +329,8 @@ const subjectForm = {
|
|||||||
type: "radio",
|
type: "radio",
|
||||||
score: 0,
|
score: 0,
|
||||||
displayDate: "",
|
displayDate: "",
|
||||||
|
optionLayout: "VERTICAL",
|
||||||
|
optionColumns: 1,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
id: this.generateId(),
|
id: this.generateId(),
|
||||||
@@ -285,14 +350,41 @@ const subjectForm = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
//题目类型切换
|
// 投票选项排列默认值
|
||||||
subjectTypeChange(subject, subjectIndex) {
|
normalizeVoteSubjectLayout(subject) {
|
||||||
// 切换题型时先清空当前题目的正确答案状态,避免沿用旧题型下的答案配置。
|
if (!subject) {
|
||||||
;(subject.options || []).forEach((option) => {
|
return
|
||||||
if (option) {
|
}
|
||||||
this.$set(option, 'isCorrect', false)
|
if (!subject.optionLayout) {
|
||||||
}
|
this.$set(subject, "optionLayout", "VERTICAL")
|
||||||
|
}
|
||||||
|
if (subject.optionLayout === "HORIZONTAL") {
|
||||||
|
const columns = Number(subject.optionColumns)
|
||||||
|
this.$set(subject, "optionColumns", columns === 3 ? 3 : 2)
|
||||||
|
} else {
|
||||||
|
this.$set(subject, "optionLayout", "VERTICAL")
|
||||||
|
this.$set(subject, "optionColumns", 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
normalizeVoteSubjectLayouts() {
|
||||||
|
if (this.activity.category !== "VOTE") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.subjects.forEach((subject) => {
|
||||||
|
this.normalizeVoteSubjectLayout(subject)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onVoteLayoutChange(subject) {
|
||||||
|
if (subject.optionLayout === "HORIZONTAL") {
|
||||||
|
this.$set(subject, "optionColumns", Number(subject.optionColumns) === 3 ? 3 : 2)
|
||||||
|
} else {
|
||||||
|
this.$set(subject, "optionColumns", 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
subjectTypeChange(subject, subjectIndex) {
|
||||||
if (subject.type === 'text') {
|
if (subject.type === 'text') {
|
||||||
subject.options = []
|
subject.options = []
|
||||||
}
|
}
|
||||||
@@ -317,7 +409,7 @@ const subjectForm = {
|
|||||||
addOption(subject) {
|
addOption(subject) {
|
||||||
subject.options.push({
|
subject.options.push({
|
||||||
id: this.generateId(),
|
id: this.generateId(),
|
||||||
text: "选项" + (subject.options.length + 1),
|
text: "选项" + subject.options.length + 1,
|
||||||
hint: "",
|
hint: "",
|
||||||
imgUrl: null,
|
imgUrl: null,
|
||||||
isCorrect: false
|
isCorrect: false
|
||||||
@@ -358,7 +450,7 @@ const subjectForm = {
|
|||||||
|
|
||||||
//选项设置确认
|
//选项设置确认
|
||||||
onSettingConfirm({option, ext}) {
|
onSettingConfirm({option, ext}) {
|
||||||
this.subjects[ext.subjectIndex].options[ext.optionIndex] = option
|
this.$set(this.subjects[ext.subjectIndex].options, ext.optionIndex, option)
|
||||||
console.log(this.subjects[ext.subjectIndex].options[ext.optionIndex])
|
console.log(this.subjects[ext.subjectIndex].options[ext.optionIndex])
|
||||||
|
|
||||||
// this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "isCorrect", option.isCorrect)
|
// this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "isCorrect", option.isCorrect)
|
||||||
@@ -367,7 +459,7 @@ const subjectForm = {
|
|||||||
|
|
||||||
//从文本导入
|
//从文本导入
|
||||||
openTxtImport() {
|
openTxtImport() {
|
||||||
this.$refs.txtImportRef.visible = true;
|
this.$refs.txtImportRef.onOpen(this.activity.category);
|
||||||
},
|
},
|
||||||
|
|
||||||
//处理导入的题目
|
//处理导入的题目
|
||||||
@@ -382,6 +474,9 @@ const subjectForm = {
|
|||||||
// 更新导入题目的序号
|
// 更新导入题目的序号
|
||||||
importedQuestions.forEach((question, index) => {
|
importedQuestions.forEach((question, index) => {
|
||||||
question.sortNum = startSortNum + index;
|
question.sortNum = startSortNum + index;
|
||||||
|
if (this.activity.category === "VOTE") {
|
||||||
|
this.normalizeVoteSubjectLayout(question)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 将导入的题目添加到现有题目列表
|
// 将导入的题目添加到现有题目列表
|
||||||
@@ -400,67 +495,129 @@ const subjectForm = {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 单选题切换正确答案时保持只有一个正确选项,避免界面状态与题型规则不一致。
|
// 打开存到题库对话框
|
||||||
handleCorrectAnswerChange(subject, optionIndex) {
|
openSaveBankDialog() {
|
||||||
if (!subject || subject.type !== 'radio') {
|
if (!this.subjects || this.subjects.length === 0) {
|
||||||
|
this.$message.warning("请先添加题目")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const options = subject.options || []
|
this.saveBankDialog.formData.title = this.activity && this.activity.title ? this.activity.title : ""
|
||||||
const currentOption = options[optionIndex]
|
this.saveBankDialog.visible = true
|
||||||
if (!currentOption || !currentOption.isCorrect) {
|
this.$nextTick(() => {
|
||||||
return
|
if (this.$refs.saveBankFormRef) {
|
||||||
}
|
this.$refs.saveBankFormRef.clearValidate()
|
||||||
for (let index = 0; index < options.length; index++) {
|
|
||||||
if (index !== optionIndex) {
|
|
||||||
this.$set(options[index], 'isCorrect', false)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 保存前校验选项内容,避免题目存在空选项或缺少正确答案时仍然提交到后端。
|
// 复制题目到题库时重新生成ID,避免与活动题目关联
|
||||||
validateSubjectOptions() {
|
buildBankSubjects() {
|
||||||
for (let subjectIndex = 0; subjectIndex < this.subjects.length; subjectIndex++) {
|
const subjects = JSON.parse(JSON.stringify(this.subjects || []))
|
||||||
const subject = this.subjects[subjectIndex]
|
const subjectIdMap = {}
|
||||||
if (!subject || subject.type === 'text') {
|
const optionIdMap = {}
|
||||||
continue
|
const remapValue = (value, map) => {
|
||||||
}
|
if (Array.isArray(value)) {
|
||||||
const options = subject.options || []
|
return value.map((item) => map[item] || item)
|
||||||
if (options.length === 0) {
|
|
||||||
this.$message.warning('请设置第' + (subjectIndex + 1) + '题的选项')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let correctAnswerCount = 0
|
|
||||||
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
|
|
||||||
const option = options[optionIndex]
|
|
||||||
const optionText = option && option.text ? option.text.trim() : ''
|
|
||||||
if (!optionText) {
|
|
||||||
this.$message.warning('第' + (subjectIndex + 1) + '题的第' + (optionIndex + 1) + '个选项不能为空')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (option && option.isCorrect) {
|
|
||||||
correctAnswerCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.activity.category === 'QUIZ') {
|
|
||||||
if (subject.type === 'radio' && correctAnswerCount > 1) {
|
|
||||||
this.$message.warning('第' + (subjectIndex + 1) + '题为单选题,只能设置一个正确答案')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (correctAnswerCount === 0) {
|
|
||||||
this.$message.warning('第' + (subjectIndex + 1) + '题请至少设置一个正确答案')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return map[value] || value
|
||||||
}
|
}
|
||||||
return true
|
const remapVisibleConditions = (conditions) => {
|
||||||
|
if (!Array.isArray(conditions)) {
|
||||||
|
return conditions
|
||||||
|
}
|
||||||
|
return conditions.map((condition) => {
|
||||||
|
if (condition.subjectId) {
|
||||||
|
condition.subjectId = subjectIdMap[condition.subjectId] || condition.subjectId
|
||||||
|
}
|
||||||
|
if (condition.field) {
|
||||||
|
condition.field = subjectIdMap[condition.field] || condition.field
|
||||||
|
}
|
||||||
|
if (condition.optionId) {
|
||||||
|
condition.optionId = optionIdMap[condition.optionId] || condition.optionId
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(condition, "value")) {
|
||||||
|
condition.value = remapValue(condition.value, optionIdMap)
|
||||||
|
}
|
||||||
|
if (Array.isArray(condition.children)) {
|
||||||
|
condition.children = remapVisibleConditions(condition.children)
|
||||||
|
}
|
||||||
|
return condition
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects.forEach((subject) => {
|
||||||
|
const oldSubjectId = subject.id
|
||||||
|
const newSubjectId = this.generateId()
|
||||||
|
subject._bankNewSubjectId = newSubjectId
|
||||||
|
if (oldSubjectId) {
|
||||||
|
subjectIdMap[oldSubjectId] = newSubjectId
|
||||||
|
}
|
||||||
|
if (Array.isArray(subject.options)) {
|
||||||
|
subject.options.forEach((option) => {
|
||||||
|
const oldOptionId = option.id
|
||||||
|
const newOptionId = this.generateId()
|
||||||
|
option._bankNewOptionId = newOptionId
|
||||||
|
if (oldOptionId) {
|
||||||
|
optionIdMap[oldOptionId] = newOptionId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return subjects.map((subject, index) => {
|
||||||
|
const newSubjectId = this.generateId()
|
||||||
|
subject.id = subject._bankNewSubjectId || newSubjectId
|
||||||
|
delete subject._bankNewSubjectId
|
||||||
|
subject.activityId = null
|
||||||
|
subject.sortNum = index + 1
|
||||||
|
if (Array.isArray(subject.options)) {
|
||||||
|
subject.options = subject.options.map((option, optionIndex) => {
|
||||||
|
option.id = option._bankNewOptionId || this.generateId()
|
||||||
|
delete option._bankNewOptionId
|
||||||
|
option.subjectId = subject.id
|
||||||
|
option.sortNum = optionIndex + 1
|
||||||
|
return option
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Array.isArray(subject.correctAnswer)) {
|
||||||
|
subject.correctAnswer = remapValue(subject.correctAnswer, optionIdMap)
|
||||||
|
}
|
||||||
|
if (Array.isArray(subject.visibleRuleConditions)) {
|
||||||
|
subject.visibleRuleConditions = remapVisibleConditions(subject.visibleRuleConditions)
|
||||||
|
}
|
||||||
|
return subject
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存当前全部题目到题库
|
||||||
|
saveToBank() {
|
||||||
|
this.$refs.saveBankFormRef.validate((valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const qsvBank = {
|
||||||
|
title: this.saveBankDialog.formData.title,
|
||||||
|
category: this.activity.category,
|
||||||
|
subjects: this.buildBankSubjects()
|
||||||
|
}
|
||||||
|
this.saveBankDialog.loading = true
|
||||||
|
$.post("/platform/qsv/bank/save", { qsvBank: JSON.stringify(qsvBank) }).then((res) => {
|
||||||
|
this.saveBankDialog.loading = false
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success("已存到题库")
|
||||||
|
this.saveBankDialog.visible = false
|
||||||
|
}
|
||||||
|
}).fail(() => {
|
||||||
|
this.saveBankDialog.loading = false
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
//保存
|
//保存
|
||||||
saveQuestionnaire() {
|
saveQuestionnaire() {
|
||||||
if (!this.validateSubjectOptions()) {
|
this.normalizeVoteSubjectLayouts()
|
||||||
return
|
$
|
||||||
}
|
.post("/platform/qsv/activity/saveSubjects", {
|
||||||
$.post("/platform/qsv/activity/saveSubjects", {
|
|
||||||
activityId: this.id,
|
activityId: this.id,
|
||||||
subjects: JSON.stringify(this.subjects)
|
subjects: JSON.stringify(this.subjects)
|
||||||
})
|
})
|
||||||
@@ -474,6 +631,38 @@ const subjectForm = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: /*language=CSS*/ `
|
style: /*language=CSS*/ `
|
||||||
|
/deep/ .qsv-subject-editor {
|
||||||
|
padding-bottom: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .qsv-subject-footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 900;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/deep/ .qsv-subject-footer {
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .save-bank-dialog {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
transform: translateY(calc(-50% + 250px));
|
||||||
|
}
|
||||||
|
|
||||||
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
|
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
|
||||||
/* background: rgb(248, 249, 250);*/
|
/* background: rgb(248, 249, 250);*/
|
||||||
/*}*/
|
/*}*/
|
||||||
@@ -513,6 +702,15 @@ const subjectForm = {
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/deep/ .subject-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .subject-title-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/deep/ .subject-toolbar {
|
/deep/ .subject-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,52 @@
|
|||||||
const basicForm = {
|
const basicForm = {
|
||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<el-dialog :visible.sync="dialogVisible" title="基础设置" width="70%">
|
<el-drawer
|
||||||
<div style="overflow-y: auto">
|
:title="formData.id ? '编辑题库' : '新增题库'"
|
||||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="150px">
|
:visible.sync="dialogVisible"
|
||||||
<el-form-item label="类型" prop="category">
|
direction="rtl"
|
||||||
<el-radio-group v-model="formData.category" size="small">
|
size="1024px"
|
||||||
<el-radio v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :label="item.code"
|
custom-class="qsv-bank-basic-drawer"
|
||||||
:key="item.code"
|
append-to-body
|
||||||
border>
|
:wrapper-closable="false"
|
||||||
{{item.label}}
|
:close-on-press-escape="false">
|
||||||
</el-radio>
|
<div class="qsv-bank-basic-drawer-page" v-loading="saveLoading">
|
||||||
</el-radio-group>
|
<el-card shadow="never" class="qsv-bank-basic-drawer-card">
|
||||||
</el-form-item>
|
<h3 class="qsv-bank-sub-title">基础信息</h3>
|
||||||
|
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="96px">
|
||||||
|
<el-form-item label="类型" prop="category">
|
||||||
|
<el-radio-group v-model="formData.category" size="small">
|
||||||
|
<el-radio v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :label="item.code"
|
||||||
|
:key="item.code"
|
||||||
|
border>
|
||||||
|
{{item.label || item.name}}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="标题" prop="title">
|
<el-form-item label="标题" prop="title">
|
||||||
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
|
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
|
||||||
show-word-limit></el-input>
|
show-word-limit></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="说明" prop="description">
|
<el-form-item label="说明" prop="description">
|
||||||
<text-editor v-model="formData.description" :height="150"></text-editor>
|
<text-editor v-model="formData.description" :height="180"></text-editor>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div class="qsv-bank-basic-drawer-footer">
|
||||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="onSubmit">确 定</el-button>
|
<el-button type="primary" :loading="saveLoading" @click="onSubmit">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
`,
|
`,
|
||||||
dicts: ["ACTIVITY_QSV_CATEGORY", "ACTIVITY_QSV_MODE", "ACTIVITY_QSV_REPEAT_MODE", "ACTIVITY_QSV_SCORE_MODE"],
|
dicts: ["ACTIVITY_QSV_CATEGORY", "ACTIVITY_QSV_MODE", "ACTIVITY_QSV_REPEAT_MODE", "ACTIVITY_QSV_SCORE_MODE"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
|
saveLoading: false,
|
||||||
formData: {
|
formData: {
|
||||||
category: ''
|
category: 'QUIZ'
|
||||||
},
|
},
|
||||||
formRules: {
|
formRules: {
|
||||||
category: [{required: true, message: "请选择类型", trigger: "change"}],
|
category: [{required: true, message: "请选择类型", trigger: "change"}],
|
||||||
@@ -45,27 +57,42 @@ const basicForm = {
|
|||||||
methods: {
|
methods: {
|
||||||
onOpen(id) {
|
onOpen(id) {
|
||||||
this.dialogVisible = true
|
this.dialogVisible = true
|
||||||
|
this.saveLoading = false
|
||||||
if (id) {
|
if (id) {
|
||||||
$.post("/platform/qsv/bank/findOne", {id}).then((res) => {
|
$.post("/platform/qsv/bank/findOne", {id}).then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.formData = res.data
|
this.formData = res.data || {category: 'QUIZ'}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.formData = {}
|
this.formData = {
|
||||||
|
category: 'QUIZ'
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.formRef) {
|
||||||
|
this.$refs.formRef.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.$refs.formRef.validate((valid) => {
|
this.$refs.formRef.validate((valid) => {
|
||||||
if (valid) {
|
if (!valid) {
|
||||||
$.post("/platform/qsv/bank/" + (this.formData.id ? "update" : "save"), {qsvBank: JSON.stringify(this.formData)}).then((res) => {
|
return
|
||||||
if (res.code === 0) {
|
|
||||||
this.$message.success(res.msg)
|
|
||||||
this.dialogVisible = false
|
|
||||||
this.$emit("refresh", null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
this.saveLoading = true
|
||||||
|
$.post("/platform/qsv/bank/" + (this.formData.id ? "update" : "save"), {
|
||||||
|
qsvBank: JSON.stringify(this.formData)
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success(res.msg || "保存成功")
|
||||||
|
this.dialogVisible = false
|
||||||
|
this.$emit("refresh", null)
|
||||||
|
}
|
||||||
|
this.saveLoading = false
|
||||||
|
}).fail(() => {
|
||||||
|
this.saveLoading = false
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,152 @@
|
|||||||
layout("/layouts/platform.html"){
|
layout("/layouts/platform.html"){
|
||||||
#-->
|
#-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.el-drawer.qsv-bank-drawer {
|
||||||
|
top: 64px !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: auto !important;
|
||||||
|
width: calc(100% - 250px) !important;
|
||||||
|
height: calc(100vh - 64px) !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-drawer .el-drawer__header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 18px 24px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-drawer .el-drawer__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f0f2f5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-drawer-page {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 24px 72px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-drawer-card {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-sub-title {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-drawer-footer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer.qsv-bank-basic-drawer {
|
||||||
|
top: 64px !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: auto !important;
|
||||||
|
width: 1024px !important;
|
||||||
|
height: calc(100vh - 64px) !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-basic-drawer .el-drawer__header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 18px 24px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-basic-drawer .el-drawer__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f0f2f5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-basic-drawer-page {
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 16px 72px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-basic-drawer-card {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-bank-basic-drawer-footer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.el-drawer.qsv-bank-drawer,
|
||||||
|
.el-drawer.qsv-bank-basic-drawer {
|
||||||
|
top: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<guava ref="guava">
|
<guava ref="guava">
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<search @search="doSearch">
|
<search @search="doSearch">
|
||||||
<search-item label="标题">
|
<search-item label="标题名称">
|
||||||
<el-input placeholder="请输入标题" v-model="pageForm.title" clearable></el-input>
|
<el-input placeholder="请输入标题名称" v-model="pageForm.title" clearable></el-input>
|
||||||
</search-item>
|
</search-item>
|
||||||
</search>
|
</search>
|
||||||
|
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card shadow="never" class="mt10">
|
<el-card shadow="never" class="mt10">
|
||||||
<table-tool label="管理列表">
|
<table-tool label="题库列表">
|
||||||
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
|
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
|
|
||||||
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
|
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
|
||||||
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
|
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
|
||||||
<el-table-column label="标题" prop="title" sortable></el-table-column>
|
<el-table-column label="标题名称" prop="title" sortable></el-table-column>
|
||||||
<el-table-column label="类型" prop="category" sortable width="200">
|
<el-table-column label="类型" prop="category" sortable width="200">
|
||||||
<template scope="{row}">
|
<template scope="{row}">
|
||||||
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
|
<el-tag size="mini" :type="getCategoryTagType(row.category)">
|
||||||
|
{{getCategoryText(row.category)}}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" fixed="right" width="450px">
|
<el-table-column label="操作" width="450px">
|
||||||
<template scope="{row}">
|
<template scope="{row}">
|
||||||
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
|
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
|
||||||
@@ -61,6 +183,21 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getCategoryText(category) {
|
||||||
|
const options = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_CATEGORY || []) : []
|
||||||
|
const item = options.find(item => item.code === category)
|
||||||
|
return item ? item.name : category
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategoryTagType(category) {
|
||||||
|
const map = {
|
||||||
|
SURVEY: "success",
|
||||||
|
QUIZ: "primary",
|
||||||
|
VOTE: "danger"
|
||||||
|
}
|
||||||
|
return map[category] || "info"
|
||||||
|
},
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
openEdit(row) {
|
openEdit(row) {
|
||||||
this.$refs.basicFormRef.onOpen(row.id)
|
this.$refs.basicFormRef.onOpen(row.id)
|
||||||
@@ -80,7 +217,7 @@ layout("/layouts/platform.html"){
|
|||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
type: "warning"
|
type: "warning"
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
$.post("/platform/qsv/activity/delete", {id}).then((res) => {
|
$.post("/platform/qsv/bank/delete", {id}).then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.$message.success("删除成功")
|
this.$message.success("删除成功")
|
||||||
this.pageData()
|
this.pageData()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
const subjectForm = {
|
const subjectForm = {
|
||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<div class="subject-form-dialog">
|
<div class="subject-form-dialog qsv-subject-editor">
|
||||||
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
|
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
|
||||||
<div style="min-height:50vh;overflow-y: auto">
|
<div style="min-height:50vh;overflow-y: auto">
|
||||||
<draggable v-model="subjects" handle=".drag-handler">
|
<draggable v-model="subjects" handle=".drag-handler">
|
||||||
@@ -192,10 +192,10 @@ const subjectForm = {
|
|||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
|
<div class="qsv-subject-footer">
|
||||||
<!-- <el-button @click="visible = false">取消</el-button>-->
|
<!-- <el-button @click="visible = false">取消</el-button>-->
|
||||||
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
|
<el-button type="primary" icon="el-icon-upload" @click="openTxtImport">
|
||||||
从文本导入
|
导入题目
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
|
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
|
||||||
添加题目
|
添加题目
|
||||||
@@ -349,7 +349,9 @@ const subjectForm = {
|
|||||||
|
|
||||||
//从文本导入
|
//从文本导入
|
||||||
openTxtImport() {
|
openTxtImport() {
|
||||||
this.$refs.txtImportRef.visible = true;
|
this.$refs.txtImportRef.onOpen(this.activity.category, {
|
||||||
|
excludeBankId: this.id
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
//处理导入的题目
|
//处理导入的题目
|
||||||
@@ -387,6 +389,32 @@ const subjectForm = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: /*language=CSS*/ `
|
style: /*language=CSS*/ `
|
||||||
|
/deep/ .qsv-subject-editor {
|
||||||
|
padding-bottom: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .qsv-subject-footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 900;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, .04);
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/deep/ .qsv-subject-footer {
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
|
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
|
||||||
/* background: rgb(248, 249, 250);*/
|
/* background: rgb(248, 249, 250);*/
|
||||||
/*}*/
|
/*}*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@ layout("/layouts/platform.html"){
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" class="mt10">
|
<el-card shadow="never" class="mt10">
|
||||||
<table-tool label="管理列表">
|
<table-tool label="统计列表">
|
||||||
<el-button @click="exportXlsx" icon="el-icon-download" size="small" type="primary" class="mr5">
|
<el-button @click="exportXlsx" icon="el-icon-download" size="small" type="primary" class="mr5">
|
||||||
导出xlsx
|
导出xlsx
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const answer = {
|
|||||||
<el-row type="flex" justify="end" class="mb10">
|
<el-row type="flex" justify="end" class="mb10">
|
||||||
<el-button type="primary" size="small" icon="el-icon-download" @click="exportUserAnswerXlsx">导出xlsx</el-button>
|
<el-button type="primary" size="small" icon="el-icon-download" @click="exportUserAnswerXlsx">导出xlsx</el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-table :data="tableData">
|
<el-table :data="tableData" v-loading="tableLoading">
|
||||||
<el-table-column label="序号" type="index" width="70"></el-table-column>
|
<el-table-column label="序号" type="index" width="70"></el-table-column>
|
||||||
<el-table-column v-for="item in tableColumns" :key="item.prop" :label="item.label" :prop="item.prop"></el-table-column>
|
<el-table-column v-for="item in tableColumns" :key="item.prop" :label="item.label" :prop="item.prop"></el-table-column>
|
||||||
<el-table-column label="操作" fixed="right" width="100px">
|
<el-table-column label="操作" fixed="right" width="100px">
|
||||||
@@ -30,17 +30,24 @@ const answer = {
|
|||||||
onOpen(activityId) {
|
onOpen(activityId) {
|
||||||
this.activityId = activityId
|
this.activityId = activityId
|
||||||
this.dialogVisible = true
|
this.dialogVisible = true
|
||||||
|
this.tableColumns = []
|
||||||
|
this.tableData = []
|
||||||
|
this.pageForm.pageNumber = 1
|
||||||
|
this.pageForm.totalCount = 0
|
||||||
this.pageData()
|
this.pageData()
|
||||||
},
|
},
|
||||||
|
|
||||||
pageData() {
|
pageData() {
|
||||||
this.pageForm.activityId = this.activityId
|
this.pageForm.activityId = this.activityId
|
||||||
|
this.tableLoading = true
|
||||||
$.post("/platform/qsv/survey/userAnswer", this.pageForm).then((res) => {
|
$.post("/platform/qsv/survey/userAnswer", this.pageForm).then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.tableColumns = res.data.tableColumns
|
this.tableColumns = res.data.tableColumns
|
||||||
this.tableData = res.data.tableData.list
|
this.tableData = res.data.tableData.list
|
||||||
this.pageForm.totalCount = res.data.tableData.totalCount
|
this.pageForm.totalCount = res.data.tableData.totalCount
|
||||||
}
|
}
|
||||||
|
}).always(() => {
|
||||||
|
this.tableLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -53,7 +60,7 @@ const answer = {
|
|||||||
$.post("/platform/qsv/survey/deleteUserAnswer", { id }).then((res) => {
|
$.post("/platform/qsv/survey/deleteUserAnswer", { id }).then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.$message.success(res.msg)
|
this.$message.success(res.msg)
|
||||||
this.pageData()()
|
this.pageData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,42 @@
|
|||||||
layout("/layouts/platform.html"){
|
layout("/layouts/platform.html"){
|
||||||
#-->
|
#-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.survey-report-dialog {
|
||||||
|
margin-top: 40px !important;
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-report-dialog .el-dialog__header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-report-dialog .el-dialog__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-report-toolbar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-report-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<guava>
|
<guava>
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
@@ -24,17 +60,19 @@ layout("/layouts/platform.html"){
|
|||||||
</table-tool>
|
</table-tool>
|
||||||
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
|
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
|
||||||
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
|
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
|
||||||
<el-table-column label="标题" prop="title" width="320" sortable></el-table-column>
|
<el-table-column label="标题名称" prop="title" width="320" sortable></el-table-column>
|
||||||
<el-table-column label="类型" prop="category" sortable>
|
<el-table-column label="类型" prop="category" sortable>
|
||||||
<template scope="{row}">
|
<template scope="{row}">
|
||||||
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
|
<el-tag size="mini" :type="getCategoryTagType(row.category)">
|
||||||
|
{{getCategoryText(row.category)}}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
|
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
|
||||||
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
|
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
|
||||||
<el-table-column label="操作" fixed="right" width="200px">
|
<el-table-column label="操作" fixed="right" width="200px">
|
||||||
<template scope="{row}">
|
<template scope="{row}">
|
||||||
<el-button size="mini" type="primary" @click="$refs.reportRef.onOpen(row.id)">查看分析
|
<el-button size="mini" type="primary" @click="openReportPage(row.id)">查看分析
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="primary" @click="$refs.answerRef.onOpen(row.id)">查看答卷
|
<el-button size="mini" type="primary" @click="$refs.answerRef.onOpen(row.id)">查看答卷
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -44,22 +82,38 @@ layout("/layouts/platform.html"){
|
|||||||
<!--#include("/layouts/pagination.html"){}#-->
|
<!--#include("/layouts/pagination.html"){}#-->
|
||||||
</el-card>
|
</el-card>
|
||||||
</guava>
|
</guava>
|
||||||
<report ref="reportRef"></report>
|
|
||||||
<answer ref="answerRef"></answer>
|
<answer ref="answerRef"></answer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="${cspNonce!}">
|
<script nonce="${cspNonce!}">
|
||||||
<!--#include('report.js'){}#-->
|
|
||||||
<!--#include('answer.js'){}#-->
|
<!--#include('answer.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
dicts: ["ACTIVITY_QSV_CATEGORY"],
|
dicts: ["ACTIVITY_QSV_CATEGORY"],
|
||||||
components: {report, answer},
|
components: {answer},
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getCategoryText(category) {
|
||||||
|
const options = this.dict && this.dict.type ? (this.dict.type.ACTIVITY_QSV_CATEGORY || []) : []
|
||||||
|
const item = options.find(item => item.code === category)
|
||||||
|
return item ? item.name : category
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategoryTagType(category) {
|
||||||
|
const map = {
|
||||||
|
SURVEY: "success",
|
||||||
|
QUIZ: "primary",
|
||||||
|
VOTE: "danger"
|
||||||
|
}
|
||||||
|
return map[category] || "info"
|
||||||
|
},
|
||||||
|
|
||||||
|
openReportPage(activityId) {
|
||||||
|
window.location.href = "/platform/qsv/survey/reportPage?activityId=" + encodeURIComponent(activityId)
|
||||||
|
},
|
||||||
exportXlsx() {
|
exportXlsx() {
|
||||||
this.$downLoad("/platform/qsv/survey/exportXlsx", this.pageForm)
|
this.$downLoad("/platform/qsv/survey/exportXlsx", this.pageForm)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,44 @@
|
|||||||
const report = {
|
const report = {
|
||||||
/*language=HTML*/
|
/*language=HTML*/
|
||||||
template: `
|
template: `
|
||||||
<el-dialog title="分析报告" :visible.sync="dialogVisible" width="50%">
|
<el-dialog title="分析报告" :visible.sync="dialogVisible" width="50%" custom-class="survey-report-dialog">
|
||||||
<el-row type="flex" justify="end" class="mb10">
|
<div class="survey-report-toolbar">
|
||||||
<el-button type="primary" size="small" icon="el-icon-download" @click="exportReportXlsx">导出xlsx</el-button>
|
<el-row type="flex" justify="end" class="mb10">
|
||||||
</el-row>
|
<el-button type="primary" size="small" icon="el-icon-download" @click="exportReportXlsx">导出xlsx</el-button>
|
||||||
<div v-for="(subject,subjectIndex) in subjects" class="subject">
|
</el-row>
|
||||||
<div>
|
</div>
|
||||||
<div class="title">
|
<div class="survey-report-body">
|
||||||
第{{subjectIndex+1}}题: {{subject.title}}
|
<div v-for="(subject,subjectIndex) in subjects" class="subject">
|
||||||
<span class="type" v-if="subject.type==='text'">[填空题]</span>
|
<div>
|
||||||
<span class="type" v-else-if="subject.type==='radio'">[单选题]</span>
|
<div class="title">
|
||||||
<span class="type" v-else-if="subject.type==='text'">[多选题]</span>
|
第{{subjectIndex+1}}题: {{subject.title}}
|
||||||
</div>
|
<span class="type" v-if="subject.type==='text'">[填空题]</span>
|
||||||
|
<span class="type" v-else-if="subject.type==='radio'">[单选题]</span>
|
||||||
|
<span class="type" v-else-if="subject.type==='checkbox'">[多选题]</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="subject.type==='text'">
|
<div v-if="subject.type==='text'">
|
||||||
{{subject.texts.join('、')}}
|
<el-button type="text" @click="openTextDetail(subject)">查看填写内容</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="subject.type==='radio' || subject.type==='checkbox'">
|
<div v-else-if="subject.type==='radio' || subject.type==='checkbox'">
|
||||||
<el-table :data="subject.options" size="small">
|
<el-table :data="subject.options" size="small">
|
||||||
<el-table-column label="选项" prop="text"></el-table-column>
|
<el-table-column label="选项" prop="text"></el-table-column>
|
||||||
<el-table-column label="小计" prop="selectCount" width="100"></el-table-column>
|
<el-table-column label="小计" prop="selectCount" width="100"></el-table-column>
|
||||||
<el-table-column label="比例" width="500">
|
<el-table-column label="比例" width="500">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-progress
|
<el-progress
|
||||||
:percentage="Math.round(scope.row.selectCount / subject.selectTotal * 100)"></el-progress>
|
:percentage="Math.round(scope.row.selectCount / subject.selectTotal * 100)"></el-progress>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="详情" width="100px">
|
<el-table-column label="详情" width="100px">
|
||||||
<template slot-scope="{row}">
|
<template slot-scope="{row}">
|
||||||
<el-button type="text" @click="openDetail(subject.id,row.id)">详情</el-button>
|
<el-button type="text" @click="openDetail(subject.id,row.id)">详情</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,9 +48,22 @@ const report = {
|
|||||||
<el-table-column label="工号" prop="loginName"></el-table-column>
|
<el-table-column label="工号" prop="loginName"></el-table-column>
|
||||||
<el-table-column label="所属工会" prop="unionName"></el-table-column>
|
<el-table-column label="所属工会" prop="unionName"></el-table-column>
|
||||||
<el-table-column label="所属单位" prop="unitName"></el-table-column>
|
<el-table-column label="所属单位" prop="unitName"></el-table-column>
|
||||||
|
<el-table-column label="补充理由" prop="fillContent" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column label="选择时间" prop="attemptDate"></el-table-column>
|
<el-table-column label="选择时间" prop="attemptDate"></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog :title="textSubject.title || '填写内容'" :visible.sync="textVisible" width="50%" append-to-body>
|
||||||
|
<el-table :data="textAnswers" size="small">
|
||||||
|
<el-table-column label="序号" type="index" width="70"></el-table-column>
|
||||||
|
<el-table-column label="姓名" prop="userName" width="100"></el-table-column>
|
||||||
|
<el-table-column label="工号" prop="loginName" width="120"></el-table-column>
|
||||||
|
<el-table-column label="所属工会" prop="unionName" width="140"></el-table-column>
|
||||||
|
<el-table-column label="所属单位" prop="unitName" width="180" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column label="填写内容" prop="text" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column label="提交时间" prop="submitTime" width="160"></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
`,
|
`,
|
||||||
@@ -59,7 +75,10 @@ const report = {
|
|||||||
report: null,
|
report: null,
|
||||||
subjects: [],
|
subjects: [],
|
||||||
optionVisible: false,
|
optionVisible: false,
|
||||||
optionUsers: []
|
optionUsers: [],
|
||||||
|
textVisible: false,
|
||||||
|
textSubject: {},
|
||||||
|
textAnswers: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -73,8 +92,6 @@ const report = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
openDetail(subjectId, optionId) {
|
openDetail(subjectId, optionId) {
|
||||||
console.log(subjectId)
|
|
||||||
console.log(optionId)
|
|
||||||
$.post("/platform/qsv/survey/selectOptionUsers", {
|
$.post("/platform/qsv/survey/selectOptionUsers", {
|
||||||
activityId: this.activityId,
|
activityId: this.activityId,
|
||||||
subjectId,
|
subjectId,
|
||||||
@@ -87,6 +104,13 @@ const report = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
openTextDetail(subject) {
|
||||||
|
this.textSubject = subject
|
||||||
|
this.textAnswers = subject.textAnswers || (subject.texts || []).map((text) => {
|
||||||
|
return { text }
|
||||||
|
})
|
||||||
|
this.textVisible = true
|
||||||
|
},
|
||||||
exportReportXlsx(){
|
exportReportXlsx(){
|
||||||
this.$downLoad("/platform/qsv/survey/exportReportXlsx", { activityId: this.activityId })
|
this.$downLoad("/platform/qsv/survey/exportReportXlsx", { activityId: this.activityId })
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -20,7 +20,7 @@
|
|||||||
<!-- <el-input placeholder="请输入内容" clearable v-model="pageForm.searchKeyword"></el-input>-->
|
<!-- <el-input placeholder="请输入内容" clearable v-model="pageForm.searchKeyword"></el-input>-->
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="所属工会">
|
<search-item label="所属工会">
|
||||||
<el-select @change="flushUnits" @clear="flushUnits" clearable filterable
|
<el-select @change="handleUnionChange" @clear="handleUnionClear" clearable filterable
|
||||||
placeholder="请选择所属工会" style="width: 100%;" v-model="pageForm.unionId">
|
placeholder="请选择所属工会" style="width: 100%;" v-model="pageForm.unionId">
|
||||||
<el-option :key="item.id" :label="item.name" :value="item.id"
|
<el-option :key="item.id" :label="item.name" :value="item.id"
|
||||||
v-for="item in unions"></el-option>
|
v-for="item in unions"></el-option>
|
||||||
@@ -94,6 +94,17 @@
|
|||||||
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
|
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
|
||||||
this.$emit('search', pageForm)
|
this.$emit('search', pageForm)
|
||||||
},
|
},
|
||||||
|
handleUnionChange() {
|
||||||
|
this.flushUnits()
|
||||||
|
this.$emit("union-change", this.pageForm.unionId || "")
|
||||||
|
},
|
||||||
|
handleUnionClear() {
|
||||||
|
this.handleUnionChange()
|
||||||
|
},
|
||||||
|
setUnionFromTree(unionId) {
|
||||||
|
this.$set(this.pageForm, "unionId", unionId || "")
|
||||||
|
this.flushUnits()
|
||||||
|
},
|
||||||
async initData(){
|
async initData(){
|
||||||
if (this.$auth.hasRoleOr("SYSADMIN, SCHOOL_UNION_ADMIN, SCHOOL_UNION_MEMBER_ADMIN")) {
|
if (this.$auth.hasRoleOr("SYSADMIN, SCHOOL_UNION_ADMIN, SCHOOL_UNION_MEMBER_ADMIN")) {
|
||||||
this.$businessTool.listUnion(this.pageForm.unionId).then((data) => {
|
this.$businessTool.listUnion(this.pageForm.unionId).then((data) => {
|
||||||
|
|||||||
@@ -4,88 +4,187 @@ layout("/layouts/platform.html"){
|
|||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<guava ref="guava">
|
<guava ref="guava">
|
||||||
<template>
|
<template>
|
||||||
<el-card shadow="never">
|
<el-row type="flex" :gutter="20" style="height: calc(100vh - 126px)">
|
||||||
<common-query ref="commonQueryRef" @search="search"></common-query>
|
<el-col :span="5">
|
||||||
</el-card>
|
<el-card shadow="never" style="height: 100%" :body-style="{ height: '100%' }">
|
||||||
|
<el-input placeholder="请输入关键字进行查找" v-model="filterText" clearable></el-input>
|
||||||
|
<div style="max-height: calc(100vh - 205px); overflow-y: auto; margin-top: 10px;">
|
||||||
|
<el-tree
|
||||||
|
:data="treeData"
|
||||||
|
ref="treeRef"
|
||||||
|
node-key="id"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:props="{
|
||||||
|
children: 'children',
|
||||||
|
label: 'name'
|
||||||
|
}"
|
||||||
|
default-expand-all
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
highlight-current
|
||||||
|
@node-click="treeNodeClick"
|
||||||
|
>
|
||||||
|
<template slot-scope="{ node, data }">
|
||||||
|
<span class="el-tree-node__label">
|
||||||
|
<i class="el-icon-folder-opened" v-if="node.level===1"></i>
|
||||||
|
<i class="el-icon-folder" v-else></i>
|
||||||
|
{{node.label}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="19">
|
||||||
|
<el-card shadow="never" v-show="tabActive === 'memberList'">
|
||||||
|
<common-query ref="commonQueryRef" @search="search" @union-change="queryUnionChange"></common-query>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never" v-show="tabActive !== 'memberList' && currentUnionId">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; color: #303133;">
|
||||||
|
<i class="el-icon-office-building"></i>
|
||||||
|
<span>{{ unionOverview.unionName || '-' }}</span>
|
||||||
|
<el-tag size="mini" type="primary">基层工会</el-tag>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px; color: #606266; font-size: 13px;">
|
||||||
|
<span>负责人:{{ unionOverview.chairman || '-' }}</span>
|
||||||
|
<span style="margin: 0 12px; color: #dcdfe6;">|</span>
|
||||||
|
<span>联系电话:{{ unionOverview.telephone || unionOverview.chairmanMobile || '-' }}</span>
|
||||||
|
<span style="margin: 0 12px; color: #dcdfe6;">|</span>
|
||||||
|
<span>当前会员总数:{{ unionOverview.memberTotal || 0 }} 人</span>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="12" style="margin-top: 18px;">
|
||||||
|
<el-col :span="6" v-for="card in unionOverviewCards" :key="card.label">
|
||||||
|
<div style="background: #f5f9ff; border: 1px solid #e6f0ff; border-radius: 6px; padding: 18px 20px;">
|
||||||
|
<div style="font-size: 28px; line-height: 1; font-weight: 700; color: #0b72c9;">{{ card.value }}</div>
|
||||||
|
<div style="margin-top: 10px; color: #606266; font-size: 13px;">{{ card.label }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" class="mt10">
|
<el-card shadow="never" class="mt10">
|
||||||
<table-tool label="会员列表">
|
<el-tabs v-model="tabActive" type="card" @tab-click="tabChange">
|
||||||
<el-button type="primary" size="small" icon="el-icon-upload2" @click="openImport">导入会员</el-button>
|
<el-tab-pane name="memberList">
|
||||||
<el-button
|
<span slot="label">
|
||||||
type="primary"
|
<i class="el-icon-user"></i>
|
||||||
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"
|
会员列表
|
||||||
size="small"
|
</span>
|
||||||
icon="el-icon-time"
|
<table-tool label="会员列表">
|
||||||
@click="openArchive"
|
<el-button type="primary" size="small" icon="el-icon-upload2" @click="openImport">导入会员</el-button>
|
||||||
>
|
<el-button
|
||||||
备份历史会员
|
type="primary"
|
||||||
</el-button>
|
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"
|
||||||
<el-button type="primary" size="small" icon="el-icon-download" @click="exportMember">会员档案导出</el-button>
|
size="small"
|
||||||
<el-popover
|
icon="el-icon-time"
|
||||||
placement="bottom"
|
@click="openArchive"
|
||||||
trigger="click"
|
>
|
||||||
width="200">
|
备份历史会员
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" size="small" icon="el-icon-download" @click="exportMember">会员档案导出</el-button>
|
||||||
|
<el-popover
|
||||||
|
placement="bottom"
|
||||||
|
trigger="click"
|
||||||
|
width="200">
|
||||||
|
|
||||||
<div style="padding:4px 0;border-bottom:1px solid #eee;">
|
<div style="padding:4px 0;border-bottom:1px solid #eee;">
|
||||||
<el-button size="mini" @click="checkAll">全选</el-button>
|
<el-button size="mini" @click="checkAll">全选</el-button>
|
||||||
<!-- <el-button size="mini" @click="invertCheck">反选</el-button>-->
|
<!-- <el-button size="mini" @click="invertCheck">反选</el-button>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="max-height:40vh;overflow-y:auto;padding:6px 0;">
|
<div style="max-height:40vh;overflow-y:auto;padding:6px 0;">
|
||||||
<el-checkbox-group v-model="checkedFields">
|
<el-checkbox-group v-model="checkedFields">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-for="f in tableColumns"
|
v-for="f in tableColumns"
|
||||||
:key="f.prop"
|
:key="f.prop"
|
||||||
:label="f.prop"
|
:label="f.prop"
|
||||||
style="display:block;margin:6px 0;">
|
style="display:block;margin:6px 0;">
|
||||||
{{ f.label }}
|
{{ f.label }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
slot="reference"
|
slot="reference"
|
||||||
type="text"
|
type="text"
|
||||||
icon="el-icon-s-operation"
|
icon="el-icon-s-operation"
|
||||||
size="small"
|
size="small"
|
||||||
style="margin-left:12px;">
|
style="margin-left:12px;">
|
||||||
列设置
|
列设置
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
|
|
||||||
<el-table :data="tableData" style="width: 100%" row-key="id" @sort-change="pageOrder" :size="tableSize" class="vi-table">
|
<el-table :data="tableData" style="width: 100%" row-key="id" @sort-change="pageOrder" :size="tableSize" class="vi-table">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
align="center"
|
align="center"
|
||||||
header-align="center"
|
header-align="center"
|
||||||
type="index"
|
type="index"
|
||||||
fixed="left"
|
fixed="left"
|
||||||
:index="indexMethod"
|
:index="indexMethod"
|
||||||
label="序号"
|
label="序号"
|
||||||
width="80px"
|
width="80px"
|
||||||
></el-table-column>
|
></el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
align="center"
|
align="center"
|
||||||
header-align="center"
|
header-align="center"
|
||||||
v-for="column in tableColumns"
|
v-for="column in tableColumns"
|
||||||
v-if="checkedFields.includes(column.prop)"
|
v-if="checkedFields.includes(column.prop)"
|
||||||
:label="column.label"
|
:label="column.label"
|
||||||
:fixed="column.fixed"
|
:fixed="column.fixed"
|
||||||
:prop="column.prop"
|
:prop="column.prop"
|
||||||
:sortable="column.sortable"
|
:sortable="column.sortable"
|
||||||
:width="column.width"
|
:width="column.width"
|
||||||
min-width="50"
|
min-width="50"
|
||||||
></el-table-column>
|
></el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="userOnline" align="center" header-align="center" label="操作" fixed="right" width="100px">
|
<el-table-column prop="userOnline" align="center" header-align="center" label="操作" fixed="right" width="100px">
|
||||||
<template scope="scope">
|
<template scope="scope">
|
||||||
<el-button size="mini" type="primary" @click="openView(scope.row.id)">查看</el-button>
|
<el-button size="mini" type="primary" @click="openView(scope.row.id)">查看</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<!--#include("/layouts/pagination.html"){}#-->
|
<!--#include("/layouts/pagination.html"){}#-->
|
||||||
</el-card>
|
</el-tab-pane>
|
||||||
|
<el-tab-pane name="branchUnionUserManage">
|
||||||
|
<span slot="label">
|
||||||
|
<i class="el-icon-school"></i>
|
||||||
|
分工会干部
|
||||||
|
</span>
|
||||||
|
<sys-union-branch-union-user-manage
|
||||||
|
v-if="currentUnionId"
|
||||||
|
ref="branchUnionUserManageRef"
|
||||||
|
:union_id="currentUnionId"
|
||||||
|
></sys-union-branch-union-user-manage>
|
||||||
|
<el-empty v-else description="请选择一个分工会"></el-empty>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane name="branchUnionPartUnitManage">
|
||||||
|
<span slot="label">
|
||||||
|
<i class="el-icon-school"></i>
|
||||||
|
组成单位
|
||||||
|
</span>
|
||||||
|
<sys-union-branch-union-part-unit-manage
|
||||||
|
v-if="currentUnionId"
|
||||||
|
ref="branchUnionPartUnitManageRef"
|
||||||
|
:union_id="currentUnionId"
|
||||||
|
></sys-union-branch-union-part-unit-manage>
|
||||||
|
<el-empty v-else description="请选择一个分工会"></el-empty>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane name="branchUnionGroup">
|
||||||
|
<span slot="label">
|
||||||
|
<i class="el-icon-school"></i>
|
||||||
|
工会小组
|
||||||
|
</span>
|
||||||
|
<sys-union-branch-union-group-manage
|
||||||
|
v-if="currentUnionId"
|
||||||
|
ref="branchUnionGroupRef"
|
||||||
|
:union_id="currentUnionId"
|
||||||
|
></sys-union-branch-union-group-manage>
|
||||||
|
<el-empty v-else description="请选择一个分工会"></el-empty>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #view>
|
<template #view>
|
||||||
@@ -109,12 +208,21 @@ layout("/layouts/platform.html"){
|
|||||||
<!--#include("archiveMember.js"){}#-->
|
<!--#include("archiveMember.js"){}#-->
|
||||||
<!--#include("../common/commonQuery.js"){}#-->
|
<!--#include("../common/commonQuery.js"){}#-->
|
||||||
<!--#include("../../common/info/memberInfo.js"){}#-->
|
<!--#include("../../common/info/memberInfo.js"){}#-->
|
||||||
|
<!--#include("/platform/sys/union/branchUnionUserManage.js"){}#-->
|
||||||
|
<!--#include("/platform/sys/union/branchUnionPartUnitManage.js"){}#-->
|
||||||
|
<!--#include("/platform/sys/union/branchUnionGroupManage.js"){}#-->
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
filterText: null,
|
||||||
|
unionRootId: "member-union-root",
|
||||||
|
treeData: [],
|
||||||
|
currentTreeData: null,
|
||||||
|
tabActive: "memberList",
|
||||||
|
unionOverview: {},
|
||||||
pageForm: {},
|
pageForm: {},
|
||||||
tableColumns: [
|
tableColumns: [
|
||||||
{ prop: "loginname", label: "工号", width: 120, fixed: "left"},
|
{ prop: "loginname", label: "工号", width: 120, fixed: "left"},
|
||||||
@@ -122,17 +230,17 @@ layout("/layouts/platform.html"){
|
|||||||
{ prop: "sex", label: "性别", width: 120},
|
{ prop: "sex", label: "性别", width: 120},
|
||||||
{ prop: "birthday", label: "出生年月", width: 120, sortable: true },
|
{ prop: "birthday", label: "出生年月", width: 120, sortable: true },
|
||||||
{ prop: "nationality", label: "国籍", width: 120, sortable: true, checked: 0 },
|
{ prop: "nationality", label: "国籍", width: 120, sortable: true, checked: 0 },
|
||||||
{ prop: "nation", label: "民族", width: 120, sortable: true},
|
// { prop: "nation", label: "民族", width: 120, sortable: true},
|
||||||
{ prop: "mobile", label: "联系电话", width: 120 },
|
{ prop: "mobile", label: "联系电话", width: 120 },
|
||||||
{ prop: "political", label: "政治面貌", width: 120, sortable: true},
|
// { prop: "political", label: "政治面貌", width: 120, sortable: true},
|
||||||
{ prop: "userState", label: "在职状态", width: 120, sortable: true },
|
{ prop: "userState", label: "在职状态", width: 120, sortable: true },
|
||||||
{ prop: "personType", label: "人员类型", width: 120, sortable: true, checked: 0 },
|
{ prop: "personType", label: "人员类型", width: 120, sortable: true, checked: 0 },
|
||||||
// { prop: "preparedBy", label: "编制类别", width: 120, sortable: true },
|
// { prop: "preparedBy", label: "编制类别", width: 120, sortable: true },
|
||||||
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
|
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
|
||||||
{ prop: "unionName", label: "所属工会", width: 120, sortable: true },
|
{ prop: "unionName", label: "所属工会", width: 120, sortable: true },
|
||||||
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
|
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
|
||||||
{ prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
|
||||||
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
// { prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
|
||||||
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
|
||||||
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
|
||||||
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
|
||||||
@@ -154,11 +262,19 @@ layout("/layouts/platform.html"){
|
|||||||
importDialog: false
|
importDialog: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
filterText(val) {
|
||||||
|
this.$refs.treeRef.filter(val)
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
"file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime()),
|
"file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime()),
|
||||||
"archive-member": ARCHIVE_MEMBER,
|
"archive-member": ARCHIVE_MEMBER,
|
||||||
"member-info": MEMBER_INFO,
|
"member-info": MEMBER_INFO,
|
||||||
"common-query": COMMON_QUERY,
|
"common-query": COMMON_QUERY,
|
||||||
|
"sys-union-branch-union-user-manage": branchUnionUserManage,
|
||||||
|
"sys-union-branch-union-part-unit-manage": branchUnionPartUnitManage,
|
||||||
|
"sys-union-branch-union-group-manage": branchUnionGroupManage,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openImport() {
|
openImport() {
|
||||||
@@ -188,6 +304,137 @@ layout("/layouts/platform.html"){
|
|||||||
this.$refs.guava.view(() => {
|
this.$refs.guava.view(() => {
|
||||||
this.$refs.infoRef.onOpen(userId)
|
this.$refs.infoRef.onOpen(userId)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
initDefaultView() {
|
||||||
|
this.tabActive = "memberList"
|
||||||
|
this.currentTreeData = null
|
||||||
|
this.unionOverview = {}
|
||||||
|
this.pageForm = {
|
||||||
|
...this.pageForm,
|
||||||
|
unionId: "",
|
||||||
|
unitId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
treeNodeClick(data) {
|
||||||
|
const unionId = data && data.type === "union" ? data.id : ""
|
||||||
|
this.currentTreeData = data
|
||||||
|
if (this.$refs.commonQueryRef) {
|
||||||
|
this.$refs.commonQueryRef.setUnionFromTree(unionId)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.tabActive === "memberList") {
|
||||||
|
this.$refs.commonQueryRef.doSearch()
|
||||||
|
} else {
|
||||||
|
this.loadUnionOverview()
|
||||||
|
this.refreshCurrentTab()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.pageForm = {
|
||||||
|
...this.pageForm,
|
||||||
|
unionId,
|
||||||
|
unitId: null
|
||||||
|
}
|
||||||
|
if (this.tabActive === "memberList") {
|
||||||
|
this.pageData()
|
||||||
|
} else {
|
||||||
|
this.loadUnionOverview()
|
||||||
|
this.refreshCurrentTab()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
queryUnionChange(unionId) {
|
||||||
|
const nodeKey = unionId || this.unionRootId
|
||||||
|
this.currentTreeData = this.findTreeNode(this.treeData, nodeKey)
|
||||||
|
this.setCurrentTreeNode(nodeKey)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.tabActive !== "memberList") {
|
||||||
|
this.loadUnionOverview()
|
||||||
|
this.refreshCurrentTab()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
filterNode(value, data) {
|
||||||
|
if (!value) return true
|
||||||
|
return data.name.indexOf(value) !== -1
|
||||||
|
},
|
||||||
|
findTreeNode(list, id) {
|
||||||
|
for (let i = 0; i < (list || []).length; i++) {
|
||||||
|
const item = list[i]
|
||||||
|
if (item.id === id) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
const child = this.findTreeNode(item.children, id)
|
||||||
|
if (child) {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
getTreeData() {
|
||||||
|
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "SCHOOL_UNION_MEMBER_ADMIN"])
|
||||||
|
const unionId = hasAdmin ? null : this.$store.state.user.union.id
|
||||||
|
this.$businessTool.listUnion(unionId).then((data) => {
|
||||||
|
this.treeData = [{
|
||||||
|
id: this.unionRootId,
|
||||||
|
name: "工会委员会",
|
||||||
|
type: "root",
|
||||||
|
children: (data || []).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
type: "union"
|
||||||
|
}))
|
||||||
|
}]
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.currentTreeData = this.treeData[0]
|
||||||
|
this.setCurrentTreeNode(this.unionRootId)
|
||||||
|
if (this.tabActive !== "memberList") {
|
||||||
|
this.tabActive = "memberList"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setCurrentTreeNode(nodeKey) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.treeRef) {
|
||||||
|
this.$refs.treeRef.setCurrentKey(nodeKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tabChange(tab) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (tab.name === "memberList") {
|
||||||
|
if (this.$refs.commonQueryRef) {
|
||||||
|
this.$refs.commonQueryRef.doSearch()
|
||||||
|
} else {
|
||||||
|
this.pageData()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.loadUnionOverview()
|
||||||
|
this.refreshCurrentTab(tab.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadUnionOverview() {
|
||||||
|
if (!this.currentUnionId) {
|
||||||
|
this.unionOverview = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$axios.post(loc() + "/unionOverview", { unionId: this.currentUnionId }).then((data) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.unionOverview = data.data || {}
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshCurrentTab(tabName = this.tabActive) {
|
||||||
|
if (tabName === "memberList" || !this.currentUnionId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ref = this.$refs[tabName + "Ref"]
|
||||||
|
if (ref && ref.doSearch) {
|
||||||
|
ref.doSearch()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
search(pageForm) {
|
search(pageForm) {
|
||||||
this.pageForm = {
|
this.pageForm = {
|
||||||
@@ -227,10 +474,23 @@ layout("/layouts/platform.html"){
|
|||||||
computed: {
|
computed: {
|
||||||
showColumns() {
|
showColumns() {
|
||||||
return this.tableColumns.filter(c => this.checkedFields.includes(c.prop));
|
return this.tableColumns.filter(c => this.checkedFields.includes(c.prop));
|
||||||
}
|
},
|
||||||
|
currentUnionId() {
|
||||||
|
return this.currentTreeData && this.currentTreeData.type === "union" ? this.currentTreeData.id : ""
|
||||||
|
},
|
||||||
|
unionOverviewCards() {
|
||||||
|
return [
|
||||||
|
{ label: "会员总数", value: this.unionOverview.memberTotal || 0 },
|
||||||
|
{ label: "教职工数", value: this.unionOverview.staffTotal || 0 },
|
||||||
|
{ label: "男性", value: this.unionOverview.maleTotal || 0 },
|
||||||
|
{ label: "女性", value: this.unionOverview.femaleTotal || 0 },
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.checkedFields = this.tableColumns.filter(c => c.checked !== 0).map(c => c.prop);
|
this.checkedFields = this.tableColumns.filter(c => c.checked !== 0).map(c => c.prop);
|
||||||
|
this.initDefaultView()
|
||||||
|
this.getTreeData()
|
||||||
this.pageData()
|
this.pageData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,30 +2,71 @@
|
|||||||
layout("/layouts/platform_h5.html"){
|
layout("/layouts/platform_h5.html"){
|
||||||
#-->
|
#-->
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak class="qsv-h5-page">
|
||||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
|
<div class="qsv-h5-header">
|
||||||
|
<div class="qsv-h5-nav">
|
||||||
|
<button class="qsv-h5-back-btn" @click="historyBack">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</button>
|
||||||
|
<div class="qsv-h5-nav-title">问卷服务</div>
|
||||||
|
<div class="qsv-h5-nav-placeholder"></div>
|
||||||
|
</div>
|
||||||
|
<div class="qsv-h5-search-row">
|
||||||
|
<van-search
|
||||||
|
v-model="pageForm.title"
|
||||||
|
shape="round"
|
||||||
|
background="transparent"
|
||||||
|
placeholder="搜索标题名称"
|
||||||
|
clearable
|
||||||
|
@search="onRefresh"
|
||||||
|
@clear="onRefresh">
|
||||||
|
</van-search>
|
||||||
|
<button class="qsv-h5-search-btn" @click="onRefresh">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="qsv-h5-filter-row">
|
||||||
|
<button
|
||||||
|
v-for="item in categoryOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:class="['qsv-h5-filter-btn', pageForm.category === item.value ? 'active' : '']"
|
||||||
|
@click="changeCategory(item.value)">
|
||||||
|
<i :class="pageForm.category === item.value ? 'fa fa-check-circle' : 'fa fa-circle-o'"></i>
|
||||||
|
<span>{{item.label}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<van-sticky offset-top="46px">
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" class="qsv-h5-list-wrap">
|
||||||
<van-tabs v-model="pageForm.category" @change="doSearch">
|
<van-list v-model="loading" :finished="finished" finished-text="暂无更多内容" @load="onLoad">
|
||||||
<van-tab v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :name="item.code" :title="item.label"
|
<div
|
||||||
:key="item.code"></van-tab>
|
v-for="row in list"
|
||||||
</van-tabs>
|
:key="row.id"
|
||||||
</van-sticky>
|
class="qsv-h5-card"
|
||||||
|
@click="onEnter(row)">
|
||||||
<table-list api="/platform/h5/qsv/pageData" :page_form.sync="pageForm" ref="tableListRef" title="title"
|
<div class="qsv-h5-cover">
|
||||||
@ready="doSearch">
|
<img :src="row.cover || defaultCover" alt="">
|
||||||
<!-- <template #header="{index,row}"></template>-->
|
</div>
|
||||||
<template v-slot="{index,row}">
|
<div class="qsv-h5-card-body">
|
||||||
<table-column label="开始时间">{{row.startTime}}</table-column>
|
<div class="qsv-h5-title-row">
|
||||||
<table-column label="结束时间">{{row.endTime}}</table-column>
|
<div class="qsv-h5-title">{{row.title}}</div>
|
||||||
</template>
|
<div class="qsv-h5-answered" v-if="row.answeredText || row.isAnswered">{{row.answeredText || getAnsweredText(row.category)}}</div>
|
||||||
<template #actions="{index,row}">
|
</div>
|
||||||
<div class="action-btn" @click="onEnter(row)">
|
<div class="qsv-h5-meta-row">
|
||||||
<i class="fa fa-eye"></i>
|
<div class="qsv-h5-type">
|
||||||
<span>查看</span>
|
<i :class="getCategoryIcon(row.category)"></i>
|
||||||
|
<span>{{getCategoryText(row.category)}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="qsv-h5-time">
|
||||||
|
<div>开始时间:{{formatDateTime(row.startTime)}}</div>
|
||||||
|
<div>结束时间:{{formatDateTime(row.endTime)}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<van-empty v-if="!loading && list.length === 0" description="暂无内容"></van-empty>
|
||||||
</table-list>
|
</van-list>
|
||||||
|
</van-pull-refresh>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="${cspNonce!}">
|
<script nonce="${cspNonce!}">
|
||||||
@@ -35,12 +76,26 @@ layout("/layouts/platform_h5.html"){
|
|||||||
store,
|
store,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
loading: false,
|
||||||
|
requesting: false,
|
||||||
|
finished: false,
|
||||||
|
refreshing: false,
|
||||||
|
cacheRestored: false,
|
||||||
|
list: [],
|
||||||
|
defaultCover: "/assets/mobile/svg/qsv/icon.svg",
|
||||||
pageForm: {
|
pageForm: {
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
category: "QUIZ"
|
title: "",
|
||||||
|
category: ""
|
||||||
},
|
},
|
||||||
|
categoryOptions: [
|
||||||
|
{label: "全部", value: ""},
|
||||||
|
{label: "答题", value: "QUIZ"},
|
||||||
|
{label: "问卷", value: "SURVEY"},
|
||||||
|
{label: "投票", value: "VOTE"}
|
||||||
|
],
|
||||||
categoryPaths: {
|
categoryPaths: {
|
||||||
QUIZ: "/platform/h5/qsv/quiz",
|
QUIZ: "/platform/h5/qsv/quiz",
|
||||||
SURVEY: "/platform/h5/qsv/survey",
|
SURVEY: "/platform/h5/qsv/survey",
|
||||||
@@ -50,30 +105,114 @@ layout("/layouts/platform_h5.html"){
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
historyBack() {
|
historyBack() {
|
||||||
this.$pjaxReplace('/platform/h5/home')
|
this.$pjaxReplace('/platform/h5/home')
|
||||||
},
|
},
|
||||||
|
|
||||||
doSearch() {
|
changeCategory(category) {
|
||||||
this.$nextTick(() => {
|
if (this.pageForm.category === category) {
|
||||||
this.pageForm.pageNumber = 1
|
return
|
||||||
this.pageForm.totalCount = 0
|
}
|
||||||
this.$refs.tableListRef.doSearch()
|
this.pageForm.category = category
|
||||||
|
this.onRefresh()
|
||||||
|
},
|
||||||
|
|
||||||
|
onRefresh() {
|
||||||
|
this.pageForm.pageNumber = 1
|
||||||
|
this.pageForm.totalCount = 0
|
||||||
|
this.list = []
|
||||||
|
this.finished = false
|
||||||
|
this.clearListCache()
|
||||||
|
this.onLoad()
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
if (this.requesting || this.finished) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.requesting = true
|
||||||
|
this.loading = true
|
||||||
|
this.$axios.post("/platform/h5/qsv/pageData", this.pageForm).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
const pageData = res.data || {}
|
||||||
|
const rows = pageData.list || []
|
||||||
|
this.list = this.list.concat(rows)
|
||||||
|
this.pageForm.totalCount = pageData.totalCount || 0
|
||||||
|
if (this.list.length >= this.pageForm.totalCount || rows.length === 0) {
|
||||||
|
this.finished = true
|
||||||
|
}
|
||||||
|
this.pageForm.pageNumber++
|
||||||
|
this.saveListCache()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.requesting = false
|
||||||
|
this.loading = false
|
||||||
|
this.refreshing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async onEnter(item) {
|
refreshListSilently() {
|
||||||
if (item.groupId) {
|
if (this.requesting) {
|
||||||
const {
|
return
|
||||||
code,
|
|
||||||
data
|
|
||||||
} = await $.get('/platform/activity/basic/scope/getScopeUser', {activityGroupId: item.groupId})
|
|
||||||
if (code === 0 && data === 0) {
|
|
||||||
this.$toast.fail("您没有权限参与")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const requestForm = Object.assign({}, this.pageForm, {
|
||||||
|
pageNumber: 1
|
||||||
|
})
|
||||||
|
this.requesting = true
|
||||||
|
this.$axios.post("/platform/h5/qsv/pageData", requestForm).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
const pageData = res.data || {}
|
||||||
|
const rows = pageData.list || []
|
||||||
|
this.list = rows
|
||||||
|
this.pageForm = Object.assign({}, this.pageForm, {
|
||||||
|
pageNumber: 2,
|
||||||
|
totalCount: pageData.totalCount || 0
|
||||||
|
})
|
||||||
|
this.finished = rows.length >= this.pageForm.totalCount || rows.length === 0
|
||||||
|
this.saveListCache()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.requesting = false
|
||||||
|
this.loading = false
|
||||||
|
this.refreshing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
const {startTime, endTime} = item
|
getCategoryText(category) {
|
||||||
|
const map = {
|
||||||
|
QUIZ: "答题",
|
||||||
|
SURVEY: "问卷",
|
||||||
|
VOTE: "投票"
|
||||||
|
}
|
||||||
|
return map[category] || "活动"
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategoryIcon(category) {
|
||||||
|
const map = {
|
||||||
|
QUIZ: "fa fa-check-square-o",
|
||||||
|
SURVEY: "fa fa-list-alt",
|
||||||
|
VOTE: "fa fa-bar-chart"
|
||||||
|
}
|
||||||
|
return map[category] || "fa fa-tags"
|
||||||
|
},
|
||||||
|
|
||||||
|
getAnsweredText(category) {
|
||||||
|
const map = {
|
||||||
|
QUIZ: "已答题",
|
||||||
|
SURVEY: "已填写",
|
||||||
|
VOTE: "已投票"
|
||||||
|
}
|
||||||
|
return map[category] || "已完成"
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return this.$moment(value).format("YYYY-MM-DD HH:mm")
|
||||||
|
},
|
||||||
|
|
||||||
|
onEnter(item) {
|
||||||
|
const {startTime} = item
|
||||||
if (this.$moment(startTime).unix() > this.$moment().unix()) {
|
if (this.$moment(startTime).unix() > this.$moment().unix()) {
|
||||||
this.$toast("未开始")
|
this.$toast("未开始")
|
||||||
return
|
return
|
||||||
@@ -81,13 +220,315 @@ layout("/layouts/platform_h5.html"){
|
|||||||
|
|
||||||
const path = this.categoryPaths[item.category]
|
const path = this.categoryPaths[item.category]
|
||||||
if (path) {
|
if (path) {
|
||||||
|
this.saveListCache()
|
||||||
pjaxReplace(path + "?id=" + item.id)
|
pjaxReplace(path + "?id=" + item.id)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCacheKey() {
|
||||||
|
return "qsv:h5:list"
|
||||||
|
},
|
||||||
|
|
||||||
|
saveListCache() {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(this.getCacheKey(), JSON.stringify({
|
||||||
|
time: Date.now(),
|
||||||
|
list: this.list,
|
||||||
|
pageForm: this.pageForm,
|
||||||
|
finished: this.finished
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
restoreListCache() {
|
||||||
|
try {
|
||||||
|
const cacheText = sessionStorage.getItem(this.getCacheKey())
|
||||||
|
if (!cacheText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cache = JSON.parse(cacheText)
|
||||||
|
if (!cache || Date.now() - cache.time > 5 * 60 * 1000) {
|
||||||
|
this.clearListCache()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.list = cache.list || []
|
||||||
|
this.pageForm = Object.assign({}, this.pageForm, cache.pageForm || {})
|
||||||
|
this.finished = !!cache.finished
|
||||||
|
this.cacheRestored = this.list.length > 0
|
||||||
|
} catch (e) {
|
||||||
|
this.clearListCache()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearListCache() {
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem(this.getCacheKey())
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.restoreListCache()
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.cacheRestored) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.refreshListSilently()
|
||||||
|
}, 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.qsv-h5-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f4f8ff;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-header {
|
||||||
|
padding: 10px 14px 16px;
|
||||||
|
background: linear-gradient(180deg, #1688f8 0%, #0f83ee 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-nav {
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-back-btn,
|
||||||
|
.qsv-h5-nav-placeholder {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-back-btn {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, .16);
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-nav-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-header .van-search {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-header .van-search__content {
|
||||||
|
background: rgba(255, 255, 255, .92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-search-btn {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, .45);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, .18);
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-filter-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-filter-row::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-filter-btn {
|
||||||
|
height: 32px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 17px;
|
||||||
|
background: rgba(255, 255, 255, .92);
|
||||||
|
color: #1688f8;
|
||||||
|
font-size: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-filter-btn.active {
|
||||||
|
background: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-filter-btn i {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-list-wrap {
|
||||||
|
min-height: calc(100vh - 170px);
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 13px;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 102, 168, .1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-cover {
|
||||||
|
width: 116px;
|
||||||
|
height: 87px;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #eaf2ff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #1f2f46;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-answered {
|
||||||
|
height: 22px;
|
||||||
|
min-width: 50px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid #19a05f;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: #f0fff8;
|
||||||
|
color: #149356;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-type {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 1px solid #ff9f43;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff7ec;
|
||||||
|
color: #f28b18;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-type i {
|
||||||
|
color: #f28b18;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-time {
|
||||||
|
color: #71829b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.qsv-h5-card {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-cover {
|
||||||
|
width: 96px;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qsv-h5-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!--#
|
<!--#
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ layout("/layouts/platform_h5.html"){
|
|||||||
#-->
|
#-->
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
|
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="pageTitle" placeholder fixed></van-nav-bar>
|
||||||
|
|
||||||
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
|
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
|
||||||
<van-sticky offset-top="46px">
|
<div class="quiz-hero" :style="heroStyle">
|
||||||
<!--开启计时 未完成 活动未结束-->
|
<div class="quiz-hero-mask">
|
||||||
<div class="timer" v-if="activity.timeLimit > 0 && !answerRecord.isFinish && !isEnd">⏰{{ remainingTime }}s
|
<div class="timer" v-if="activity.timeLimit > 0 && !answerRecord.isFinish && !isEnd">
|
||||||
|
⏰ {{ remainingTime }}s
|
||||||
|
</div>
|
||||||
|
<h2 class="quiz-title">{{ activity.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="title">{{ activity.title }}</h2>
|
</div>
|
||||||
</van-sticky>
|
|
||||||
|
|
||||||
<div v-if="!answerRecord.isFinish">
|
<div class="quiz-card-scroll" v-if="!answerRecord.isFinish">
|
||||||
<div v-for="(subject, index) in subjects" :key="index" class="subject">
|
<div v-for="(subject, index) in subjects" :key="index" class="subject">
|
||||||
<p>{{index+1}}、{{ subject.title }}</p>
|
<p>{{index+1}}、{{ subject.title }}</p>
|
||||||
<div class="tag">
|
<div class="tag">
|
||||||
@@ -84,9 +86,28 @@ layout("/layouts/platform_h5.html"){
|
|||||||
return this.activity.timeLimit * 60 - this.answerTime
|
return this.activity.timeLimit * 60 - this.answerTime
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
},
|
||||||
|
pageTitle() {
|
||||||
|
return this.getCategoryPageTitle(this.activity && this.activity.category, "在线答题")
|
||||||
|
},
|
||||||
|
heroStyle() {
|
||||||
|
if (this.activity && this.activity.cover) {
|
||||||
|
return {
|
||||||
|
backgroundImage: "url(" + this.activity.cover + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getCategoryPageTitle(category, defaultTitle) {
|
||||||
|
const titleMap = {
|
||||||
|
QUIZ: "在线答题",
|
||||||
|
SURVEY: "问卷调查",
|
||||||
|
VOTE: "投票"
|
||||||
|
}
|
||||||
|
return titleMap[category] || defaultTitle
|
||||||
|
},
|
||||||
//获取活动、题目
|
//获取活动、题目
|
||||||
listSubjects() {
|
listSubjects() {
|
||||||
this.$axios.post("/platform/h5/qsv/quiz/subjects", {activityId: this.id}).then((res) => {
|
this.$axios.post("/platform/h5/qsv/quiz/subjects", {activityId: this.id}).then((res) => {
|
||||||
@@ -245,14 +266,6 @@ layout("/layouts/platform_h5.html"){
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOptionIds = subject.userSelectOptionIds || []
|
|
||||||
const hasSelected = selectedOptionIds.includes(optionId)
|
|
||||||
// 多选题达到最大可选数量后禁止继续勾选,但允许取消已选项。
|
|
||||||
if (subject.type === "checkbox" && !hasSelected && subject.maxMulti && subject.maxMulti > 0 && selectedOptionIds.length >= subject.maxMulti) {
|
|
||||||
this.$toast("本题最多选择" + subject.maxMulti + "项")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subject.type === "radio") {
|
if (subject.type === "radio") {
|
||||||
this.$refs["subject" + subject.id][0].toggleAll(false)
|
this.$refs["subject" + subject.id][0].toggleAll(false)
|
||||||
}
|
}
|
||||||
@@ -341,20 +354,53 @@ layout("/layouts/platform_h5.html"){
|
|||||||
style: /*language=CSS*/ `
|
style: /*language=CSS*/ `
|
||||||
.container {
|
.container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
background: #f4f8ff;
|
||||||
|
height: calc(100vh - 46px);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .container .title {
|
/deep/ .quiz-hero {
|
||||||
text-align: center;
|
flex: 0 0 auto;
|
||||||
padding: 20px;
|
min-height: 170px;
|
||||||
background: #ffffff;
|
background: linear-gradient(135deg, #1688f8, #0a55c5);
|
||||||
margin: 0 0 10px 0 !important;
|
background-size: cover;
|
||||||
color: var(--color-primary);
|
background-position: center;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 12px 12px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .card {
|
/deep/ .quiz-hero-mask {
|
||||||
padding: 10px;
|
min-height: 170px;
|
||||||
background: #ffffff;
|
padding: 24px 20px 22px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, .16), rgba(0, 0, 0, .56));
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .quiz-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-card-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject {
|
.subject {
|
||||||
@@ -377,10 +423,15 @@ layout("/layouts/platform_h5.html"){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .timer {
|
/deep/ .timer {
|
||||||
text-align: center;
|
align-self: flex-start;
|
||||||
font-size: 18px;
|
margin-bottom: 12px;
|
||||||
background: #fff;
|
padding: 5px 10px;
|
||||||
padding: 10px 0;
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, .22);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button {
|
.submit-button {
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
|
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="问卷列表" placeholder fixed></van-nav-bar>
|
||||||
|
|
||||||
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
|
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
|
||||||
<van-sticky offset-top="46px">
|
<div class="quiz-hero" :style="heroStyle">
|
||||||
<h2 class="title">{{ activity.title }}</h2>
|
<div class="quiz-hero-mask">
|
||||||
<div class="score-form">
|
<h2 class="quiz-title">{{ activity.title }}</h2>
|
||||||
<div class="score-form-total">
|
<div class="quiz-score">
|
||||||
<div class="score-font-style">{{totalScore}}</div>
|
<span>{{totalScore}}</span>
|
||||||
<i class="score-underline"></i>
|
<em>分</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-sticky>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div v-for="(subject, index) in subjects" :key="index" class="subject">
|
<div v-for="(subject, index) in subjects" :key="index" class="subject">
|
||||||
@@ -111,6 +111,14 @@ layout("/layouts/platform_h5.html"){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return totalScore
|
return totalScore
|
||||||
|
},
|
||||||
|
heroStyle() {
|
||||||
|
if (this.activity && this.activity.cover) {
|
||||||
|
return {
|
||||||
|
backgroundImage: "url(" + this.activity.cover + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
@@ -199,23 +207,60 @@ layout("/layouts/platform_h5.html"){
|
|||||||
style: /*language=CSS*/ `
|
style: /*language=CSS*/ `
|
||||||
.container {
|
.container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
background: #f4f8ff;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .container .title {
|
/deep/ .quiz-hero {
|
||||||
text-align: center;
|
min-height: 190px;
|
||||||
padding: 20px;
|
background: linear-gradient(135deg, #1688f8, #0a55c5);
|
||||||
margin: 0 !important;
|
background-size: cover;
|
||||||
background: #ffffff;
|
background-position: center;
|
||||||
color: var(--color-primary);
|
position: relative;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
overflow: hidden;
|
||||||
|
margin: 12px 12px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .score {
|
/deep/ .quiz-hero-mask {
|
||||||
|
min-height: 190px;
|
||||||
|
padding: 28px 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, .18), rgba(0, 0, 0, .58));
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .card {
|
/deep/ .quiz-title {
|
||||||
padding: 10px;
|
margin: 0 0 14px;
|
||||||
background: #ffffff;
|
color: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .quiz-score {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .quiz-score span {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .quiz-score em {
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject {
|
.subject {
|
||||||
@@ -237,46 +282,6 @@ layout("/layouts/platform_h5.html"){
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .score-form {
|
|
||||||
background: #ffffff;
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-top: 1px solid #f1f1f1;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .score-form-total {
|
|
||||||
flex: 1;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
padding: 10px 20px 10px 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .score-text-news {
|
|
||||||
flex: 1;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .score-font-style {
|
|
||||||
font-size: 32px;
|
|
||||||
color: #ff6a00;
|
|
||||||
word-break: keep-all;
|
|
||||||
line-height: 38px;
|
|
||||||
min-width: 47px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/deep/ .score-underline {
|
|
||||||
background: url(//image.wjx.cn/images/newimg/score-form/score-underline@2x.png) no-repeat center;
|
|
||||||
background-size: 47px 16px;
|
|
||||||
display: inline-block;
|
|
||||||
height: 16px;
|
|
||||||
width: 47px;
|
|
||||||
margin-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.van-checkbox__icon--checked .van-icon {
|
.van-checkbox__icon--checked .van-icon {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
background-color: var(--color-primary) !important;
|
background-color: var(--color-primary) !important;
|
||||||
|
|||||||
@@ -5,18 +5,53 @@ layout("/layouts/platform_h5.html"){
|
|||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
background: #f4f8ff;
|
||||||
|
height: calc(100vh - 46px);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .title {
|
.quiz-hero {
|
||||||
text-align: center;
|
flex: 0 0 auto;
|
||||||
padding: 20px;
|
min-height: 170px;
|
||||||
background: #ffffff;
|
background: linear-gradient(135deg, #1688f8, #0a55c5);
|
||||||
margin: 0 0 10px 0;
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 12px 12px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 102, 168, .14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .card {
|
.quiz-hero-mask {
|
||||||
padding: 10px;
|
min-height: 170px;
|
||||||
background: #ffffff;
|
padding: 24px 20px 22px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, .16), rgba(0, 0, 0, .56));
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-shadow: 0 2px 8px rgba(0, 0, 0, .28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-card-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject {
|
.subject {
|
||||||
@@ -49,16 +84,30 @@ layout("/layouts/platform_h5.html"){
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.van-checkbox__icon--checked .van-icon {
|
.van-checkbox__icon--checked .van-icon,
|
||||||
|
.van-radio__icon--checked .van-icon {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
background-color: var(--color-primary) !important;
|
background-color: var(--color-primary) !important;
|
||||||
border-color: var(--color-primary) !important;
|
border-color: var(--color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.van-checkbox__icon--disabled .van-icon {
|
.van-checkbox__icon--disabled .van-icon,
|
||||||
|
.van-radio__icon--disabled .van-icon {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-option-finished-selected .survey-option-text {
|
||||||
|
color: #00a000;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-option-finished-selected .van-radio__icon--disabled .van-icon,
|
||||||
|
.survey-option-finished-selected .van-checkbox__icon--disabled .van-icon {
|
||||||
|
color: #fff !important;
|
||||||
|
background-color: #d8d8d8 !important;
|
||||||
|
border-color: #d8d8d8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-input-box {
|
.ui-input-box {
|
||||||
border: 1px solid #e3e3e3;
|
border: 1px solid #e3e3e3;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
@@ -80,57 +129,145 @@ layout("/layouts/platform_h5.html"){
|
|||||||
resize: none;
|
resize: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.option-fill-box {
|
||||||
|
border: 1px solid #e3e3e3;
|
||||||
|
margin: 0 16px 8px 46px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-fill-box input {
|
||||||
|
background-color: #fff;
|
||||||
|
border: none !important;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
resize: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-star {
|
||||||
|
color: #ee0a24;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak>
|
||||||
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
|
<van-nav-bar :title="pageTitle" left-arrow left-text="返回" @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||||
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
|
<div class="container" :style="{'padding-bottom': '84px'}">
|
||||||
<h2 class="title">{{ activity.title }}</h2>
|
<div class="quiz-hero" :style="heroStyle">
|
||||||
<div v-if="!isFinished">
|
<div class="quiz-hero-mask">
|
||||||
<div v-for="(subject, index) in subjects" :key="index" class="subject">
|
<h2 class="quiz-title">{{ activity.title }}</h2>
|
||||||
<p>{{index+1}}、{{ subject.title }}</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="quiz-card-scroll" v-if="!isFinished">
|
||||||
|
<div v-for="(subject, index) in visibleSubjects" :key="subject.id" class="subject">
|
||||||
|
<p>{{index+1}}、{{ subject.title }}<span class="required-star">*</span></p>
|
||||||
<div class="tag">
|
<div class="tag">
|
||||||
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
|
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
|
||||||
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
|
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
|
||||||
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
|
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--单选、多选-->
|
<!--单选-->
|
||||||
<van-checkbox-group
|
<van-radio-group
|
||||||
v-model="subject.userSelectOptionIds"
|
:value="getRadioValue(subject)"
|
||||||
:ref="'subject'+subject.id"
|
v-if="subject.type === 'radio'"
|
||||||
v-if="['radio','checkbox'].includes(subject.type)"
|
|
||||||
>
|
>
|
||||||
<van-cell-group>
|
<van-cell-group>
|
||||||
<van-cell
|
<template v-for="(option,index) in subject.options">
|
||||||
v-for="(option,index) in subject.options"
|
<van-cell
|
||||||
clickable
|
clickable
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
:title="option.text"
|
:title="option.text"
|
||||||
@click="cellToggle(subject,option.id)"
|
:class="getFinishedOptionClass(subject, option)"
|
||||||
>
|
@click="cellToggle(subject,option.id)"
|
||||||
<template #icon>
|
>
|
||||||
<van-checkbox :name="option.id" :ref="'option'+option.id" style="margin-right: 10px"></van-checkbox>
|
<template #title>
|
||||||
</template>
|
<span class="survey-option-text">{{option.text}}</span>
|
||||||
<img
|
</template>
|
||||||
slot="right-icon"
|
<template #icon>
|
||||||
v-if="option.imgUrl"
|
<van-radio :name="option.id" :disabled="answerRecord.isFinish" style="margin-right: 10px"></van-radio>
|
||||||
:src="option.imgUrl"
|
</template>
|
||||||
alt=""
|
<img
|
||||||
style="width: 40px; height: 40px"
|
slot="right-icon"
|
||||||
@click.stop="previewOptionImg(option.imgUrl)"
|
v-if="option.imgUrl"
|
||||||
/>
|
:src="option.imgUrl"
|
||||||
</van-cell>
|
alt=""
|
||||||
|
style="width: 40px; height: 40px"
|
||||||
|
@click.stop="previewOptionImg(option.imgUrl)"
|
||||||
|
/>
|
||||||
|
</van-cell>
|
||||||
|
<div
|
||||||
|
class="option-fill-box"
|
||||||
|
v-if="isOptionFillVisible(subject, option)"
|
||||||
|
:key="option.id + '-fill'">
|
||||||
|
<input
|
||||||
|
v-model="subject.optionFillContents[option.id]"
|
||||||
|
:placeholder="option.fillPlaceholder || '请输入补充内容'">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</van-cell-group>
|
||||||
|
</van-radio-group>
|
||||||
|
|
||||||
|
<!--多选-->
|
||||||
|
<van-checkbox-group
|
||||||
|
v-model="subject.userSelectOptionIds"
|
||||||
|
v-if="subject.type === 'checkbox'"
|
||||||
|
>
|
||||||
|
<van-cell-group>
|
||||||
|
<template v-for="(option,index) in subject.options">
|
||||||
|
<van-cell
|
||||||
|
clickable
|
||||||
|
:key="option.id"
|
||||||
|
:title="option.text"
|
||||||
|
:class="getFinishedOptionClass(subject, option)"
|
||||||
|
@click="cellToggle(subject,option.id)"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span class="survey-option-text">{{option.text}}</span>
|
||||||
|
</template>
|
||||||
|
<template #icon>
|
||||||
|
<van-checkbox :name="option.id" :ref="'option'+option.id" :disabled="answerRecord.isFinish" style="margin-right: 10px"></van-checkbox>
|
||||||
|
</template>
|
||||||
|
<img
|
||||||
|
slot="right-icon"
|
||||||
|
v-if="option.imgUrl"
|
||||||
|
:src="option.imgUrl"
|
||||||
|
alt=""
|
||||||
|
style="width: 40px; height: 40px"
|
||||||
|
@click.stop="previewOptionImg(option.imgUrl)"
|
||||||
|
/>
|
||||||
|
</van-cell>
|
||||||
|
<div
|
||||||
|
class="option-fill-box"
|
||||||
|
:key="option.id + '-fill'"
|
||||||
|
v-if="isOptionFillVisible(subject, option)">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="subject.optionFillContents[option.id]"
|
||||||
|
:placeholder="option.fillPlaceholder || '请填写补充内容'"
|
||||||
|
:readonly="answerRecord.isFinish"
|
||||||
|
@click.stop />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
|
|
||||||
<!--填空题-->
|
<!--填空题-->
|
||||||
<div class="ui-input-box" v-if="subject.type==='text'">
|
<div class="ui-input-box" v-if="subject.type==='text'">
|
||||||
<input type="text" v-model="subject.userFillContent" :readonly="answerRecord.isFinish" />
|
<input type="text" v-model="subject.userFillContent" :placeholder="'请输入' + subject.title" :readonly="answerRecord.isFinish" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-control" v-if="!answerRecord.isFinish">
|
<div class="button-control">
|
||||||
<van-button type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
|
<van-button v-if="!answerRecord.isFinish" type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
|
||||||
|
<van-button v-else type="primary" @click="historyBack" block>返回</van-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,10 +295,34 @@ layout("/layouts/platform_h5.html"){
|
|||||||
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
|
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
},
|
||||||
|
visibleSubjects() {
|
||||||
|
return this.subjects.filter((subject) => {
|
||||||
|
return this.isSubjectVisible(subject)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pageTitle() {
|
||||||
|
return this.getCategoryPageTitle(this.activity && this.activity.category, "问卷调查")
|
||||||
|
},
|
||||||
|
heroStyle() {
|
||||||
|
if (this.activity && this.activity.cover) {
|
||||||
|
return {
|
||||||
|
backgroundImage: "url(" + this.activity.cover + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
getCategoryPageTitle(category, defaultTitle) {
|
||||||
|
const titleMap = {
|
||||||
|
QUIZ: "在线答题",
|
||||||
|
SURVEY: "问卷调查",
|
||||||
|
VOTE: "投票"
|
||||||
|
}
|
||||||
|
return titleMap[category] || defaultTitle
|
||||||
|
},
|
||||||
//获取活动、题目
|
//获取活动、题目
|
||||||
listSubjects() {
|
listSubjects() {
|
||||||
this.$axios.post("/platform/h5/qsv/survey/subjects", { activityId: this.id }).then((res) => {
|
this.$axios.post("/platform/h5/qsv/survey/subjects", { activityId: this.id }).then((res) => {
|
||||||
@@ -193,14 +354,214 @@ layout("/layouts/platform_h5.html"){
|
|||||||
const answer = this.answerRecord.extJson[subject.id]
|
const answer = this.answerRecord.extJson[subject.id]
|
||||||
|
|
||||||
if (["radio", "checkbox"].includes(subject.type)) {
|
if (["radio", "checkbox"].includes(subject.type)) {
|
||||||
this.$refs["subject" + subject.id][0].toggleAll(false)
|
const optionIds = answer?.optionIds || []
|
||||||
answer?.optionIds.forEach((optionId) => {
|
this.$set(subject, "userSelectOptionIds", subject.type === "radio" ? optionIds.slice(0, 1) : optionIds)
|
||||||
this.$nextTick(() => {
|
this.$set(subject, "optionFillContents", answer?.optionFillContents || {})
|
||||||
this.$refs["option" + optionId][0].toggle()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else if (subject.type === "text") {
|
} else if (subject.type === "text") {
|
||||||
subject.userFillContent = answer?.text
|
this.$set(subject, "userFillContent", answer?.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!this.answerRecord.isFinish) {
|
||||||
|
this.clearHiddenSubjectAnswers()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//判断题目是否显示,未配置隐显逻辑的题目默认显示;已配置的题目按条件树递归计算后再执行显示或隐藏动作
|
||||||
|
isSubjectVisible(subject) {
|
||||||
|
if (!subject.visibleRuleEnabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootGroup = this.getVisibleRuleRootGroup(subject)
|
||||||
|
if (!rootGroup) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = this.evaluateVisibleRuleGroup(rootGroup)
|
||||||
|
|
||||||
|
if (subject.visibleRuleAction === "hide") {
|
||||||
|
return !matched
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
},
|
||||||
|
|
||||||
|
//读取隐显逻辑根分组,兼容旧版 subjectId/optionId 平铺条件
|
||||||
|
getVisibleRuleRootGroup(subject) {
|
||||||
|
let conditions = subject.visibleRuleConditions || []
|
||||||
|
if (typeof conditions === "string") {
|
||||||
|
try {
|
||||||
|
conditions = JSON.parse(conditions)
|
||||||
|
} catch (e) {
|
||||||
|
conditions = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Array.isArray(conditions) || conditions.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (conditions.length === 1 && conditions[0].type === "group") {
|
||||||
|
return this.normalizeVisibleRuleGroup(conditions[0])
|
||||||
|
}
|
||||||
|
const children = conditions.filter((condition) => {
|
||||||
|
return condition.subjectId && condition.optionId
|
||||||
|
}).map((condition) => {
|
||||||
|
return {
|
||||||
|
type: "item",
|
||||||
|
field: condition.subjectId,
|
||||||
|
operator: "contains",
|
||||||
|
value: condition.optionId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (children.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "group",
|
||||||
|
logic: subject.visibleRuleLogic === "OR" ? "or" : "and",
|
||||||
|
children
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//规整条件分组,避免历史数据或空字段影响递归判断
|
||||||
|
normalizeVisibleRuleGroup(group) {
|
||||||
|
return {
|
||||||
|
type: "group",
|
||||||
|
logic: (group.logic || "and").toLowerCase() === "or" ? "or" : "and",
|
||||||
|
children: Array.isArray(group.children) ? group.children.map((child) => {
|
||||||
|
if (child.type === "group") {
|
||||||
|
return this.normalizeVisibleRuleGroup(child)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "item",
|
||||||
|
field: child.field || child.subjectId || "",
|
||||||
|
operator: child.operator || (child.optionId ? "contains" : "=="),
|
||||||
|
value: child.value !== undefined && child.value !== null ? child.value : (child.optionId || "")
|
||||||
|
}
|
||||||
|
}) : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//递归计算条件分组,支持并且、或者和嵌套分组
|
||||||
|
evaluateVisibleRuleGroup(group) {
|
||||||
|
const children = (group.children || []).filter((child) => {
|
||||||
|
return child.type === "group" || (child.field && child.operator)
|
||||||
|
})
|
||||||
|
if (children.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const results = children.map((child) => {
|
||||||
|
if (child.type === "group") {
|
||||||
|
return this.evaluateVisibleRuleGroup(child)
|
||||||
|
}
|
||||||
|
return this.evaluateVisibleRuleItem(child)
|
||||||
|
})
|
||||||
|
return group.logic === "or"
|
||||||
|
? results.some((item) => {
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
: results.every((item) => {
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
//计算单条条件,选择题按选项ID判断,填空题支持文本包含和数字大小比较
|
||||||
|
evaluateVisibleRuleItem(condition) {
|
||||||
|
const triggerSubject = this.subjects.find((item) => {
|
||||||
|
return item.id === condition.field
|
||||||
|
})
|
||||||
|
if (!triggerSubject) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const answer = ["radio", "checkbox"].includes(triggerSubject.type)
|
||||||
|
? (triggerSubject.userSelectOptionIds || [])
|
||||||
|
: triggerSubject.userFillContent
|
||||||
|
return this.compareVisibleRuleValue(answer, condition.operator, condition.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
//比较条件值,和后台逻辑抽屉实时预览保持同一套语义
|
||||||
|
compareVisibleRuleValue(answer, operator, value) {
|
||||||
|
if (Array.isArray(answer)) {
|
||||||
|
if (operator === "!=") {
|
||||||
|
return !answer.includes(value)
|
||||||
|
}
|
||||||
|
return answer.includes(value)
|
||||||
|
}
|
||||||
|
if ([">", "<", ">=", "<="].includes(operator)) {
|
||||||
|
const answerNumber = Number(answer)
|
||||||
|
const valueNumber = Number(value)
|
||||||
|
if (isNaN(answerNumber) || isNaN(valueNumber)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (operator === ">") {
|
||||||
|
return answerNumber > valueNumber
|
||||||
|
}
|
||||||
|
if (operator === "<") {
|
||||||
|
return answerNumber < valueNumber
|
||||||
|
}
|
||||||
|
if (operator === ">=") {
|
||||||
|
return answerNumber >= valueNumber
|
||||||
|
}
|
||||||
|
return answerNumber <= valueNumber
|
||||||
|
}
|
||||||
|
const answerText = answer === undefined || answer === null ? "" : String(answer)
|
||||||
|
const valueText = value === undefined || value === null ? "" : String(value)
|
||||||
|
if (operator === "!=") {
|
||||||
|
return answerText !== valueText
|
||||||
|
}
|
||||||
|
if (operator === "contains") {
|
||||||
|
return answerText.indexOf(valueText) !== -1
|
||||||
|
}
|
||||||
|
return answerText === valueText
|
||||||
|
},
|
||||||
|
|
||||||
|
//清空当前隐藏题目的答案,避免隐藏题残留旧选项或填空内容参与后续提交
|
||||||
|
clearHiddenSubjectAnswers() {
|
||||||
|
this.subjects.forEach((subject) => {
|
||||||
|
if (this.isSubjectVisible(subject)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (["radio", "checkbox"].includes(subject.type)) {
|
||||||
|
this.$set(subject, "userSelectOptionIds", [])
|
||||||
|
this.$set(subject, "optionFillContents", {})
|
||||||
|
} else if (subject.type === "text") {
|
||||||
|
this.$set(subject, "userFillContent", null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
//判断选项是否已被选择,用于控制选项补充填写框显示和提交校验
|
||||||
|
isOptionSelected(subject, optionId) {
|
||||||
|
return subject.userSelectOptionIds && subject.userSelectOptionIds.includes(optionId)
|
||||||
|
},
|
||||||
|
|
||||||
|
getFinishedOptionClass(subject, option) {
|
||||||
|
return this.answerRecord.isFinish && this.isOptionSelected(subject, option.id)
|
||||||
|
? "survey-option-finished-selected"
|
||||||
|
: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
//单选题仍按数组保存,这里只取第一个值用于 radio 组件展示
|
||||||
|
getRadioValue(subject) {
|
||||||
|
return subject.userSelectOptionIds && subject.userSelectOptionIds.length > 0
|
||||||
|
? subject.userSelectOptionIds[0]
|
||||||
|
: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
//选中需要补充填写的选项后显示输入框,例如选择“否”后填写原因
|
||||||
|
isOptionFillVisible(subject, option) {
|
||||||
|
return option.fillRequired && this.isOptionSelected(subject, option.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
//清空未选中选项的补充填写内容,避免取消选择后残留旧内容
|
||||||
|
clearUnselectedOptionFillContents(subject) {
|
||||||
|
if (!subject.optionFillContents) {
|
||||||
|
this.$set(subject, "optionFillContents", {})
|
||||||
|
}
|
||||||
|
if (!subject.options) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subject.options.forEach((option) => {
|
||||||
|
if (!this.isOptionSelected(subject, option.id)) {
|
||||||
|
this.$delete(subject.optionFillContents, option.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -212,9 +573,14 @@ layout("/layouts/platform_h5.html"){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subject.type === "radio") {
|
if (subject.type === "radio") {
|
||||||
this.$refs["subject" + subject.id][0].toggleAll(false)
|
this.$set(subject, "userSelectOptionIds", [optionId])
|
||||||
|
} else {
|
||||||
|
this.$refs["option" + optionId][0].toggle()
|
||||||
}
|
}
|
||||||
this.$refs["option" + optionId][0].toggle()
|
this.$nextTick(() => {
|
||||||
|
this.clearUnselectedOptionFillContents(subject)
|
||||||
|
this.clearHiddenSubjectAnswers()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
//预览图片
|
//预览图片
|
||||||
@@ -231,13 +597,20 @@ layout("/layouts/platform_h5.html"){
|
|||||||
}
|
}
|
||||||
|
|
||||||
//提示那些题没有作答
|
//提示那些题没有作答
|
||||||
for (let i = 0; i < this.subjects.length; i++) {
|
for (let i = 0; i < this.visibleSubjects.length; i++) {
|
||||||
const subject = this.subjects[i]
|
const subject = this.visibleSubjects[i]
|
||||||
if (["radio", "checkbox"].includes(subject.type)) {
|
if (["radio", "checkbox"].includes(subject.type)) {
|
||||||
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
|
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
|
||||||
this.$toast.fail("第" + (i + 1) + "题未做答")
|
this.$toast.fail("第" + (i + 1) + "题未做答")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
for (let j = 0; j < subject.options.length; j++) {
|
||||||
|
const option = subject.options[j]
|
||||||
|
if (this.isOptionFillVisible(subject, option) && !subject.optionFillContents[option.id]) {
|
||||||
|
this.$toast.fail("请填写第" + (i + 1) + "题的补充内容")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if ("text" === subject.type) {
|
} else if ("text" === subject.type) {
|
||||||
if (!subject.userFillContent) {
|
if (!subject.userFillContent) {
|
||||||
this.$toast.fail("第" + (i + 1) + "题未作答")
|
this.$toast.fail("第" + (i + 1) + "题未作答")
|
||||||
@@ -255,11 +628,12 @@ layout("/layouts/platform_h5.html"){
|
|||||||
answer: JSON.stringify({
|
answer: JSON.stringify({
|
||||||
activityId: this.id,
|
activityId: this.id,
|
||||||
answerRecordId: this.answerRecordId,
|
answerRecordId: this.answerRecordId,
|
||||||
subjects: this.subjects.map((subject) => {
|
subjects: this.visibleSubjects.map((subject) => {
|
||||||
return {
|
return {
|
||||||
id: subject.id,
|
id: subject.id,
|
||||||
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : [],
|
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : [],
|
||||||
userFillContent: subject.type === "text" ? subject.userFillContent : null
|
userFillContent: subject.type === "text" ? subject.userFillContent : null,
|
||||||
|
optionFillContents: ["checkbox", "radio"].includes(subject.type) ? subject.optionFillContents : {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,872 @@
|
|||||||
|
<!--#
|
||||||
|
layout("/layouts/platform_h5.html"){
|
||||||
|
#-->
|
||||||
|
|
||||||
|
<div id="app" v-cloak class="vote-page">
|
||||||
|
<van-nav-bar :title="pageTitle" left-arrow left-text="返回" @click-left="historyBack" placeholder fixed></van-nav-bar>
|
||||||
|
|
||||||
|
<div class="vote-header">
|
||||||
|
<h1>{{activity.title}}</h1>
|
||||||
|
<div class="vote-stat-box">
|
||||||
|
<div class="vote-stat-item">
|
||||||
|
<strong>{{voteStats.optionCount || optionList.length}}</strong>
|
||||||
|
<span>参与选项</span>
|
||||||
|
</div>
|
||||||
|
<div class="vote-stat-item">
|
||||||
|
<strong>{{voteStats.totalParticipants || 0}}</strong>
|
||||||
|
<span>投票总人数</span>
|
||||||
|
</div>
|
||||||
|
<button class="vote-rank-btn" @click="rankShow = true">
|
||||||
|
<i class="fa fa-bar-chart"></i>
|
||||||
|
<span>排行榜</span>
|
||||||
|
<van-icon name="arrow"></van-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vote-countdown" v-if="!isEnd">
|
||||||
|
距离投票结束还有:
|
||||||
|
<b>{{countdown.days}}</b> 天
|
||||||
|
<b>{{countdown.hours}}</b> 时
|
||||||
|
<b>{{countdown.minutes}}</b> 分
|
||||||
|
<b>{{countdown.seconds}}</b> 秒
|
||||||
|
</div>
|
||||||
|
<div class="vote-countdown" v-else>投票已结束</div>
|
||||||
|
|
||||||
|
<div class="vote-search">
|
||||||
|
<van-search
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="请输入选项编号/名称"
|
||||||
|
clearable
|
||||||
|
shape="square"
|
||||||
|
background="transparent">
|
||||||
|
</van-search>
|
||||||
|
<button @click="noop">搜索</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vote-subject" v-if="currentSubject">
|
||||||
|
<div class="vote-subject-title">
|
||||||
|
<span>*</span>
|
||||||
|
{{currentSubject.title}}
|
||||||
|
<em v-if="currentSubject.type === 'checkbox' && currentSubject.maxMulti">
|
||||||
|
【请选择1-{{currentSubject.maxMulti}}项,已选择{{selectedOptionIds.length}}项】
|
||||||
|
</em>
|
||||||
|
<em v-else>【请选择{{currentSubject.type === 'radio' ? '1' : '至少1'}}项】</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="getVoteOptionListClass(currentSubject)">
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in filteredOptions"
|
||||||
|
:key="option.id"
|
||||||
|
:class="['vote-option-card', isOptionSelected(option.id) ? 'active' : '']"
|
||||||
|
@click="toggleOption(option)">
|
||||||
|
<div class="vote-option-img" v-if="option.imgUrl">
|
||||||
|
<img :src="option.imgUrl" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="vote-option-img vote-option-placeholder" v-else>
|
||||||
|
<span>{{index + 1}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vote-option-info">
|
||||||
|
<input
|
||||||
|
class="vote-option-control"
|
||||||
|
:type="currentSubject.type === 'radio' ? 'radio' : 'checkbox'"
|
||||||
|
:checked="isOptionSelected(option.id)"
|
||||||
|
:disabled="answerRecord.isFinish || isEnd"
|
||||||
|
@click.stop.prevent="toggleOption(option)">
|
||||||
|
<div class="vote-option-text">
|
||||||
|
<strong>{{getOptionVotes(option.id)}}票({{getOptionPercent(option.id)}}%)</strong>
|
||||||
|
<span>{{option.text}}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="vote-option-detail-btn"
|
||||||
|
v-if="hasOptionDetail(option)"
|
||||||
|
@click.stop="openOptionDetail(option)">
|
||||||
|
详情
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<van-empty v-if="!currentSubject" description="暂无投票题目"></van-empty>
|
||||||
|
|
||||||
|
<div class="vote-bottom">
|
||||||
|
<van-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
color="#ff4d00"
|
||||||
|
:disabled="answerRecord.isFinish || isEnd"
|
||||||
|
@click="onSubmit">
|
||||||
|
{{answerRecord.isFinish ? '已投票' : '投票'}}
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<van-action-sheet v-model="rankShow" title="排行榜">
|
||||||
|
<div class="vote-rank-list">
|
||||||
|
<div v-for="(item, index) in voteStats.rankList || []" :key="item.optionId" class="vote-rank-item">
|
||||||
|
<span class="vote-rank-index">{{index + 1}}</span>
|
||||||
|
<div class="vote-rank-name">{{item.text}}</div>
|
||||||
|
<div class="vote-rank-votes">{{item.votes}}票</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-action-sheet>
|
||||||
|
|
||||||
|
<div class="vote-detail-page" v-if="detailVisible && detailOption">
|
||||||
|
<div class="vote-detail-hero">
|
||||||
|
<h2>{{activity.title}}</h2>
|
||||||
|
<h3>》 {{detailOption.text}} 《</h3>
|
||||||
|
<div class="vote-detail-stats">
|
||||||
|
<span><i class="fa fa-th-large"></i> {{getOptionVotes(detailOption.id)}}票</span>
|
||||||
|
<span><i class="fa fa-institution"></i> 第{{getOptionRank(detailOption.id)}}名</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vote-countdown vote-detail-countdown" v-if="!isEnd">
|
||||||
|
距离投票结束还有:
|
||||||
|
<b>{{countdown.days}}</b> 天
|
||||||
|
<b>{{countdown.hours}}</b> 时
|
||||||
|
<b>{{countdown.minutes}}</b> 分
|
||||||
|
<b>{{countdown.seconds}}</b> 秒
|
||||||
|
</div>
|
||||||
|
<div class="vote-countdown vote-detail-countdown" v-else>投票已结束</div>
|
||||||
|
<div class="vote-detail-section">
|
||||||
|
<h4>投票规则</h4>
|
||||||
|
<p><i class="fa fa-play-circle"></i> 开始时间:{{activity.startTime || '-'}}</p>
|
||||||
|
<p><i class="fa fa-stop-circle"></i> 结束时间:{{activity.endTime || '-'}}</p>
|
||||||
|
<a v-if="detailOption.link" :href="detailOption.link" target="_blank">查看规则详情</a>
|
||||||
|
</div>
|
||||||
|
<div class="vote-detail-section">
|
||||||
|
<h4>详情介绍</h4>
|
||||||
|
<div class="vote-detail-content" v-if="detailOption.description" v-html="detailOption.description"></div>
|
||||||
|
<div class="vote-detail-empty" v-else>暂无详情</div>
|
||||||
|
</div>
|
||||||
|
<div class="vote-detail-bottom">
|
||||||
|
<button @click="closeOptionDetail">返回</button>
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
:disabled="answerRecord.isFinish || isEnd"
|
||||||
|
@click="voteFromDetail">
|
||||||
|
{{answerRecord.isFinish ? '已投票' : '投票'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script nonce="${cspNonce!}">
|
||||||
|
const vue = new Vue({
|
||||||
|
el: "#app",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
id: GetQueryString("id"),
|
||||||
|
activity: {},
|
||||||
|
subjects: [],
|
||||||
|
answerRecordId: null,
|
||||||
|
answerRecord: {},
|
||||||
|
voteStats: {},
|
||||||
|
keyword: "",
|
||||||
|
selectedOptionIds: [],
|
||||||
|
countdown: {
|
||||||
|
days: 0,
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0
|
||||||
|
},
|
||||||
|
countdownTimer: null,
|
||||||
|
rankShow: false,
|
||||||
|
detailVisible: false,
|
||||||
|
detailOption: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentSubject() {
|
||||||
|
return this.subjects.length > 0 ? this.subjects[0] : null
|
||||||
|
},
|
||||||
|
optionList() {
|
||||||
|
return this.currentSubject && this.currentSubject.options ? this.currentSubject.options : []
|
||||||
|
},
|
||||||
|
filteredOptions() {
|
||||||
|
const keyword = (this.keyword || "").trim()
|
||||||
|
if (!keyword) {
|
||||||
|
return this.optionList
|
||||||
|
}
|
||||||
|
return this.optionList.filter((option, index) => {
|
||||||
|
return String(index + 1).indexOf(keyword) > -1 || (option.text || "").indexOf(keyword) > -1
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pageTitle() {
|
||||||
|
return this.getCategoryPageTitle(this.activity && this.activity.category, "投票")
|
||||||
|
},
|
||||||
|
isEnd() {
|
||||||
|
if (!this.activity || !this.activity.endTime) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCategoryPageTitle(category, defaultTitle) {
|
||||||
|
const titleMap = {
|
||||||
|
QUIZ: "在线答题",
|
||||||
|
SURVEY: "问卷调查",
|
||||||
|
VOTE: "投票"
|
||||||
|
}
|
||||||
|
return titleMap[category] || defaultTitle
|
||||||
|
},
|
||||||
|
noop() {
|
||||||
|
},
|
||||||
|
|
||||||
|
getVoteOptionColumns(subject) {
|
||||||
|
if (!subject || subject.optionLayout !== "HORIZONTAL") {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return Number(subject.optionColumns) === 3 ? 3 : 2
|
||||||
|
},
|
||||||
|
|
||||||
|
getVoteOptionListClass(subject) {
|
||||||
|
return "vote-option-list vote-option-list-" + this.getVoteOptionColumns(subject)
|
||||||
|
},
|
||||||
|
|
||||||
|
hasOptionDetail(option) {
|
||||||
|
return !!(option && ((option.description && option.description.trim()) || option.link))
|
||||||
|
},
|
||||||
|
|
||||||
|
openOptionDetail(option) {
|
||||||
|
this.detailOption = option
|
||||||
|
this.detailVisible = true
|
||||||
|
},
|
||||||
|
|
||||||
|
closeOptionDetail() {
|
||||||
|
this.detailVisible = false
|
||||||
|
this.detailOption = null
|
||||||
|
},
|
||||||
|
|
||||||
|
getOptionRank(optionId) {
|
||||||
|
const list = this.voteStats.rankList || []
|
||||||
|
const index = list.findIndex((item) => String(item.optionId) === String(optionId))
|
||||||
|
return index > -1 ? index + 1 : "-"
|
||||||
|
},
|
||||||
|
|
||||||
|
voteFromDetail() {
|
||||||
|
if (!this.detailOption || this.answerRecord.isFinish || this.isEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.isOptionSelected(this.detailOption.id)) {
|
||||||
|
this.toggleOption(this.detailOption)
|
||||||
|
}
|
||||||
|
if (this.isOptionSelected(this.detailOption.id)) {
|
||||||
|
this.onSubmit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
listSubjects() {
|
||||||
|
this.$axios.post("/platform/h5/qsv/vote/subjects", {activityId: this.id}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.activity = res.data.activity || {}
|
||||||
|
this.subjects = res.data.subjects || []
|
||||||
|
this.answerRecordId = res.data.answerRecordId
|
||||||
|
this.answerRecord = res.data.answerRecord || {}
|
||||||
|
this.voteStats = res.data.voteStats || {}
|
||||||
|
this.initAnswer()
|
||||||
|
this.startCountdown()
|
||||||
|
} else {
|
||||||
|
this.$dialog.alert({
|
||||||
|
title: "提示",
|
||||||
|
message: res.msg
|
||||||
|
}).then(() => {
|
||||||
|
pjaxReplace("/platform/h5/qsv")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
initAnswer() {
|
||||||
|
if (!this.currentSubject || !this.answerRecord || !this.answerRecord.extJson) {
|
||||||
|
this.selectedOptionIds = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const answer = this.answerRecord.extJson[this.currentSubject.id]
|
||||||
|
this.selectedOptionIds = answer && answer.optionIds ? answer.optionIds.map(String) : []
|
||||||
|
},
|
||||||
|
|
||||||
|
startCountdown() {
|
||||||
|
if (this.countdownTimer) {
|
||||||
|
clearInterval(this.countdownTimer)
|
||||||
|
}
|
||||||
|
this.updateCountdown()
|
||||||
|
this.countdownTimer = setInterval(() => {
|
||||||
|
this.updateCountdown()
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCountdown() {
|
||||||
|
if (!this.activity || !this.activity.endTime) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const diff = Math.max(0, this.$moment(this.activity.endTime).unix() - this.$moment().unix())
|
||||||
|
this.countdown.days = Math.floor(diff / 86400)
|
||||||
|
this.countdown.hours = Math.floor((diff % 86400) / 3600)
|
||||||
|
this.countdown.minutes = Math.floor((diff % 3600) / 60)
|
||||||
|
this.countdown.seconds = diff % 60
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleOption(option) {
|
||||||
|
if (this.answerRecord.isFinish || this.isEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const optionId = String(option.id)
|
||||||
|
const index = this.selectedOptionIds.indexOf(optionId)
|
||||||
|
if (this.currentSubject.type === "radio") {
|
||||||
|
this.selectedOptionIds = index > -1 ? [] : [optionId]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (index > -1) {
|
||||||
|
this.selectedOptionIds.splice(index, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.currentSubject.maxMulti && this.selectedOptionIds.length >= this.currentSubject.maxMulti) {
|
||||||
|
this.$toast("最多选择" + this.currentSubject.maxMulti + "项")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectedOptionIds.push(optionId)
|
||||||
|
},
|
||||||
|
|
||||||
|
isOptionSelected(optionId) {
|
||||||
|
return this.selectedOptionIds.indexOf(String(optionId)) > -1
|
||||||
|
},
|
||||||
|
|
||||||
|
getOptionStats(optionId) {
|
||||||
|
return this.voteStats.optionStats && this.voteStats.optionStats[optionId]
|
||||||
|
? this.voteStats.optionStats[optionId]
|
||||||
|
: {votes: 0, percent: 0}
|
||||||
|
},
|
||||||
|
|
||||||
|
getOptionVotes(optionId) {
|
||||||
|
return this.getOptionStats(optionId).votes || 0
|
||||||
|
},
|
||||||
|
|
||||||
|
getOptionPercent(optionId) {
|
||||||
|
const percent = this.getOptionStats(optionId).percent || 0
|
||||||
|
return Number(percent).toFixed(2)
|
||||||
|
},
|
||||||
|
|
||||||
|
previewOptionImg(img) {
|
||||||
|
vant.ImagePreview([img])
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
if (!this.currentSubject) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isEnd) {
|
||||||
|
this.$toast("投票已结束")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.selectedOptionIds.length === 0) {
|
||||||
|
this.$toast.fail("请选择投票选项")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$axios.post("/platform/h5/qsv/vote/submitAnswer", {
|
||||||
|
answer: JSON.stringify({
|
||||||
|
activityId: this.id,
|
||||||
|
answerRecordId: this.answerRecordId,
|
||||||
|
subjects: [
|
||||||
|
{
|
||||||
|
id: this.currentSubject.id,
|
||||||
|
userSelectOptionIds: this.selectedOptionIds
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$toast.success(res.msg)
|
||||||
|
this.listSubjects()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
historyBack() {
|
||||||
|
pjaxReplace("/platform/h5/qsv")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.id) {
|
||||||
|
this.listSubjects()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.countdownTimer) {
|
||||||
|
clearInterval(this.countdownTimer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vote-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #fff1e9;
|
||||||
|
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-header {
|
||||||
|
padding: 36px 20px 28px;
|
||||||
|
background: radial-gradient(circle at right top, #fff 0, #fff7f2 38%, #fffdfb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-header h1 {
|
||||||
|
margin: 0 0 34px;
|
||||||
|
color: #ff4d00;
|
||||||
|
font-size: 22px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-stat-box {
|
||||||
|
height: 106px;
|
||||||
|
border: 1px solid #ff6a2b;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
background: rgba(255, 255, 255, .62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-stat-box:before,
|
||||||
|
.vote-stat-box:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #fff7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-stat-box:before {
|
||||||
|
left: -1px;
|
||||||
|
border-right: 1px solid #ff6a2b;
|
||||||
|
border-bottom: 1px solid #ff6a2b;
|
||||||
|
border-radius: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-stat-box:after {
|
||||||
|
right: -1px;
|
||||||
|
border-left: 1px solid #ff6a2b;
|
||||||
|
border-bottom: 1px solid #ff6a2b;
|
||||||
|
border-radius: 0 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-stat-item {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-stat-item strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #ff4d00;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-stat-item span {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-rank-btn {
|
||||||
|
justify-self: center;
|
||||||
|
min-width: 82px;
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid #ff6a2b;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: #ff4d00;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-rank-btn .fa {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-countdown {
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #ff4d00;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-countdown b {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #ff5a1f;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 8px 4px 0;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-search .van-search {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-search button {
|
||||||
|
width: 58px;
|
||||||
|
height: 44px;
|
||||||
|
border: 0;
|
||||||
|
border-left: 1px solid #f0f0f0;
|
||||||
|
background: #fff;
|
||||||
|
color: #ff4d00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-subject {
|
||||||
|
padding: 14px 15px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-subject-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-subject-title span {
|
||||||
|
color: #e61f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-subject-title em {
|
||||||
|
color: #666;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-list {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-list-2,
|
||||||
|
.vote-option-list-3 {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-list-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-list-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-card {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color .2s, background-color .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-list-2 .vote-option-card,
|
||||||
|
.vote-option-list-3 .vote-option-card {
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-card.active {
|
||||||
|
border-color: #ff4d00;
|
||||||
|
background: #fff8f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-img {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 172px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-list-2 .vote-option-img {
|
||||||
|
height: 126px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-list-3 .vote-option-img {
|
||||||
|
height: 92px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-img img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-img .van-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 8px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, .78);
|
||||||
|
color: #5a8ee8;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ff7b3c;
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #fff7f2, #ffe4d7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-height: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-control {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: #ff4d00;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-control:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-text strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #ff4d00;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-text span {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-option-detail-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 0 4px 8px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #ff4d00;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-bottom {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
margin: 0 15px;
|
||||||
|
padding: 14px 19px calc(14px + env(safe-area-inset-bottom));
|
||||||
|
background: #fff1e9;
|
||||||
|
z-index: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-rank-list {
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-rank-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-rank-index {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff5a1f;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-rank-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-rank-votes {
|
||||||
|
color: #ff4d00;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-page {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: calc(88px + env(safe-area-inset-bottom));
|
||||||
|
background: #f5f8fb;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-hero {
|
||||||
|
padding: 30px 18px 28px;
|
||||||
|
min-height: 150px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #ff4d00 0%, #ff6a1f 48%, #f24a00 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-hero h2 {
|
||||||
|
margin: 0 0 24px;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-hero h3 {
|
||||||
|
margin: 0 0 28px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-countdown {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-section {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-section h4 {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-section p {
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-section i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #ff4d00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-section a {
|
||||||
|
color: #ff4d00;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-content {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-empty {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-bottom {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 750px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
min-height: calc(54px + env(safe-area-inset-bottom));
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 1001;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-bottom button {
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #ff4d00;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-bottom button.primary {
|
||||||
|
background: #ff4d00;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-detail-bottom button:disabled {
|
||||||
|
background: #f3f3f3;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!--#
|
||||||
|
}
|
||||||
|
#-->
|
||||||
Reference in New Issue
Block a user