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..d10371e 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 @@ -1,5 +1,6 @@ package com.budwk.app.zhgh.dayofficework.qsv.h5controller; +import cn.dev33.satoken.annotation.SaCheckLogin; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONObject; import com.budwk.app.base.result.Result; @@ -34,7 +35,12 @@ public class H5QsvSurveyController { @Inject private QsvUserAnswerRecordService qsvUserAnswerRecordService; + /** + * 扫码进入问卷前校验登录状态;未登录时由统一异常处理发起认证, + * 携带原始地址及 id 参数,认证成功后返回当前问卷。 + */ @At("") + @SaCheckLogin @Ok("beetl:/platform/zhghh5/dayofficework/qsv/survey/index.html") public void index() { } 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/resources/views/platform/zhgh/dayofficework/qsv/activity/subjectForm.js b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/subjectForm.js index 1c22aaf..bdf3dd3 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/subjectForm.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/subjectForm.js @@ -20,6 +20,9 @@ const subjectForm = { placeholder="请输入题目" style="flex: 1"> + 隐显逻辑
@@ -205,7 +208,7 @@ const subjectForm = { 添加题目 - + 保存题目
@@ -214,6 +217,87 @@ const subjectForm = { + + + + + + + + + + + `, components: { @@ -230,7 +314,33 @@ const subjectForm = { subjects: [], previewDialogVisible: false, activity: {}, - formRules: {} + formLoading: false, + 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 && ["radio", "checkbox"].includes(item.type) + }) } }, methods: { @@ -358,6 +468,136 @@ const subjectForm = { // this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "hint", option.hint) }, + //打开题目隐显逻辑设置,用于配置当前题目受前置选择题的哪个选项控制显示或隐藏 + openSubjectVisibleSetting(subjectIndex, subject) { + const conditions = this.buildSubjectVisibleConditions(subject) + this.$set(this.subjectVisibleSetting, "subjectIndex", subjectIndex) + this.$set(this.subjectVisibleSetting, "formData", { + enabled: !!subject.visibleRuleEnabled, + action: subject.visibleRuleAction || "show", + logic: subject.visibleRuleLogic || "AND", + conditions + }) + this.$set(this.subjectVisibleSetting, "visible", true) + }, + + //构建题目隐显逻辑条件,已有条件按实际数量回显;未配置时默认提供一条空条件 + buildSubjectVisibleConditions(subject) { + let visibleRuleConditions = subject.visibleRuleConditions || [] + if (typeof visibleRuleConditions === "string") { + try { + visibleRuleConditions = JSON.parse(visibleRuleConditions) + } catch (e) { + visibleRuleConditions = [] + } + } + const conditions = Array.isArray(visibleRuleConditions) ? visibleRuleConditions.filter((condition) => condition) : [] + 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) { + this.$set(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.closeSubjectVisibleSetting() + }, + + //关闭配置弹框;取消时不将编辑中的条件写回题目。 + closeSubjectVisibleSetting() { + this.$set(this.subjectVisibleSetting, "visible", false) + }, + + //保存整份问卷前核对前置题和选项,阻止删除、改题型或拖动排序后留下失效依赖。 + validateSubjectVisibleRules() { + for (let index = 0; index < this.subjects.length; index++) { + const subject = this.subjects[index] + if (!subject.visibleRuleEnabled) { + continue + } + const conditions = this.buildSubjectVisibleConditions(subject) + const valid = ["show", "hide"].includes(subject.visibleRuleAction) + && ["AND", "OR"].includes(subject.visibleRuleLogic) + && conditions.every((condition) => { + return this.subjects.slice(0, index).some((trigger) => { + return trigger.id === condition.subjectId + && ["radio", "checkbox"].includes(trigger.type) + && (trigger.options || []).some((option) => option.id === condition.optionId) + }) + }) + if (!valid) { + this.$message.warning("第" + (index + 1) + "题的隐显条件已失效,请重新选择前置题目和选项或关闭规则") + return false + } + } + return true + }, + //从文本导入 openTxtImport() { this.$refs.txtImportRef.visible = true; @@ -395,6 +635,10 @@ const subjectForm = { //保存 saveQuestionnaire() { + if (this.activity.category === "SURVEY" && !this.validateSubjectVisibleRules()) { + return + } + this.$set(this, "formLoading", true) $ .post("/platform/qsv/activity/saveSubjects", { activityId: this.id, @@ -402,11 +646,14 @@ const subjectForm = { }) .then((res) => { if (res.code === 0) { - this.visible = false + this.$set(this, "visible", false) this.$message.success(res.msg) this.$emit("refresh", null) } }) + .always(() => { + this.$set(this, "formLoading", false) + }) } }, style: /*language=CSS*/ ` 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..4b26b0c 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 @@ -87,7 +87,7 @@ layout("/layouts/platform_h5.html"){

{{ activity.title }}

-
+

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

多选 @@ -98,6 +98,7 @@ layout("/layouts/platform_h5.html"){ @@ -153,6 +154,16 @@ layout("/layouts/platform_h5.html"){ }, computed: { + //按题目顺序计算隐显,只有当前可见的前置题答案可以触发后续题目。 + visibleSubjects() { + const visible = [] + this.subjects.forEach((subject) => { + if (this.isSubjectVisible(subject, visible)) { + visible.push(subject) + } + }) + return visible + }, isEnd() { if (this.activity) { return this.$moment().unix() > this.$moment(this.activity.endTime).unix() @@ -187,20 +198,66 @@ layout("/layouts/platform_h5.html"){ }) }, - //答案回显 + //直接回填答案数据,隐藏题没有组件引用,也能安全完成历史答卷回显。 initAnswer() { - this.subjects.forEach((subject, subjectIndex) => { - const answer = this.answerRecord.extJson[subject.id] - + this.subjects.forEach((subject) => { + 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 || []) } else if (subject.type === "text") { - subject.userFillContent = answer?.text + this.$set(subject, "userFillContent", answer?.text || null) + } + }) + this.onSubjectAnswerChange() + }, + + //subject 为当前题目,visible 为已判定可见的前置题;返回布尔值决定是否显示。 + isSubjectVisible(subject, visible) { + if (!subject.visibleRuleEnabled) { + return true + } + let conditions = subject.visibleRuleConditions || [] + if (typeof conditions === "string") { + try { + conditions = JSON.parse(conditions) + } catch (e) { + conditions = [] + } + } + //旧数据或无有效条件时默认展示,避免整题无法填写。 + if (!Array.isArray(conditions) || conditions.length === 0) { + return true + } + const results = conditions.map((condition) => { + if (!condition) { + return false + } + const trigger = visible.find((item) => item.id === condition.subjectId) + return !!trigger && ["radio", "checkbox"].includes(trigger.type) + && (trigger.options || []).some((option) => option.id === condition.optionId) + && (trigger.userSelectOptionIds || []).includes(condition.optionId) + }) + const matched = subject.visibleRuleLogic === "OR" + ? results.some((value) => value) + : results.every((value) => value) + return subject.visibleRuleAction === "hide" ? !matched : matched + }, + + //选择变化后清理所有隐藏题答案;保留已提交记录,避免回显时改动历史数据。 + onSubjectAnswerChange() { + if (this.answerRecord.isFinish) { + return + } + const visibleIds = this.visibleSubjects.map((subject) => subject.id) + this.subjects.forEach((subject) => { + if (visibleIds.includes(subject.id)) { + return + } + if ((subject.userSelectOptionIds || []).length > 0) { + this.$set(subject, "userSelectOptionIds", []) + } + if (subject.userFillContent) { + this.$set(subject, "userFillContent", null) } }) }, @@ -231,8 +288,8 @@ 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) + "题未做答") @@ -255,7 +312,7 @@ 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 : [],