From 990126f976e106c04995f334490f1373437b9c13 Mon Sep 17 00:00:00 2001 From: dayou <853094838@qq.com> Date: Fri, 10 May 2024 22:57:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=98=BE=E7=A4=BA=E9=80=BB=E8=BE=91=20?= =?UTF-8?q?(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/surveyTemplate/templateBase.json | 3 + web/components.d.ts | 6 + web/package.json | 4 +- web/src/common/Editor/EditorV2.vue | 68 ------ web/src/common/Editor/ReadOnly.vue | 90 -------- web/src/common/logicEngine/BasicType.ts | 12 ++ web/src/common/logicEngine/RuleBuild.ts | 139 +++++++++++++ web/src/common/logicEngine/RulesMatch.ts | 194 ++++++++++++++++++ web/src/common/logicEngine/ruleConf.ts | 36 ++++ web/src/management/hooks/useQuestionInfo.ts | 23 +++ web/src/management/hooks/useShowLogicInfo.ts | 29 +++ .../pages/edit/components/QuestionWrapper.vue | 22 +- web/src/management/pages/edit/index.vue | 1 + .../modules/contentModule/PublishPanel.vue | 21 ++ .../edit/modules/contentModule/SavePanel.vue | 21 ++ .../edit/modules/contentModule/buildData.js | 3 +- .../edit/modules/generalModule/NavPanel.vue | 3 +- .../edit/modules/logicModule/RulePanel.vue | 87 ++++++++ .../logicModule/components/ConditionView.vue | 187 +++++++++++++++++ .../logicModule/components/RuleNodeView.vue | 149 ++++++++++++++ .../modules/questionModule/PreviewPanel.vue | 1 - .../settingModule/skin/SetterPanel.vue | 21 -- .../pages/edit/pages/edit/LogicEditPage.vue | 32 +++ .../edit/pages/edit/QuestionEditPage.vue | 33 +++ .../pages/edit/pages/edit/index.vue | 68 ++++++ web/src/management/router/index.ts | 27 ++- web/src/management/store/actions.js | 5 + web/src/management/store/edit/actions.js | 8 +- web/src/management/store/edit/mutations.js | 1 + web/src/management/store/edit/state.js | 5 +- web/src/management/store/index.js | 4 +- web/src/management/store/logic/index.js | 19 ++ web/src/management/store/mutations.js | 5 +- web/src/management/store/state.js | 3 +- web/src/management/utils/constant.js | 16 ++ web/src/management/utils/index.js | 3 +- .../questions/widgets/BaseChoice/index.jsx | 1 - web/src/render/App.vue | 5 +- web/src/render/components/MainRenderer.vue | 6 +- web/src/render/components/MaterialGroup.vue | 41 ++-- web/src/render/components/QuestionWrapper.vue | 114 ++++++++++ web/src/render/hooks/useShowInput.js | 30 +++ web/src/render/hooks/useShowOthers.js | 29 +++ web/src/render/hooks/useVoteMap.js | 18 ++ web/src/render/pages/IndexPage.vue | 21 +- web/src/render/store/actions.js | 51 +++++ web/src/render/store/getters.js | 135 +++++------- web/src/render/store/mutations.js | 15 +- web/src/render/store/state.js | 3 +- 49 files changed, 1511 insertions(+), 307 deletions(-) delete mode 100644 web/src/common/Editor/EditorV2.vue delete mode 100644 web/src/common/Editor/ReadOnly.vue create mode 100644 web/src/common/logicEngine/BasicType.ts create mode 100644 web/src/common/logicEngine/RuleBuild.ts create mode 100644 web/src/common/logicEngine/RulesMatch.ts create mode 100644 web/src/common/logicEngine/ruleConf.ts create mode 100644 web/src/management/hooks/useQuestionInfo.ts create mode 100644 web/src/management/hooks/useShowLogicInfo.ts create mode 100644 web/src/management/pages/edit/modules/logicModule/RulePanel.vue create mode 100644 web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue create mode 100644 web/src/management/pages/edit/modules/logicModule/components/RuleNodeView.vue create mode 100644 web/src/management/pages/edit/pages/edit/LogicEditPage.vue create mode 100644 web/src/management/pages/edit/pages/edit/QuestionEditPage.vue create mode 100644 web/src/management/pages/edit/pages/edit/index.vue create mode 100644 web/src/management/store/logic/index.js create mode 100644 web/src/render/components/QuestionWrapper.vue create mode 100644 web/src/render/hooks/useShowInput.js create mode 100644 web/src/render/hooks/useShowOthers.js create mode 100644 web/src/render/hooks/useVoteMap.js diff --git a/server/src/modules/survey/template/surveyTemplate/templateBase.json b/server/src/modules/survey/template/surveyTemplate/templateBase.json index de6debc5..56574dcc 100644 --- a/server/src/modules/survey/template/surveyTemplate/templateBase.json +++ b/server/src/modules/survey/template/surveyTemplate/templateBase.json @@ -48,5 +48,8 @@ "contentConf": { "opacity": 100 } + }, + "logicConf": { + "showLogicConf": [] } } diff --git a/web/components.d.ts b/web/components.d.ts index 411d01a0..a61892f4 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -18,6 +18,8 @@ declare module 'vue' { ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElIcon: typeof import('element-plus/es')['ElIcon'] + ElIconCheck: typeof import('@element-plus/icons-vue')['Check'] + ElIconLoading: typeof import('@element-plus/icons-vue')['Loading'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElOption: typeof import('element-plus/es')['ElOption'] @@ -44,9 +46,13 @@ declare module 'vue' { IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default'] IEpClose: typeof import('~icons/ep/close')['default'] IEpCopyDocument: typeof import('~icons/ep/copy-document')['default'] + IEpDelete: typeof import('~icons/ep/delete')['default'] IEpLoading: typeof import('~icons/ep/loading')['default'] + IEpMinus: typeof import('~icons/ep/minus')['default'] + IEpPlus: typeof import('~icons/ep/plus')['default'] IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default'] IEpRank: typeof import('~icons/ep/rank')['default'] + IEpRankMinus: typeof import('~icons/ep/rank-minus')['default'] IEpRemove: typeof import('~icons/ep/remove')['default'] IEpSearch: typeof import('~icons/ep/search')['default'] IEpSort: typeof import('~icons/ep/sort')['default'] diff --git a/web/package.json b/web/package.json index cada0c8c..063fb000 100644 --- a/web/package.json +++ b/web/package.json @@ -23,13 +23,15 @@ "element-plus": "^2.7.0", "lodash-es": "^4.17.21", "moment": "^2.29.4", + "nanoid": "^5.0.7", "node-forge": "^1.3.1", "qrcode": "^1.5.3", "vue": "^3.4.15", "vue-router": "^4.2.5", "vuedraggable": "^4.1.0", "vuex": "^4.0.2", - "xss": "^1.0.14" + "xss": "^1.0.14", + "yup": "^1.4.0" }, "devDependencies": { "@iconify-json/ep": "^1.1.15", diff --git a/web/src/common/Editor/EditorV2.vue b/web/src/common/Editor/EditorV2.vue deleted file mode 100644 index 38a4ff07..00000000 --- a/web/src/common/Editor/EditorV2.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - diff --git a/web/src/common/Editor/ReadOnly.vue b/web/src/common/Editor/ReadOnly.vue deleted file mode 100644 index 1719dc3e..00000000 --- a/web/src/common/Editor/ReadOnly.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - diff --git a/web/src/common/logicEngine/BasicType.ts b/web/src/common/logicEngine/BasicType.ts new file mode 100644 index 00000000..61d83621 --- /dev/null +++ b/web/src/common/logicEngine/BasicType.ts @@ -0,0 +1,12 @@ +export type BasicOperator = 'in' | 'eq' | 'neq' | 'nin' | 'gt'; +// in:包含, 选择了,任一 +// eq: 等于,选择了,全部 +// nin: 不包含,不选择,任一 +// neq:不等于,不选择,全部,可以实现“填写了” +export type FieldTypes = string | string[]; + +// 定义事实对象类型 +export type Fact = { + [key: string]: any; +}; + diff --git a/web/src/common/logicEngine/RuleBuild.ts b/web/src/common/logicEngine/RuleBuild.ts new file mode 100644 index 00000000..7b69d52c --- /dev/null +++ b/web/src/common/logicEngine/RuleBuild.ts @@ -0,0 +1,139 @@ +import { nanoid } from 'nanoid'; +import * as yup from 'yup' +import { type BasicOperator, type FieldTypes } from './BasicType' + +export function generateID(prefix = 'r') { + return `${prefix}-${nanoid(5)}` +} +// 定义条件规则类 +export class ConditionNode { + id: string = ''; + public field: string = ''; + public operator: BasicOperator = 'in'; + public value: FieldTypes = [] + constructor(field: string = '', operator: BasicOperator = 'in', value: FieldTypes = []) { + this.field = field; + this.operator = operator; + this.value = value; + this.id = generateID('c') + } + setField(field: string) { + this.field = field; + } + setOperator(operator: BasicOperator) { + this.operator = operator; + } + setValue(value: FieldTypes) { + this.value = value; + } +} + +export class RuleNode { + id: string = ''; + conditions: ConditionNode[] = [] + scope: string = 'question' + target: string = '' + constructor(scope:string = 'question', target: string = '') { + this.id = generateID('r') + this.scope = scope + this.target = target + } + setTarget(value: string) { + this.target = value + } + addCondition(condition: ConditionNode) { + this.conditions.push(condition); + } + removeCondition(id: string) { + this.conditions = this.conditions.filter(v => v.id !== id); + } + findCondition(conditionId: string) { + return this.conditions.find(condition => condition.id === conditionId); + } +} + +export class RuleBuild { + rules: RuleNode[] = []; + constructor() { + this.rules = []; + } + + // 添加条件规则到规则引擎中 + addRule(rule: RuleNode) { + this.rules.push(rule); + } + removeRule(ruleId: string) { + this.rules = this.rules.filter(rule => rule.id !== ruleId); + } + findRule(ruleId: string) { + return this.rules.find(rule => rule.id === ruleId); + } + toJson() { + return this.rules.map(rule => { + return { + target: rule.target, + scope: rule.scope, + conditions: rule.conditions.map(condition => { + return { + field: condition.field, + operator: condition.operator, + value: condition.value + } + }) + } + }) + } + fromJson(ruleConf: any) { + if(ruleConf instanceof Array) { + ruleConf.forEach((rule: any) => { + const { scope, target } = rule + const ruleNode = new RuleNode(scope, target); + rule.conditions.forEach((condition: any) => { + const { field, operator, value } = condition + const conditionNode = new ConditionNode(field, operator, value); + ruleNode.addCondition(conditionNode) + }) + this.addRule(ruleNode) + }) + } + return this + } + validateSchema() { + return ruleSchema.validateSync(this.toJson()) + } + // 实现目标选择了下拉框置灰效果 + findTargetsByScope(scope: string){ + return this.rules.filter(rule => rule.scope === scope).map(rule => rule.target) + } + // 实现前置题删除校验 + findTargetsByFields(field: string) { + const nodes = this.rules.filter((rule: RuleNode) => { + const conditions = rule.conditions.filter((item: any) => { + return item.field === field + }) + return conditions.length > 0 + }) + return nodes.map((item: any) => { + return item.target + }) + } + // 根据目标题获取显示逻辑 + findConditionByTarget(target: string) { + return this.rules.filter(rule=> rule.target === target).map(item => item.conditions) + } +} + + +export const ruleSchema = yup.array().of( + yup.object({ + target: yup.string().required(), + scope: yup.string().required(), + conditions: yup.array().of( + yup.object({ + field: yup.string().required(), + operator: yup.string().required(), + value: yup.array().of(yup.string().required()) + }) + ) + }) +) \ No newline at end of file diff --git a/web/src/common/logicEngine/RulesMatch.ts b/web/src/common/logicEngine/RulesMatch.ts new file mode 100644 index 00000000..d8c02343 --- /dev/null +++ b/web/src/common/logicEngine/RulesMatch.ts @@ -0,0 +1,194 @@ + +import { type BasicOperator, type FieldTypes, type Fact } from "./BasicType"; + +// 定义条件规则类 +export class ConditionNode { + // 默认显示 + public result: boolean = false; + constructor(public field: F, public operator: O, public value: FieldTypes) { + } + + // 计算条件规则的哈希值 + calculateHash(): string { + // 假设哈希值计算方法为简单的字符串拼接或其他哈希算法 + return this.field + this.operator + this.value; + } + + match(facts: Fact): boolean { + // console.log(this.calculateHash()) + // 如果该特征在事实对象中不存在,则直接返回false + if(!facts[this.field]) { + this.result = false + return this.result + } + switch (this.operator) { + case 'eq': + if(this.value instanceof Array) { + this.result = this.value.every(v => facts[this.field].includes(v)) + return this.result + } else { + this.result = facts[this.field].includes(this.value); + return this.result + } + case 'in': + if(this.value instanceof Array) { + this.result = this.value.some(v => facts[this.field].includes(v)) + return this.result + } else { + this.result = facts[this.field].includes(this.value); + return this.result + } + case 'nin': + if(this.value instanceof Array) { + this.result = this.value.some(v => !facts[this.field].includes(v)) + return this.result + } else { + this.result = facts[this.field].includes(this.value); + return this.result + } + case 'neq': + if(this.value instanceof Array) { + this.result = this.value.every(v => !facts[this.field].includes(v)) + return this.result + } else { + this.result = facts[this.field].includes(this.value); + return this.result + } + // 其他比较操作符的判断逻辑 + default: + return this.result + } + + } + + getResult() { + return this.result + } +} + +export class RuleNode { + conditions: Map>; // 使用哈希表存储条件规则对象 + public result: boolean = false; + constructor(public target: string, public scope: string) { + this.conditions = new Map(); + } + // 添加条件规则到规则引擎中 + addCondition(condition: ConditionNode) { + const hash = condition.calculateHash(); + this.conditions.set(hash, condition); + } + + // 匹配条件规则 + match(fact: Fact) { + const res = Array.from(this.conditions.entries()).every(([key, value]) => { + const res = value.match(fact) + if (res) { + return true; + } else { + return false + } + }); + this.result = res + return res + } + getResult() { + const res = Array.from(this.conditions.entries()).every(([key, value]) => { + const res = value.getResult() + return res + }) + return res + } + + // 计算条件规则的哈希值 + calculateHash(): string { + // 假设哈希值计算方法为简单的字符串拼接或其他哈希算法 + return this.target + this.scope; + } + toJson() { + return { + target: this.target, + scope: this.scope, + conditions: Object.fromEntries( + Array.from(this.conditions, ([key, value]) => [key, value.getResult()]) + ) + }; + } + +} + +export class RuleMatch { + rules: Map; + constructor(ruleConf: any) { + this.rules = new Map(); + if(ruleConf instanceof Array) { + ruleConf.forEach((rule: any) => { + const ruleNode = new RuleNode(rule.target, rule.scope); + rule.conditions.forEach((condition: any) => { + const conditionNode = new ConditionNode(condition.field, condition.operator, condition.value); + ruleNode.addCondition(conditionNode) + }); + this.addRule(ruleNode) + }) + } + + } + + // 添加条件规则到规则引擎中 + addRule(rule: RuleNode) { + const hash = rule.calculateHash(); + if (this.rules.has(hash)) { + const existRule: any = this.rules.get(hash); + existRule.conditions.forEach((item: ConditionNode) => { + rule.addCondition(item) + }) + } + + this.rules.set(hash, rule); + } + + + // 匹配条件规则 + match(target: string, scope: string, fact: Fact) { + const hash = this.calculateHash(target, scope); + + const rule = this.rules.get(hash); + if (rule) { + const result = rule.match(fact) + // this.matchCache.set(hash, result); + return result + } else { + // 默认显示 + return true + } + } + + getResult(target: string, scope: string) { + const hash = this.calculateHash(target, scope); + const rule = this.rules.get(hash); + if (rule) { + const result = rule.getResult() + return result + } else { + // 默认显示 + return true + } + } + // 计算哈希值的方法 + calculateHash(target: string, scope: string): string { + // 假设哈希值计算方法为简单的字符串拼接或其他哈希算法 + return target + scope; + } + findTargetsByField(field: string) { + const rules = new Map([...this.rules.entries()].filter(([key, value]) => { + return [...value.conditions.entries()].filter(([key, value]) => { + return value.field === field + }) + })) + return [...rules.values()].map(obj => obj.target); + } + toJson() { + return Array.from(this.rules.entries()).map(([key, value]) => { + return value.toJson() + }) + } +} diff --git a/web/src/common/logicEngine/ruleConf.ts b/web/src/common/logicEngine/ruleConf.ts new file mode 100644 index 00000000..4f979283 --- /dev/null +++ b/web/src/common/logicEngine/ruleConf.ts @@ -0,0 +1,36 @@ +// 静态数据 +export const ruleConf = [ + { + conditions: [ + { + field: 'data515', // 题目2 + operator: 'in', + value: ['115019'] + } + ], + scope: 'question', + target: 'data648' // 题目3 + }, + { + conditions: [ + { + field: 'data648', // 题目3 + operator: 'in', + value: ['106374'] + } + ], + scope: 'question', + target: 'data517' // 题目4 + }, + { + conditions: [ + { + field: 'data648', // 题目3 + operator: 'in', + value: ['106374'] + } + ], + scope: 'option', + target: 'data517-106374' // 题目4 + } +] diff --git a/web/src/management/hooks/useQuestionInfo.ts b/web/src/management/hooks/useQuestionInfo.ts new file mode 100644 index 00000000..f8c722c8 --- /dev/null +++ b/web/src/management/hooks/useQuestionInfo.ts @@ -0,0 +1,23 @@ +import { computed } from 'vue'; +import store from '@/management/store' +import { cleanRichText } from '@/common/xss' +export const useQuestionInfo = (field: string) => { + const getQuestionTitle = computed(() => { + const questionDataList = store.state.edit.schema.questionDataList + return () => { + return questionDataList.find((item: any) => item.field === field)?.title + } + }) + const getOptionTitle = computed(() => { + const questionDataList = store.state.edit.schema.questionDataList + return (value: string | Array) => { + const options = questionDataList.find((item: any) => item.field === field)?.options || [] + if(value instanceof Array) { + return options.filter((item: any) => value.includes(item.hash)).map((item: any) => cleanRichText(item.text)) + } else { + return options.filter((item: any) => item.hash === value).map((item: any) => cleanRichText(item.text)) + } + } + }) + return { getQuestionTitle, getOptionTitle } +} diff --git a/web/src/management/hooks/useShowLogicInfo.ts b/web/src/management/hooks/useShowLogicInfo.ts new file mode 100644 index 00000000..134ce5f9 --- /dev/null +++ b/web/src/management/hooks/useShowLogicInfo.ts @@ -0,0 +1,29 @@ +import { computed, unref } from 'vue'; +import { useQuestionInfo } from './useQuestionInfo' +import { flatten } from 'lodash-es' +import store from '@/management/store' +import { cleanRichText } from '@/common/xss' + +// 目标题的显示逻辑提示文案 +export const useShowLogicInfo = (field: string) => { + const hasShowLogic = computed(() => { + const logicEngine = store.state.logic.showLogicEngine + // 判断该题是否作为了显示逻辑前置题 + const isField = logicEngine?.findTargetsByFields(field)?.length > 0 + // 判断该题是否作为了显示逻辑目标题 + const isTarget = logicEngine?.findTargetsByScope(field)?.length > 0 + return isField || isTarget + }) + const getShowLogicText = computed(() => { + const logicEngine = store.state.logic.showLogicEngine + // 获取目标题的规则 + const rules = logicEngine?.findConditionByTarget(field) || [] + + const conditions = flatten(rules).map((item:any) => { + const { getQuestionTitle, getOptionTitle } = useQuestionInfo(item.field) + return `【 ${cleanRichText(getQuestionTitle.value())}】 选择了 【${getOptionTitle.value(unref(item.value)).join('、')}】 ` + }) + return conditions.length ? conditions.join('') + ' 满足以上全部,则显示本题' :'' + }) + return { hasShowLogic, getShowLogicText } +} \ No newline at end of file diff --git a/web/src/management/pages/edit/components/QuestionWrapper.vue b/web/src/management/pages/edit/components/QuestionWrapper.vue index 4ca3df14..71819add 100644 --- a/web/src/management/pages/edit/components/QuestionWrapper.vue +++ b/web/src/management/pages/edit/components/QuestionWrapper.vue @@ -6,7 +6,7 @@ @click="clickFormItem" > - + @@ -24,12 +24,14 @@ + + diff --git a/web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue b/web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue new file mode 100644 index 00000000..9b48af63 --- /dev/null +++ b/web/src/management/pages/edit/modules/logicModule/components/ConditionView.vue @@ -0,0 +1,187 @@ + + + 如果 + + handleChange(conditionNode, 'field', val)" + > + + + + + 选择了 + + handleChange(conditionNode, 'value', val)" + > + + + + + 中的任一选项 + + + handleDelete(conditionNode.id)" + /> + + + + + diff --git a/web/src/management/pages/edit/modules/logicModule/components/RuleNodeView.vue b/web/src/management/pages/edit/modules/logicModule/components/RuleNodeView.vue new file mode 100644 index 00000000..01668941 --- /dev/null +++ b/web/src/management/pages/edit/modules/logicModule/components/RuleNodeView.vue @@ -0,0 +1,149 @@ + + + + + + + 则显示 + + handleChange(ruleNode, 'target', val)" + > + + + + + + handleDelete(ruleNode.id)" /> + + + + + + diff --git a/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue b/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue index 6479d35f..e4dfa24d 100644 --- a/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue +++ b/web/src/management/pages/edit/modules/questionModule/PreviewPanel.vue @@ -184,7 +184,6 @@ export default { .operation-wrapper { margin-top: 50px; margin-bottom: 45px; - // min-height: 812px; overflow-x: hidden; overflow-y: auto; padding-right: 30px; diff --git a/web/src/management/pages/edit/modules/settingModule/skin/SetterPanel.vue b/web/src/management/pages/edit/modules/settingModule/skin/SetterPanel.vue index a740ca64..d5314353 100644 --- a/web/src/management/pages/edit/modules/settingModule/skin/SetterPanel.vue +++ b/web/src/management/pages/edit/modules/settingModule/skin/SetterPanel.vue @@ -95,25 +95,4 @@ export default { padding: 0 !important; } } -.no-select-question { - padding-top: 125px; - display: flex; - flex-direction: column; - align-items: center; - - img { - width: 160px; - padding: 25px; - } - - .tip { - font-size: 14px; - color: $normal-color; - letter-spacing: 0; - } -} - -.question-config-form { - padding: 30px 20px 50px 20px; -} diff --git a/web/src/management/pages/edit/pages/edit/LogicEditPage.vue b/web/src/management/pages/edit/pages/edit/LogicEditPage.vue new file mode 100644 index 00000000..9c2fc470 --- /dev/null +++ b/web/src/management/pages/edit/pages/edit/LogicEditPage.vue @@ -0,0 +1,32 @@ + + + + + + + diff --git a/web/src/management/pages/edit/pages/edit/QuestionEditPage.vue b/web/src/management/pages/edit/pages/edit/QuestionEditPage.vue new file mode 100644 index 00000000..a4649e8f --- /dev/null +++ b/web/src/management/pages/edit/pages/edit/QuestionEditPage.vue @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/management/pages/edit/pages/edit/index.vue b/web/src/management/pages/edit/pages/edit/index.vue new file mode 100644 index 00000000..61915934 --- /dev/null +++ b/web/src/management/pages/edit/pages/edit/index.vue @@ -0,0 +1,68 @@ + + + + + + + + + + + + diff --git a/web/src/management/router/index.ts b/web/src/management/router/index.ts index 0492b0c6..77492e31 100644 --- a/web/src/management/router/index.ts +++ b/web/src/management/router/index.ts @@ -21,15 +21,37 @@ const routes: RouteRecordRaw[] = [ meta: { needLogin: true }, + name: 'QuestionEdit', component: () => import('../pages/edit/index.vue'), children: [ { path: '', - name: 'QuestionEditIndex', meta: { needLogin: true }, - component: () => import('../pages/edit/pages/EditPage.vue') + name: 'QuestionEditPage', + component: () => + import('../pages/edit/pages/edit/index.vue'), + children: [ + { + path: '', + name: 'QuestionEditIndex', + meta: { + needLogin: true + }, + component: () => + import('../pages/edit/pages/edit/QuestionEditPage.vue') + }, + { + path: 'logic', + name: 'LogicIndex', + meta: { + needLogin: true + }, + component: () => + import('../pages/edit/pages/edit/LogicEditPage.vue') + } + ] }, { path: 'setting', @@ -41,7 +63,6 @@ const routes: RouteRecordRaw[] = [ }, { path: 'skin', - // name: 'SkinSetting', meta: { needLogin: true }, diff --git a/web/src/management/store/actions.js b/web/src/management/store/actions.js index 91071b4c..5e8071b1 100644 --- a/web/src/management/store/actions.js +++ b/web/src/management/store/actions.js @@ -1,4 +1,5 @@ import { getBannerData } from '@/management/api/skin.js' +import { RuleBuild } from '@/common/logicEngine/RuleBuild' export default { async getBannerData({ state, commit }) { @@ -9,5 +10,9 @@ export default { if (res.code === 200) { commit('setBannerList', res.data) } + }, + initShowLogic({ commit }, ruleConf) { + const showLogicEngine = new RuleBuild(ruleConf) + commit('setShowLogicEngine', showLogicEngine) } } diff --git a/web/src/management/store/edit/actions.js b/web/src/management/store/edit/actions.js index 771917bd..b9d91509 100644 --- a/web/src/management/store/edit/actions.js +++ b/web/src/management/store/edit/actions.js @@ -10,12 +10,12 @@ export default { } dispatch('resetState') }, - async getSchemaFromRemote({ commit, state }) { + async getSchemaFromRemote({ commit, state, dispatch }) { const res = await getSurveyById(state.surveyId) if (res.code === 200) { const metaData = res.data.surveyMetaRes document.title = metaData.title - const { bannerConf, bottomConf, skinConf, baseConf, submitConf, dataConf } = + const { bannerConf, bottomConf, skinConf, baseConf, submitConf, dataConf, logicConf = {} } = res.data.surveyConfRes.code commit('initSchema', { metaData, @@ -25,9 +25,11 @@ export default { skinConf, baseConf, submitConf, - questionDataList: dataConf.dataList + questionDataList: dataConf.dataList, + logicConf } }) + } else { throw new Error(res.errmsg || '问卷不存在') } diff --git a/web/src/management/store/edit/mutations.js b/web/src/management/store/edit/mutations.js index ea62db2f..fab89077 100644 --- a/web/src/management/store/edit/mutations.js +++ b/web/src/management/store/edit/mutations.js @@ -18,6 +18,7 @@ export default { state.schema.baseConf = _merge({}, state.schema.baseConf, codeData.baseConf) state.schema.submitConf = _merge({}, state.schema.submitConf, codeData.submitConf) state.schema.questionDataList = codeData.questionDataList || [] + state.schema.logicConf = codeData.logicConf }, setSurveyId(state, data) { state.surveyId = data diff --git a/web/src/management/store/edit/state.js b/web/src/management/store/edit/state.js index 1175f2f7..1927e24a 100644 --- a/web/src/management/store/edit/state.js +++ b/web/src/management/store/edit/state.js @@ -52,6 +52,9 @@ export default { }, link: '' }, - questionDataList: [] + questionDataList: [], + logicConf: { + showLogicConf: [] + } } } diff --git a/web/src/management/store/index.js b/web/src/management/store/index.js index 69d22b47..a55831ed 100644 --- a/web/src/management/store/index.js +++ b/web/src/management/store/index.js @@ -1,6 +1,7 @@ import { createStore } from 'vuex' import edit from './edit' import user from './user' +import logic from './logic' import actions from './actions' import mutations from './mutations' @@ -13,6 +14,7 @@ export default createStore({ actions, modules: { edit, - user + user, + logic } }) diff --git a/web/src/management/store/logic/index.js b/web/src/management/store/logic/index.js new file mode 100644 index 00000000..723cbe43 --- /dev/null +++ b/web/src/management/store/logic/index.js @@ -0,0 +1,19 @@ +import { RuleBuild } from '@/common/logicEngine/RuleBuild' + +export default { + namespaced: true, + state: { + showLogicEngine: null + }, + mutations: { + setShowLogicEngine(state, logicEngine) { + state.showLogicEngine = logicEngine + } + }, + actions: { + initShowLogic({ commit }, ruleConf) { + const showLogicEngine = new RuleBuild().fromJson(ruleConf) + commit('setShowLogicEngine', showLogicEngine) + } + } +} diff --git a/web/src/management/store/mutations.js b/web/src/management/store/mutations.js index 4995fdc0..f579f2ac 100644 --- a/web/src/management/store/mutations.js +++ b/web/src/management/store/mutations.js @@ -1,5 +1,8 @@ export default { setBannerList(state, data) { state.bannerList = data - } + }, + setShowLogicEngine(state, logicEngine) { + state.logicEngine = logicEngine + }, } diff --git a/web/src/management/store/state.js b/web/src/management/store/state.js index 54246623..1ead5655 100644 --- a/web/src/management/store/state.js +++ b/web/src/management/store/state.js @@ -1,3 +1,4 @@ export default { - bannerList: [] + bannerList: [], + logicEngine: null } diff --git a/web/src/management/utils/constant.js b/web/src/management/utils/constant.js index e591a75b..1dbb1b41 100644 --- a/web/src/management/utils/constant.js +++ b/web/src/management/utils/constant.js @@ -3,3 +3,19 @@ export const QOP_MAP = { COPY: 'copy', EDIT: 'edit' } +export const qAbleList = [, + 'radio', + 'checkbox', + 'binary-choice', + 'vote', +] +export const operatorOptions = [ + { + label: '选择了', + value: 'in', + }, + { + label: '不选择', + value: 'nin', + }, +] \ No newline at end of file diff --git a/web/src/management/utils/index.js b/web/src/management/utils/index.js index d756bd8a..170e8d98 100644 --- a/web/src/management/utils/index.js +++ b/web/src/management/utils/index.js @@ -1,12 +1,11 @@ import { defaultQuestionConfig } from '../config/questionConfig' import { cloneDeep as _cloneDeep, map as _map } from 'lodash-es' - const generateQuestionField = () => { const num = Math.floor(Math.random() * 1000) return `data${num}` } -function getRandom(len) { +export function getRandom(len) { return Math.random() .toString() .slice(len && typeof len === 'number' ? 0 - len : -6) diff --git a/web/src/materials/questions/widgets/BaseChoice/index.jsx b/web/src/materials/questions/widgets/BaseChoice/index.jsx index b4322568..efc6d270 100644 --- a/web/src/materials/questions/widgets/BaseChoice/index.jsx +++ b/web/src/materials/questions/widgets/BaseChoice/index.jsx @@ -80,7 +80,6 @@ export default defineComponent({ } } emit('change', values) - // return values } return { slots, diff --git a/web/src/render/App.vue b/web/src/render/App.vue index 563cecb1..1e3b24cd 100644 --- a/web/src/render/App.vue +++ b/web/src/render/App.vue @@ -17,6 +17,7 @@ import AlertDialog from './components/AlertDialog.vue' import LogoIcon from './components/LogoIcon.vue' import { get as _get } from 'lodash-es' +import { ruleConf } from '@/common/logicEngine/ruleConf' export default { name: 'App', @@ -55,7 +56,7 @@ export default { const res = await getPublishedSurveyInfo({ surveyPath }) if (res.code === 200) { const data = res.data - const { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf } = data.code + const { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf, logicConf } = data.code document.title = data.title const questionData = { bannerConf, @@ -68,6 +69,7 @@ export default { this.setSkin(skinConf) this.$store.commit('setSurveyPath', surveyPath) this.$store.dispatch('init', questionData) + this.$store.dispatch('initRuleEngine', logicConf?.showLogicConf); this.$store.dispatch('getEncryptInfo') } else { throw new Error(res.errmsg) @@ -90,7 +92,6 @@ export default { root.style.setProperty('--primary-background-color', backgroundConf?.color) // 设置背景颜色 } if (contentConf?.opacity.toString()) { - console.log({ opacity: contentConf?.opacity / 100 }) root.style.setProperty('--opacity', contentConf?.opacity / 100) // 设置全局透明度 } } diff --git a/web/src/render/components/MainRenderer.vue b/web/src/render/components/MainRenderer.vue index 38895a07..1a2ab8d6 100644 --- a/web/src/render/components/MainRenderer.vue +++ b/web/src/render/components/MainRenderer.vue @@ -5,7 +5,7 @@ ref="formGroup" :render-data="item" :rules="rules" - :formModel="formModel" + :formValues="formValues" @formChange="changeData" /> @@ -27,8 +27,8 @@ export default { rules() { return this.$store.state.rules }, - formModel() { - return this.$store.getters.formModel + formValues() { + return this.$store.state.formValues } }, mounted() {}, diff --git a/web/src/render/components/MaterialGroup.vue b/web/src/render/components/MaterialGroup.vue index 98ab9a5a..c4fd125c 100644 --- a/web/src/render/components/MaterialGroup.vue +++ b/web/src/render/components/MaterialGroup.vue @@ -1,24 +1,23 @@ - - + + + + + diff --git a/web/src/render/hooks/useShowInput.js b/web/src/render/hooks/useShowInput.js new file mode 100644 index 00000000..2dc2df82 --- /dev/null +++ b/web/src/render/hooks/useShowInput.js @@ -0,0 +1,30 @@ +import store from '../store/index' +export const useShowInput = (questionKey) => { + const formValues = store.state.formValues + const questionVal = formValues[questionKey] + let rangeConfig = store.state.questionData[questionKey].rangeConfig + let othersValue = {} + if (rangeConfig && Object.keys(rangeConfig).length > 0) { + for(let key in rangeConfig) { + const curRange = rangeConfig[key] + if (curRange.isShowInput) { + const rangeKey = `${questionKey}_${key}` + othersValue[rangeKey] = formValues[rangeKey] + + curRange.othersKey = rangeKey, + curRange.othersValue = formValues[rangeKey] + if(!questionVal.toString().includes(key) && formValues[rangeKey]) { + // 如果分值被未被选中且对应的填写更多有值,则清空填写更多 + const data = { + key: rangeKey, + value: '' + } + store.commit('changeFormData', data) + } + } + } + } + + + return { rangeConfig, othersValue } +} \ No newline at end of file diff --git a/web/src/render/hooks/useShowOthers.js b/web/src/render/hooks/useShowOthers.js new file mode 100644 index 00000000..9323326f --- /dev/null +++ b/web/src/render/hooks/useShowOthers.js @@ -0,0 +1,29 @@ +import store from '../store/index' +export const useShowOthers = (questionKey) => { + const formValues = store.state.formValues + const questionVal = formValues[questionKey] + let othersValue = {} + let options = store.state.questionData[questionKey].options.map(optionItem => { + if (optionItem.others) { + const opKey = `${questionKey}_${optionItem.hash}` + othersValue[opKey] = formValues[opKey] + if(!questionVal.includes(optionItem.hash) && formValues[opKey]) { + // 如果选项被未被选中且对应的填写更多有值,则清空填写更多 + const data = { + key: opKey, + value: '' + } + store.commit('changeFormData', data) + } + return { + ...optionItem, + othersKey: opKey, + othersValue: formValues[opKey] + } + } else { + return optionItem + } + }) + + return { options, othersValue } +} \ No newline at end of file diff --git a/web/src/render/hooks/useVoteMap.js b/web/src/render/hooks/useVoteMap.js new file mode 100644 index 00000000..14bd96b4 --- /dev/null +++ b/web/src/render/hooks/useVoteMap.js @@ -0,0 +1,18 @@ +import { computed } from 'vue' +import store from '../store/index' +export const useVoteMap = (questionKey) => { + + let voteTotal = store.state.voteMap?.[questionKey]?.total || 0 + + const options = store.state.questionData[questionKey].options.map(option => { + const optionHash = option.hash + const voteCount = store.state.voteMap?.[questionKey]?.[optionHash] || 0 + + return { + ...option, + voteCount + } + }) + + return { options, voteTotal } +} \ No newline at end of file diff --git a/web/src/render/pages/IndexPage.vue b/web/src/render/pages/IndexPage.vue index 296596ec..19a15d9a 100644 --- a/web/src/render/pages/IndexPage.vue +++ b/web/src/render/pages/IndexPage.vue @@ -27,6 +27,7 @@ import { submitForm } from '../api/survey' import encrypt from '../utils/encrypt' import useCommandComponent from '../hooks/useCommandComponent' +import { cloneDeep } from 'lodash-es' export default { name: 'indexPage', @@ -49,9 +50,6 @@ export default { LogoIcon }, computed: { - formModel() { - return this.$store.getters.formModel - }, confirmAgain() { return this.$store.state.submitConf.confirmAgain }, @@ -94,9 +92,23 @@ export default { } }, getSubmitData() { + const formValues = cloneDeep(this.$store.state.formValues) + // 填写更多-处理提交数据 + + + + // 显示逻辑-处理提交数据 + const formModel = Object.keys(formValues) + .filter(key => this.$store.state.ruleEngine.getResult(key, 'question')) + .reduce((obj, key) => { + obj[key] = formValues[key]; + return obj; + }, {}); + + const result = { surveyPath: this.surveyPath, - data: JSON.stringify(this.formModel), + data: JSON.stringify(formModel), difTime: Date.now() - this.$store.state.enterTime, clientTime: Date.now() } @@ -118,6 +130,7 @@ export default { async submitForm() { try { const submitData = this.getSubmitData() + const res = await submitForm(submitData) if (res.code === 200) { this.$store.commit('setRouter', 'successPage') diff --git a/web/src/render/store/actions.js b/web/src/render/store/actions.js index 50dec853..3438ebea 100644 --- a/web/src/render/store/actions.js +++ b/web/src/render/store/actions.js @@ -5,6 +5,7 @@ import 'moment/locale/zh-cn' moment.locale('zh-cn') import adapter from '../adapter' import { queryVote, getEncryptInfo } from '@/render/api/survey' +import { RuleMatch } from '@/common/logicEngine/RulesMatch' /** * CODE_MAP不从management引入,在dev阶段,会导致B端 router被加载,进而导致C端路由被添加 baseUrl: /management */ @@ -13,6 +14,7 @@ const CODE_MAP = { ERROR: 500, NO_AUTH: 403 } +const VOTE_INFO_KEY = 'voteinfo' export default { // 初始化 @@ -101,18 +103,63 @@ export default { return } try { + localStorage.removeItem(VOTE_INFO_KEY) const voteRes = await queryVote({ surveyPath, fieldList: fieldList.join(',') }) if (voteRes.code === 200) { + localStorage.setItem( + VOTE_INFO_KEY, + JSON.stringify({ + ...voteRes.data + }) + ) commit('setVoteMap', voteRes.data) } } catch (error) { console.log(error) } }, + updateVoteData({ state, commit }, data) { + const { key: questionKey, value: questionVal } = data + // 更新前获取接口缓存在localStorage中的数据 + const localData = localStorage.getItem(VOTE_INFO_KEY) + const voteinfo = JSON.parse(localData) + const currentQuestion = state.questionData[questionKey] + const options = currentQuestion.options + const voteTotal = voteinfo?.[questionKey]?.total || 0 + let totalPayload = { + questionKey, + voteKey: 'total', + voteValue: voteTotal + } + options.forEach((option) => { + const optionhash = option.hash + const voteCount = voteinfo?.[questionKey]?.[optionhash] || 0 + // 如果选中值包含该选项,对应voteCount 和 voteTotal + 1 + if ( + Array.isArray(questionVal) ? questionVal.includes(optionhash) : questionVal === optionhash + ) { + const countPayload = { + questionKey, + voteKey: optionhash, + voteValue: voteCount + 1 + } + totalPayload.voteValue += 1 + commit('updateVoteMapByKey', countPayload) + } else { + const countPayload = { + questionKey, + voteKey: optionhash, + voteValue: voteCount + } + commit('updateVoteMapByKey', countPayload) + } + commit('updateVoteMapByKey', totalPayload) + }) + }, async getEncryptInfo({ commit }) { try { const res = await getEncryptInfo() @@ -122,5 +169,9 @@ export default { } catch (error) { console.log(error) } + }, + async initRuleEngine({ commit }, ruleConf) { + const ruleEngine = new RuleMatch(ruleConf) + commit('setRuleEgine', ruleEngine) } } diff --git a/web/src/render/store/getters.js b/web/src/render/store/getters.js index 9764b6b3..23ceb6ef 100644 --- a/web/src/render/store/getters.js +++ b/web/src/render/store/getters.js @@ -3,115 +3,92 @@ import { flatten } from 'lodash-es' export default { // 题目列表 renderData: (state) => { - const { questionSeq, questionData, formValues } = state + const { questionSeq, questionData } = state + let index = 1 return ( questionSeq && questionSeq.reduce((pre, item) => { const questionArr = [] - for (const questionKey of item) { + + item.forEach(questionKey => { + console.log('题目重新计算') const question = { ...questionData[questionKey] } + const { type, extraOptions, options, rangeConfig } = question - const questionVal = formValues[questionKey] + // const questionVal = formValues[questionKey] - question.value = questionVal + // question.value = questionVal // 本题开启了 if (question.showIndex) { question.indexNumber = index++ } - const allOptions = [] - if (Array.isArray(extraOptions)) { - allOptions.push(...extraOptions) - } - if (Array.isArray(options)) { - allOptions.push(...options) - } + // const allOptions = [] + // if (Array.isArray(extraOptions)) { + // allOptions.push(...extraOptions) + // } + // if (Array.isArray(options)) { + // allOptions.push(...options) + // } - let othersValue = {} - let voteTotal = 0 - const voteMap = state.voteMap - if (/vote/.test(type)) { - voteTotal = voteMap?.[questionKey]?.total || 0 - } + // let othersValue = {} + // let voteTotal = 0 + // const voteMap = state.voteMap + // if (/vote/.test(type)) { + // voteTotal = voteMap?.[questionKey]?.total || 0 + // } // 遍历所有的选项 - for (const optionItem of allOptions) { + // for (const optionItem of allOptions) { // 开启了更多输入框,生成othersValue的值 - if (optionItem.others) { - const opKey = `${questionKey}_${optionItem.hash}` - optionItem.othersKey = opKey - optionItem.othersValue = formValues[opKey] - othersValue[opKey] = formValues[opKey] - } + // if (optionItem.others) { + // const opKey = `${questionKey}_${optionItem.hash}` + // optionItem.othersKey = opKey + // optionItem.othersValue = formValues[opKey] + // othersValue[opKey] = formValues[opKey] + // } // 投票题,用户手动选择选项后,要实时更新展示数据和进度 - if (/vote/.test(type)) { - const voteCount = voteMap?.[questionKey]?.[optionItem.hash] || 0 - if ( - Array.isArray(questionVal) - ? questionVal.includes(optionItem.hash) - : questionVal === optionItem.hash - ) { - optionItem.voteCount = voteCount + 1 - voteTotal = voteTotal + 1 - } else { - optionItem.voteCount = voteCount - } - question.voteTotal = voteTotal - } - } + // if (/vote/.test(type)) { + // const voteCount = voteMap?.[questionKey]?.[optionItem.hash] || 0 + // if ( + // Array.isArray(questionVal) + // ? questionVal.includes(optionItem.hash) + // : questionVal === optionItem.hash + // ) { + // optionItem.voteCount = voteCount + 1 + // voteTotal = voteTotal + 1 + // } else { + // optionItem.voteCount = voteCount + // } + // question.voteTotal = voteTotal + // } + // } // 开启了更多输入框,要将当前的value赋值给question - if (rangeConfig && Object.keys(rangeConfig).length > 0 && rangeConfig[questionVal]) { - const curRange = rangeConfig[questionVal] - if (curRange?.isShowInput) { - const rangeKey = `${questionKey}_${questionVal}` - curRange.othersKey = rangeKey - curRange.othersValue = formValues[rangeKey] - othersValue[rangeKey] = formValues[rangeKey] - } - } + // if (rangeConfig && Object.keys(rangeConfig).length > 0 && rangeConfig[questionVal]) { + // const curRange = rangeConfig[questionVal] + // if (curRange?.isShowInput) { + // const rangeKey = `${questionKey}_${questionVal}` + // curRange.othersKey = rangeKey + // curRange.othersValue = formValues[rangeKey] + // othersValue[rangeKey] = formValues[rangeKey] + // } + // } // 将othersValue赋值给 - question.othersValue = othersValue + // question.othersValue = othersValue + questionArr.push(question) - } + }) if (questionArr && questionArr.length) { pre.push(questionArr) } + return pre }, []) ) - }, - // 根据渲染的题目生成的用户输入或者选择的数据 - formModel: (state, getters) => { - const { renderData } = getters - const formdata = flatten(renderData).reduce((pre, current) => { - const { othersValue, type, field } = current - if (othersValue && Object.keys(othersValue).length) { - Object.assign(pre, othersValue) - } - switch (type) { - // case 'fillin': - // current.fillinConfig.forEach(item => { - // item.forEach(subItem => { - // if (subItem.blanks > 0) { - // const resultField = `${field}_${subItem.hash}` - // Object.assign(pre, { [resultField]: subItem.value }) - // } - // }) - // }) - // Object.assign(pre, { [field]: formValues[field] }) - // break - default: - Object.assign(pre, { [field]: current.value }) - break - } - - return pre - }, {}) - return formdata } } diff --git a/web/src/render/store/mutations.js b/web/src/render/store/mutations.js index d88fb2a1..3362123e 100644 --- a/web/src/render/store/mutations.js +++ b/web/src/render/store/mutations.js @@ -20,8 +20,8 @@ export default { }, changeFormData(state, data) { let { key, value } = data + // console.log('formValues', key, value) set(state, `formValues.${key}`, value) - // set(state, `questionData.${key}.value`, value) }, changeSelectMoreData(state, data) { const { key, value, field } = data @@ -36,10 +36,21 @@ export default { setVoteMap(state, data) { state.voteMap = data }, + updateVoteMapByKey(state, data) { + const { questionKey, voteKey, voteValue } = data + // 兼容为空的情况 + if(!state.voteMap[questionKey]){ + state.voteMap[questionKey] = {} + } + state.voteMap[questionKey][voteKey] = voteValue + }, setQuestionSeq(state, data) { state.questionSeq = data }, setEncryptInfo(state, data) { state.encryptInfo = data - } + }, + setRuleEgine(state, ruleEngine) { + state.ruleEngine = ruleEngine + }, } diff --git a/web/src/render/store/state.js b/web/src/render/store/state.js index ac3f523f..233de0d3 100644 --- a/web/src/render/store/state.js +++ b/web/src/render/store/state.js @@ -12,5 +12,6 @@ export default { enterTime: null, questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]] voteMap: {}, - encryptInfo: null + encryptInfo: null, + ruleEngine: null }