调查问卷二维码不跳转,白屏问题;增加题目隐显逻辑
This commit is contained in:
+6
@@ -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() {
|
||||
}
|
||||
|
||||
@@ -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<VisibleRuleCondition> 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ const subjectForm = {
|
||||
placeholder="请输入题目"
|
||||
style="flex: 1">
|
||||
</el-input>
|
||||
<el-button v-if="activity.category === 'SURVEY'" type="primary"
|
||||
style="margin-left: 8px" icon="el-icon-setting"
|
||||
@click="openSubjectVisibleSetting(subjectIndex, subject)">隐显逻辑</el-button>
|
||||
</div>
|
||||
|
||||
<div class="subject-meta">
|
||||
@@ -205,7 +208,7 @@ const subjectForm = {
|
||||
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
|
||||
添加题目
|
||||
</el-button>
|
||||
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire">
|
||||
<el-button type="primary" icon="el-icon-check" :loading="formLoading" @click="saveQuestionnaire">
|
||||
保存题目
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -214,6 +217,87 @@ const subjectForm = {
|
||||
<setting ref="settingRef" @confirm="onSettingConfirm"></setting>
|
||||
<txt-import ref="txtImportRef" @confirm="onTxtImportConfirm"></txt-import>
|
||||
<bank ref="bankRef" @confirm="onBankConfirm"></bank>
|
||||
|
||||
<el-dialog
|
||||
title="设置题目隐显逻辑"
|
||||
:visible.sync="subjectVisibleSetting.visible"
|
||||
append-to-body
|
||||
width="520px">
|
||||
<el-form :model="subjectVisibleSetting.formData" label-width="110px">
|
||||
<el-form-item label="启用逻辑">
|
||||
<el-switch
|
||||
v-model="subjectVisibleSetting.formData.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="关闭">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<template v-if="subjectVisibleSetting.formData.enabled">
|
||||
<el-form-item label="处理方式">
|
||||
<el-radio-group v-model="subjectVisibleSetting.formData.action">
|
||||
<el-radio label="show">选中后显示本题</el-radio>
|
||||
<el-radio label="hide">选中后隐藏本题</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="条件关系">
|
||||
<el-radio-group v-model="subjectVisibleSetting.formData.logic">
|
||||
<el-radio label="AND">并且</el-radio>
|
||||
<el-radio label="OR">或者</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div
|
||||
v-for="(condition, conditionIndex) in subjectVisibleSetting.formData.conditions"
|
||||
:key="conditionIndex"
|
||||
class="subject-visible-condition">
|
||||
<el-form-item :label="'条件' + (conditionIndex + 1)">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-select
|
||||
v-model="condition.subjectId"
|
||||
placeholder="请选择触发题目"
|
||||
style="flex: 1;"
|
||||
@change="subjectVisibleRuleSubjectChange(condition)">
|
||||
<el-option
|
||||
v-for="item in subjectVisibleRuleSubjects"
|
||||
:key="item.id"
|
||||
:label="item.title || '未填写题目'"
|
||||
:value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="condition.optionId"
|
||||
placeholder="请选择触发选项"
|
||||
style="flex: 1;">
|
||||
<el-option
|
||||
v-for="item in getSubjectVisibleRuleOptions(condition.subjectId)"
|
||||
:key="item.id"
|
||||
:label="item.text || '未填写选项'"
|
||||
:value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
@click="removeSubjectVisibleRuleCondition(conditionIndex)">
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
size="small"
|
||||
@click="addSubjectVisibleRuleCondition">
|
||||
添加条件
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="closeSubjectVisibleSetting">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubjectVisibleSettingConfirm">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
`,
|
||||
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*/ `
|
||||
|
||||
@@ -87,7 +87,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
|
||||
<h2 class="title">{{ activity.title }}</h2>
|
||||
<div v-if="!isFinished">
|
||||
<div v-for="(subject, index) in subjects" :key="index" class="subject">
|
||||
<div v-for="(subject, index) in visibleSubjects" :key="subject.id" class="subject">
|
||||
<p>{{index+1}}、{{ subject.title }}</p>
|
||||
<div class="tag">
|
||||
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
|
||||
@@ -98,6 +98,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<!--单选、多选-->
|
||||
<van-checkbox-group
|
||||
v-model="subject.userSelectOptionIds"
|
||||
@change="onSubjectAnswerChange"
|
||||
:ref="'subject'+subject.id"
|
||||
v-if="['radio','checkbox'].includes(subject.type)"
|
||||
>
|
||||
@@ -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 : [],
|
||||
|
||||
Reference in New Issue
Block a user