From 601e1ac039dcf50f25ddf0348cbcf7aa5d5f8006 Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Mon, 15 Jun 2026 14:20:03 +0800 Subject: [PATCH] Changes --- .../qsv/controller/QsvNewController.java | 21 + .../qsv/controller/QsvOnlineController.java | 562 +++++++ .../qsv/h5controller/H5QsvVoteController.java | 301 ++++ .../db/qsv_subject_add_vote_option_layout.sql | 3 + .../dayofficework/qsv/activity/logicForm.js | 744 +++++++++ .../zhgh/dayofficework/qsv/activity/new.html | 496 ++++++ .../zhgh/dayofficework/qsv/online/index.html | 1165 +++++++++++++++ .../dayofficework/qsv/survey/reportPage.html | 1323 +++++++++++++++++ .../zhghh5/dayofficework/qsv/vote/index.html | 872 +++++++++++ 9 files changed, 5487 insertions(+) create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvNewController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvOnlineController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvVoteController.java create mode 100644 src/main/resources/db/qsv_subject_add_vote_option_layout.sql create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/logicForm.js create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/new.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/qsv/online/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/reportPage.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/qsv/vote/index.html diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvNewController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvNewController.java new file mode 100644 index 00000000..e1a5855e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvNewController.java @@ -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() { + + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvOnlineController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvOnlineController.java new file mode 100644 index 00000000..0d1a4394 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvOnlineController.java @@ -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 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 activities = dao.query(QsvActivity.class, cnd); + List activityIds = activities.stream().map(QsvActivity::getId).collect(Collectors.toList()); + Map> recordGroup = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId()) + .and("activityId", "in", activityIds)) + .stream().collect(Collectors.groupingBy(QsvUserAnswerRecord::getActivityId)); + List 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 activitySubjects = queryActivitySubjectsForSync(activity); + syncAnswerRecordSubjects(answerRecord, activity, activitySubjects); + + List 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 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 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 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 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 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 subjects = dao.query(QsvSubject.class, subjectCnd.asc("sortNum")); + if (subjects.size() < randomCount) { + throw new RuntimeException("题目数不够,无法生成题目"); + } + Collections.shuffle(subjects); + List randomSubjectIds = subjects.subList(0, randomCount).stream().map(QsvSubject::getId).collect(Collectors.toList()); + return qsvUserAnswerRecordService.insertRecord(activity.getId(), randomSubjectIds); + } + + private List 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 activitySubjects) { + if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) { + return; + } + List currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()); + List 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 currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds); + List 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 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 querySubjectsByRecordOrder(List subjectIds) { + if (subjectIds == null || subjectIds.isEmpty()) { + return new ArrayList<>(); + } + List subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds)); + Map 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 subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); + dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); + + Map 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 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 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 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); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvVoteController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvVoteController.java new file mode 100644 index 00000000..d1498afc --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvVoteController.java @@ -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 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 subjectIds = activitySubjects.stream() + .map(QsvSubject::getId) + .collect(Collectors.toList()); + answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, subjectIds); + } + syncAnswerRecordSubjects(answerRecord, activitySubjects); + + List 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 activitySubjects) { + if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) { + return; + } + List currentSubjectIds = activitySubjects.stream() + .map(QsvSubject::getId) + .collect(Collectors.toList()); + List recordSubjectIds = answerRecord.getSubjectIds(); + if (recordSubjectIds == null) { + recordSubjectIds = new ArrayList<>(); + } + JSONObject extJson = answerRecord.getExtJson(); + if (extJson == null) { + extJson = new JSONObject(); + } + + LinkedHashSet currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds); + List 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 querySubjectsByRecordOrder(List subjectIds) { + if (subjectIds == null || subjectIds.isEmpty()) { + return new ArrayList<>(); + } + List subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds)); + Map 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 subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); + dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); + + Map 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 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 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 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); + } +} diff --git a/src/main/resources/db/qsv_subject_add_vote_option_layout.sql b/src/main/resources/db/qsv_subject_add_vote_option_layout.sql new file mode 100644 index 00000000..2b07d078 --- /dev/null +++ b/src/main/resources/db/qsv_subject_add_vote_option_layout.sql @@ -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 '投票选项横向列数'; diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/logicForm.js b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/logicForm.js new file mode 100644 index 00000000..59a747df --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/logicForm.js @@ -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*/ ` +
+
条件规则
+
+ + + + +
+ 添加条件 + 添加分组 + + +
+
+ +
+
+ + + +
+ +
+ 暂无条件,请点击上方按钮添加 +
+
+ + +
+ `, + 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*/ ` + +
+
+
+ 题目列表 + + 保存问卷 + +
+
+
+
+ {{index + 1}} + + {{getSubjectTypeText(subject)}} + + + {{subject.title || '未填写题目'}} + +
+
+ + + + + + + + + + +
+
+ +
+
+ +
+
+ 实时预览 +
+ 显示序号 + 配置即所得 +
+
+
+
+ {{index + 1}}. +
+
{{subject.title || '未填写题目'}}
+ + + + + + + +
+
+ +
+
+
+ + + + + + + + + + + +
+ `, + 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 + }) + } + } +} diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/new.html b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/new.html new file mode 100644 index 00000000..8889c22c --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/new.html @@ -0,0 +1,496 @@ + + + + +
+
+
+

{{pageTitle}}

+ 返回 +
+ + +

基础信息

+ + + + + {{item.label}} + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 设置 + +
+
+ +

题目信息

+ + + + + +
+ 只能上传1个文件;只能上传.jpg,.jpeg,.png文件;单个文件大小不能超过1.00M; +
+
+
+
+ + + + +
+
+ + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/online/index.html b/src/main/resources/views/platform/zhgh/dayofficework/qsv/online/index.html new file mode 100644 index 00000000..e89ef94d --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/online/index.html @@ -0,0 +1,1165 @@ + + + + +
+ + + + + + + + + + + + + + + + 已答题 + 未答题 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

{{answerDialog.activity.title}}

+
+
+ {{answerDialog.voteStats.optionCount || voteOptionList.length}} + 参与选项 +
+
+ {{answerDialog.voteStats.totalParticipants || 0}} + 投票总人数 +
+
+ + 排行榜 +
+
+
+
+ 距离投票结束还有: + + {{answerDialog.voteCountdown.days}}天 + {{answerDialog.voteCountdown.hours}}时 + {{answerDialog.voteCountdown.minutes}}分 + {{answerDialog.voteCountdown.seconds}}秒 + +
+
投票已结束
+ +
+
+ * + {{currentVoteSubject.title}} + + 【请选择1-{{currentVoteSubject.maxMulti}}项,已选择{{currentVoteSubject.userSelectOptionIds.length}}项】 + + 【请选择{{currentVoteSubject.type === 'radio' ? '1' : '至少1'}}项】 +
+
+
+
+ +
+
+ {{index + 1}} +
+
+ + +
+ {{getVoteOptionVotes(option.id)}}票({{getVoteOptionPercent(option.id)}}%) + {{option.text}} +
+
+ 详情 +
+
+
+
+
+ +
+ +
+
+ {{index + 1}}. + {{subject.title}} + * +
+ +
+ + + +
+ +
+ + + +
+ + +
+
+ +
+ + + + + + + + + + + +
+
+ +
+

{{answerDialog.voteDetailOption.text}}

+
+ {{getVoteOptionVotes(answerDialog.voteDetailOption.id)}}票 + {{getVoteOptionPercent(answerDialog.voteDetailOption.id)}}% + 第{{getVoteOptionRank(answerDialog.voteDetailOption.id)}}名 +
+
+

开始时间:{{answerDialog.activity.startTime || '-'}}

+

结束时间:{{answerDialog.activity.endTime || '-'}}

+ 查看规则详情 +
+
+ +
+ +
+
+ + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/reportPage.html b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/reportPage.html new file mode 100644 index 00000000..20a9415b --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/reportPage.html @@ -0,0 +1,1323 @@ + + + + +
+ + +
+
{{activityTitle || '分析报告'}}
+
+ 返回 + 导出xlsx + 下载报告 +
+
+ + + +
+ +
暂无分析数据
+
+
+ + +
+
选择条件
+
+
{{index + 1}}.
+ + + + +
+ +
选择分类
+
+ + {{item.label}} + +
+
请选择两个选择题条件后生成分类
+
+ +
+ 当前分类:{{categoryActiveLabel}} +
+ +
+ +
暂无分析数据
+
+
+ + +
+ + 交叉分析 + 对比分析 + + + + + +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/qsv/vote/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/qsv/vote/index.html new file mode 100644 index 00000000..a0da21a7 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/qsv/vote/index.html @@ -0,0 +1,872 @@ + + +
+ + +
+

{{activity.title}}

+
+
+ {{voteStats.optionCount || optionList.length}} + 参与选项 +
+
+ {{voteStats.totalParticipants || 0}} + 投票总人数 +
+ +
+
+ +
+ 距离投票结束还有: + {{countdown.days}} 天 + {{countdown.hours}} 时 + {{countdown.minutes}} 分 + {{countdown.seconds}} 秒 +
+
投票已结束
+ + + +
+
+ * + {{currentSubject.title}} + + 【请选择1-{{currentSubject.maxMulti}}项,已选择{{selectedOptionIds.length}}项】 + + 【请选择{{currentSubject.type === 'radio' ? '1' : '至少1'}}项】 +
+ +
+
+
+ +
+
+ {{index + 1}} +
+
+ +
+ {{getOptionVotes(option.id)}}票({{getOptionPercent(option.id)}}%) + {{option.text}} +
+ +
+
+
+
+ + + +
+ + {{answerRecord.isFinish ? '已投票' : '投票'}} + +
+ + +
+
+ {{index + 1}} +
{{item.text}}
+
{{item.votes}}票
+
+
+
+ +
+
+

{{activity.title}}

+

》 {{detailOption.text}} 《

+
+ {{getOptionVotes(detailOption.id)}}票 + 第{{getOptionRank(detailOption.id)}}名 +
+
+
+ 距离投票结束还有: + {{countdown.days}} 天 + {{countdown.hours}} 时 + {{countdown.minutes}} 分 + {{countdown.seconds}} 秒 +
+
投票已结束
+
+

投票规则

+

开始时间:{{activity.startTime || '-'}}

+

结束时间:{{activity.endTime || '-'}}

+ 查看规则详情 +
+
+

详情介绍

+
+
暂无详情
+
+
+ + +
+
+
+ + + + + +