From 22c82c67513acd944d025a2207a685b4ae2c37c7 Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Mon, 1 Jun 2026 17:19:45 +0800 Subject: [PATCH] commit --- .../qsv/controller/QsvSurveyController.java | 31 ++- .../h5controller/H5QsvSurveyController.java | 53 ++++- .../dayofficework/qsv/models/QsvOption.java | 10 + .../dayofficework/qsv/models/QsvSubject.java | 38 +++ .../qsv/param/QsvAnswerParam.java | 2 + .../service/impl/QsvSurveyServiceImpl.java | 144 ++++++++---- .../dayofficework/qsv/activity/setting.js | 20 +- .../dayofficework/qsv/activity/subjectForm.js | 218 +++++++++++++++++- .../zhgh/dayofficework/qsv/survey/answer.js | 11 +- .../zhgh/dayofficework/qsv/survey/index.html | 36 +++ .../zhgh/dayofficework/qsv/survey/report.js | 94 +++++--- .../dayofficework/qsv/survey/index.html | 206 ++++++++++++++--- 12 files changed, 748 insertions(+), 115 deletions(-) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java index 2187793..753b7d3 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java @@ -1,6 +1,7 @@ package com.budwk.app.zhgh.dayofficework.qsv.controller; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.json.JSONArray; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONObject; import com.budwk.app.base.annotation.SLog; @@ -77,8 +78,34 @@ public class QsvSurveyController { @SaCheckPermission("qsv.survey") @ApiOperation("选项选择详情") public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId) { - List answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)); - List selectOptionUsers = answerRecords.stream().filter(ext -> ObjectUtil.isNotNull(ext.getExtJson().get(subjectId, JSONObject.class)) && ext.getExtJson().get(subjectId, JSONObject.class).getJSONArray("optionIds").contains(optionId)).toList(); + List answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("isFinish", "=", true)); + List selectOptionUsers = answerRecords.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(); return Result.success(selectOptionUsers); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java index 664aed4..8a922c0 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java @@ -12,6 +12,8 @@ import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam; import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService; import org.nutz.dao.Cnd; 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.IocBean; import org.nutz.lang.util.NutMap; @@ -23,6 +25,7 @@ import javax.validation.Valid; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; @IocBean @At("/platform/h5/qsv/survey") @@ -40,6 +43,7 @@ public class H5QsvSurveyController { } @At + @Aop(TransAop.READ_COMMITTED) public Result subjects(String activityId) { QsvActivity activity = dao.fetch(QsvActivity.class, activityId); @@ -51,16 +55,17 @@ public class H5QsvSurveyController { String answerRecordId = null; + 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 subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); - qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).toList()); + qsvUserAnswerRecordService.insertRecord(activityId, activitySubjects.stream().map(QsvSubject::getId).toList()); } QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) .and("userId", "=", SecurityUtil.getUserId())); + syncAnswerRecordSubjects(answerRecord2, activitySubjects); answerRecordId = answerRecord2.getId(); @@ -77,6 +82,48 @@ public class H5QsvSurveyController { return Result.success(result); } + /** + * 同步问卷当前题目到未完成答题记录。后台新增题目后,已进入过问卷的用户记录里没有新题ID, + * 这里补齐 subjectIds 和 extJson,保证手机端能拿到最新题目并正常提交答案。 + * + * @param answerRecord 用户当前问卷答题记录 + * @param activitySubjects 当前活动下按排序查询出的最新题目列表 + */ + 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(); + } + + boolean changed = !recordSubjectIds.equals(currentSubjectIds); + 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(currentSubjectIds); + answerRecord.setExtJson(extJson); + dao.update(answerRecord); + } + } + @At public Result answerRecord(@Valid String answerRecordId) { QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId); @@ -84,6 +131,7 @@ public class H5QsvSurveyController { } @At + @Aop(TransAop.READ_COMMITTED) public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) { QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId()); JSONObject extJson = answerRecord.getExtJson(); @@ -93,6 +141,7 @@ public class H5QsvSurveyController { if (ObjectUtil.isNotEmpty(entries)) { entries.set("optionIds", subject.getUserSelectOptionIds()); entries.set("text", subject.getUserFillContent()); + entries.set("optionFillContents", subject.getOptionFillContents()); } extJson.set(subject.getId(), entries); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java index 9c69355..7d53ae4 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java @@ -51,6 +51,16 @@ public class QsvOption extends BaseModel implements Serializable { @ColDefine(customType = "longtext") private String description; + @Column + @Comment("选中后是否需要填写补充内容") + @ColDefine(type = ColType.BOOLEAN) + private Boolean fillRequired; + + @Column + @Comment("补充内容输入提示") + @ColDefine(type = ColType.VARCHAR, width = 255) + private String fillPlaceholder; + @Column @Comment("排序") @ColDefine(type = ColType.INT) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java index b72f5bc..3d50e73 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java @@ -63,6 +63,26 @@ public class QsvSubject extends BaseModel implements Serializable { @ColDefine(type = ColType.INT, width = 1) 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 visibleRuleConditions; + /** * 选项 */ @@ -79,4 +99,22 @@ public class QsvSubject extends BaseModel implements Serializable { */ private String userFillContent; + /** + * 题目隐显逻辑条件,一条条件表示选择指定题目的指定选项后参与显示或隐藏判断。 + */ + @Data + public static class VisibleRuleCondition implements Serializable { + + /** + * 作为触发条件的题目ID。 + */ + private String subjectId; + + /** + * 作为触发条件的选项ID。 + */ + private String optionId; + + } + } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java index c614794..6c7e2e3 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java @@ -3,6 +3,7 @@ package com.budwk.app.zhgh.dayofficework.qsv.param; import lombok.Data; import java.util.List; +import java.util.Map; @Data //答题参数 @@ -21,6 +22,7 @@ public class QsvAnswerParam { private String id; private List userSelectOptionIds; private String userFillContent; + private Map optionFillContents; } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java index ff21f11..1591688 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java @@ -68,47 +68,47 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i // 处理每个题目 for (NutMap subject : subjects) { // 获取当前题目的选项列表 - List subjectOptions = Lang.collection2list(optionsGroup.get(subject.getString("id")), NutMap.class); + List currentOptions = optionsGroup.get(subject.getString("id")); + List subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class); // 获取题目类型 String subjectType = subject.getString("type"); // 处理文本类型题目 if ("text".equals(subjectType)) { - List texts = answerExtList.stream() - .filter(ext -> ObjectUtil.isNull(ext.get(subject.getString("id"), JSONObject.class))) - .map(ext -> ext.get(subject.getString("id"), JSONObject.class).getStr("text")) + List textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id")); + List 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 -> ext.containsKey(subject.getString("id"))) + .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 = Math.round((double) selectCount / selectTotal * 100); + long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100); subjectOption.put("selectPercent", round + "%"); subjectOption.put("selectCount", selectCount); }); subjectOptions.sort((o1, o2) -> { - int count1 = o1.getInt("selectCount"); - int count2 = o2.getInt("selectCount"); - if (count1 == count2) { - return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum")); - } - return Integer.compare(count2, count1); + return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum")); }); subject.put("selectTotal", selectTotal); @@ -134,10 +134,8 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20)); exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20)); - Map> listMap = report.stream().collect(Collectors.groupingBy(v -> v.getString("id"))); Workbook workbook = new XSSFWorkbook(); - listMap.forEach((k, v) -> { - NutMap nutMap = v.get(0); + report.forEach(nutMap -> { ExcelExportService service = new ExcelExportService(); ExportParams exportParams = new ExportParams(); exportParams.setTitle(nutMap.getString("title")); @@ -171,7 +169,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i List answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList(); // 查询活动题目列表 - List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId)); + List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); // 将题目列表转换为Map Map subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v)); // 获取题目ID列表 @@ -185,7 +183,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i // 构建Excel导出实体 List excelExportEntities = buildExcelExportEntities(subjects); // 构建答题记录列表 - List list = buildAnswerRecords(answerRecords, subjectMap, options); + List list = buildAnswerRecords(answerRecords, subjectMap, optionMap); // 设置导出参数 ExportParams exportParams = new ExportParams(); @@ -223,7 +221,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i // List answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList(); // 查询活动题目列表 - List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId)); + List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); // 将题目列表转换为Map Map subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v)); // 获取题目ID列表 @@ -245,18 +243,15 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i .addv("unitName", record.getString("unitName")) .addv("unionName", record.getString("unionName")); JSONObject extJson = record.getAs("extJson", JSONObject.class); + if (ObjectUtil.isEmpty(extJson)) { + return map; + } extJson.forEach((k, v) -> { - JSONObject jsonVal = (JSONObject) v; - - String type = subjectMap.get(k).getType(); - 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); + QsvSubject subject = subjectMap.get(k); + if (subject == null || !(v instanceof JSONObject jsonVal)) { + return; } + map.addv(k, buildAnswerText(jsonVal, subject, optionMap)); }); return map; }).toList(); @@ -309,7 +304,37 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i return excelExportEntities; } - private List buildAnswerRecords(List answerRecords, Map subjectMap, List options) { + /** + * 构建填空题填写详情。分析弹窗需要同时展示填写内容和答题人信息, + * 因此这里从已完成答题记录中提取当前题目的文本答案,并携带姓名、工号、所属工会和单位。 + * + * @param answerRecords 当前活动已完成的答题记录 + * @param subjectId 当前填空题ID,用于从 extJson 中取出该题填写内容 + * @return 填空题填写详情列表,每条包含 loginName、userName、unionName、unitName、submitTime、text + */ + private List buildTextAnswerDetails(List answerRecords, String subjectId) { + List 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 buildAnswerRecords(List answerRecords, Map subjectMap, Map optionMap) { return answerRecords.stream().map(record -> { NutMap map = NutMap.NEW() .addv("id", record.getId()) @@ -318,20 +343,61 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i .addv("unitName", record.getUnitName()) .addv("unionName", record.getUnionName()); JSONObject extJson = record.getExtJson(); + if (ObjectUtil.isEmpty(extJson)) { + return map; + } extJson.forEach((k, v) -> { - JSONObject jsonVal = (JSONObject) v; - - String type = subjectMap.get(k).getType(); - 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); + QsvSubject subject = subjectMap.get(k); + if (subject == null || !(v instanceof JSONObject jsonVal)) { + return; } + map.addv(k, buildAnswerText(jsonVal, subject, optionMap)); }); return map; }).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 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 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); + } } diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js index 5367a46..e188f8f 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js @@ -8,6 +8,20 @@ const setting = { + + + + + + + +
@@ -214,6 +222,87 @@ const subjectForm = { + + + + + + + + + + +
`, components: { @@ -230,7 +319,32 @@ const subjectForm = { subjects: [], previewDialogVisible: false, activity: {}, - formRules: {} + formRules: {}, + subjectVisibleSetting: { + visible: false, + subjectIndex: null, + formData: { + enabled: false, + action: "show", + logic: "AND", + conditions: [ + { + subjectId: "", + optionId: "" + } + ] + } + } + } + }, + computed: { + subjectVisibleRuleSubjects() { + if (this.subjectVisibleSetting.subjectIndex === null) { + return [] + } + return this.subjects.filter((item, index) => { + return index < this.subjectVisibleSetting.subjectIndex && item.type !== "text" + }) } }, methods: { @@ -351,13 +465,113 @@ const subjectForm = { //选项设置确认 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]) // this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "isCorrect", option.isCorrect) // this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "hint", option.hint) }, + //打开题目隐显逻辑设置,用于配置当前题目受前置选择题的哪个选项控制显示或隐藏 + openSubjectVisibleSetting(subjectIndex, subject) { + const conditions = this.buildSubjectVisibleConditions(subject) + this.subjectVisibleSetting.subjectIndex = subjectIndex + this.subjectVisibleSetting.formData = { + enabled: !!subject.visibleRuleEnabled, + action: subject.visibleRuleAction || "show", + logic: subject.visibleRuleLogic || "AND", + conditions + } + this.subjectVisibleSetting.visible = true + }, + + //构建题目隐显逻辑条件,已有条件按实际数量回显;未配置时默认提供一条空条件 + buildSubjectVisibleConditions(subject) { + let visibleRuleConditions = subject.visibleRuleConditions || [] + if (typeof visibleRuleConditions === "string") { + try { + visibleRuleConditions = JSON.parse(visibleRuleConditions) + } catch (e) { + visibleRuleConditions = [] + } + } + const conditions = visibleRuleConditions || [] + const result = conditions.map((condition) => { + return { + subjectId: condition.subjectId || "", + optionId: condition.optionId || "" + } + }) + if (result.length === 0) { + result.push({ + subjectId: "", + optionId: "" + }) + } + return result + }, + + //根据触发题目获取可选项,用于每条隐显条件选择触发选项 + getSubjectVisibleRuleOptions(subjectId) { + const subject = this.subjects.find((item) => { + return item.id === subjectId + }) + if (!subject || !subject.options) { + return [] + } + return subject.options + }, + + //切换触发题目后清空当前条件的旧选项,避免保留其他题目的选项导致隐显条件错误 + subjectVisibleRuleSubjectChange(condition) { + condition.optionId = "" + }, + + //添加一条隐显逻辑条件,多个条件通过条件关系字段统一按并且或或者计算 + addSubjectVisibleRuleCondition() { + this.subjectVisibleSetting.formData.conditions.push({ + subjectId: "", + optionId: "" + }) + }, + + //删除一条隐显逻辑条件,至少保留一条条件用于继续配置 + removeSubjectVisibleRuleCondition(conditionIndex) { + if (this.subjectVisibleSetting.formData.conditions.length === 1) { + this.$message.warning("至少保留一条条件") + return + } + this.subjectVisibleSetting.formData.conditions.splice(conditionIndex, 1) + }, + + //保存题目隐显逻辑到当前题目对象,后续保存题目时随 subjects 一起提交 + onSubjectVisibleSettingConfirm() { + const subjectIndex = this.subjectVisibleSetting.subjectIndex + if (subjectIndex === null) { + return + } + const formData = this.subjectVisibleSetting.formData + if (formData.enabled) { + for (let i = 0; i < formData.conditions.length; i++) { + const condition = formData.conditions[i] + if (!condition.subjectId || !condition.optionId) { + this.$message.warning("请选择条件" + (i + 1) + "的触发题目和触发选项") + return + } + } + } + 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", formData.enabled ? formData.logic : "AND") + this.$set(this.subjects[subjectIndex], "visibleRuleConditions", formData.enabled ? formData.conditions.map((condition) => { + return { + subjectId: condition.subjectId, + optionId: condition.optionId + } + }) : []) + this.subjectVisibleSetting.visible = false + }, + //从文本导入 openTxtImport() { this.$refs.txtImportRef.visible = true; diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/answer.js b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/answer.js index 90f0ef4..9e2a1e2 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/answer.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/answer.js @@ -5,7 +5,7 @@ const answer = { 导出xlsx - + @@ -30,17 +30,24 @@ const answer = { onOpen(activityId) { this.activityId = activityId this.dialogVisible = true + this.tableColumns = [] + this.tableData = [] + this.pageForm.pageNumber = 1 + this.pageForm.totalCount = 0 this.pageData() }, pageData() { this.pageForm.activityId = this.activityId + this.tableLoading = true $.post("/platform/qsv/survey/userAnswer", this.pageForm).then((res) => { if (res.code === 0) { this.tableColumns = res.data.tableColumns this.tableData = res.data.tableData.list 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) => { if (res.code === 0) { this.$message.success(res.msg) - this.list() + this.pageData() } }) }) diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/index.html b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/index.html index 031b7d3..0e464e3 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/index.html @@ -2,6 +2,42 @@ layout("/layouts/platform.html"){ #--> + +
diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/report.js b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/report.js index b1ecc49..b78613b 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/report.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/survey/report.js @@ -1,41 +1,44 @@ const report = { /*language=HTML*/ template: ` - - - 导出xlsx - -
-
-
- 第{{subjectIndex+1}}题: {{subject.title}} - [填空题] - [单选题] - [多选题] -
+ +
+ + 导出xlsx + +
+
+
+
+
+ 第{{subjectIndex+1}}题: {{subject.title}} + [填空题] + [单选题] + [多选题] +
-
- {{subject.texts.join('、')}} -
+
+ 查看填写内容 +
-
- - - - - - - - - - +
+ + + + + + + + + + +
-
@@ -45,9 +48,22 @@ const report = { + + + + + + + + + + + + + `, @@ -59,7 +75,10 @@ const report = { report: null, subjects: [], optionVisible: false, - optionUsers: [] + optionUsers: [], + textVisible: false, + textSubject: {}, + textAnswers: [] } }, methods: { @@ -73,8 +92,6 @@ const report = { }) }, openDetail(subjectId, optionId) { - console.log(subjectId) - console.log(optionId) $.post("/platform/qsv/survey/selectOptionUsers", { activityId: this.activityId, 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(){ this.$downLoad("/platform/qsv/survey/exportReportXlsx", { activityId: this.activityId }) } diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/qsv/survey/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/qsv/survey/index.html index 156996b..b137365 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/qsv/survey/index.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/qsv/survey/index.html @@ -80,6 +80,27 @@ layout("/layouts/platform_h5.html"){ resize: none; 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%; + }
@@ -87,7 +108,7 @@ layout("/layouts/platform_h5.html"){

{{ activity.title }}

-
+

{{index+1}}、{{ subject.title }}

多选 @@ -102,31 +123,43 @@ layout("/layouts/platform_h5.html"){ v-if="['radio','checkbox'].includes(subject.type)" > - - - - +
- +
@@ -158,6 +191,11 @@ layout("/layouts/platform_h5.html"){ return this.$moment().unix() > this.$moment(this.activity.endTime).unix() } return true + }, + visibleSubjects() { + return this.subjects.filter((subject) => { + return this.isSubjectVisible(subject) + }) } }, @@ -193,14 +231,106 @@ layout("/layouts/platform_h5.html"){ const answer = this.answerRecord.extJson[subject.id] if (["radio", "checkbox"].includes(subject.type)) { - this.$refs["subject" + subject.id][0].toggleAll(false) - answer?.optionIds.forEach((optionId) => { - this.$nextTick(() => { - this.$refs["option" + optionId][0].toggle() - }) - }) + this.$set(subject, "userSelectOptionIds", answer?.optionIds || []) + this.$set(subject, "optionFillContents", answer?.optionFillContents || {}) } 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 conditions = this.getVisibleRuleConditions(subject) + if (conditions.length === 0) { + return true + } + + const results = conditions.map((condition) => { + const triggerSubject = this.subjects.find((item) => { + return item.id === condition.subjectId + }) + return triggerSubject + && triggerSubject.userSelectOptionIds + && triggerSubject.userSelectOptionIds.includes(condition.optionId) + }) + + const matched = subject.visibleRuleLogic === "OR" + ? results.some((item) => { + return item + }) + : results.every((item) => { + return item + }) + + if (subject.visibleRuleAction === "hide") { + return !matched + } + return matched + }, + + //读取隐显逻辑条件,只使用后台保存的条件数组参与显示或隐藏判断 + getVisibleRuleConditions(subject) { + let conditions = subject.visibleRuleConditions || [] + if (typeof conditions === "string") { + try { + conditions = JSON.parse(conditions) + } catch (e) { + conditions = [] + } + } + conditions = conditions.filter((condition) => { + return condition.subjectId && condition.optionId + }) + if (conditions.length > 0) { + return conditions + } + return [] + }, + + //清空当前隐藏题目的答案,避免隐藏题残留旧选项或填空内容参与后续提交 + 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) + }, + + //选中需要补充填写的选项后显示输入框,例如选择“否”后填写原因 + 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) } }) }, @@ -215,6 +345,10 @@ layout("/layouts/platform_h5.html"){ this.$refs["subject" + subject.id][0].toggleAll(false) } this.$refs["option" + optionId][0].toggle() + this.$nextTick(() => { + this.clearUnselectedOptionFillContents(subject) + this.clearHiddenSubjectAnswers() + }) }, //预览图片 @@ -231,13 +365,20 @@ layout("/layouts/platform_h5.html"){ } //提示那些题没有作答 - for (let i = 0; i < this.subjects.length; i++) { - const subject = this.subjects[i] + for (let i = 0; i < this.visibleSubjects.length; i++) { + const subject = this.visibleSubjects[i] if (["radio", "checkbox"].includes(subject.type)) { if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) { this.$toast.fail("第" + (i + 1) + "题未做答") 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) { if (!subject.userFillContent) { this.$toast.fail("第" + (i + 1) + "题未作答") @@ -255,11 +396,12 @@ layout("/layouts/platform_h5.html"){ answer: JSON.stringify({ activityId: this.id, answerRecordId: this.answerRecordId, - subjects: this.subjects.map((subject) => { + subjects: this.visibleSubjects.map((subject) => { return { id: subject.id, 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 : {} } }) })