feat: 显示逻辑 (#107)
This commit is contained in:
parent
412fc75cfe
commit
990126f976
@ -48,5 +48,8 @@
|
|||||||
"contentConf": {
|
"contentConf": {
|
||||||
"opacity": 100
|
"opacity": 100
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"logicConf": {
|
||||||
|
"showLogicConf": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
web/components.d.ts
vendored
6
web/components.d.ts
vendored
@ -18,6 +18,8 @@ declare module 'vue' {
|
|||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
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']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
@ -44,9 +46,13 @@ declare module 'vue' {
|
|||||||
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
|
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
|
||||||
IEpClose: typeof import('~icons/ep/close')['default']
|
IEpClose: typeof import('~icons/ep/close')['default']
|
||||||
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
||||||
|
IEpDelete: typeof import('~icons/ep/delete')['default']
|
||||||
IEpLoading: typeof import('~icons/ep/loading')['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']
|
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
|
||||||
IEpRank: typeof import('~icons/ep/rank')['default']
|
IEpRank: typeof import('~icons/ep/rank')['default']
|
||||||
|
IEpRankMinus: typeof import('~icons/ep/rank-minus')['default']
|
||||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
IEpRemove: typeof import('~icons/ep/remove')['default']
|
||||||
IEpSearch: typeof import('~icons/ep/search')['default']
|
IEpSearch: typeof import('~icons/ep/search')['default']
|
||||||
IEpSort: typeof import('~icons/ep/sort')['default']
|
IEpSort: typeof import('~icons/ep/sort')['default']
|
||||||
|
@ -23,13 +23,15 @@
|
|||||||
"element-plus": "^2.7.0",
|
"element-plus": "^2.7.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"vue": "^3.4.15",
|
"vue": "^3.4.15",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuex": "^4.0.2",
|
"vuex": "^4.0.2",
|
||||||
"xss": "^1.0.14"
|
"xss": "^1.0.14",
|
||||||
|
"yup": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/ep": "^1.1.15",
|
"@iconify-json/ep": "^1.1.15",
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="editor-v2">
|
|
||||||
<RichEditor :modelValue="realData" @input="handleChange" @blur="handleBlur" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import RichEditor from './RichEditor.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
RichEditor
|
|
||||||
// ReadOnly,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
realData: {
|
|
||||||
type: String,
|
|
||||||
default: () => ''
|
|
||||||
},
|
|
||||||
questionDataList: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
watch: {},
|
|
||||||
unmounted() {
|
|
||||||
this.$emit('onDestroy')
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleChange(v) {
|
|
||||||
this.$emit('change', v)
|
|
||||||
},
|
|
||||||
handleBlur(v) {
|
|
||||||
this.$emit('blur', v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.editor-v2 {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
.operation {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
left: 8px;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.RichEditor {
|
|
||||||
width: 95.5%;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 3px;
|
|
||||||
margin-left: -7px;
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
margin-left: 4px;
|
|
||||||
font-size: 17px;
|
|
||||||
color: #888;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.icon.delete:hover {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,90 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="read-only" :style="getStyle">
|
|
||||||
<div v-html="getHtml"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import { filterXSS } from '@/common/xss'
|
|
||||||
export default {
|
|
||||||
name: 'ReadOnly',
|
|
||||||
props: {
|
|
||||||
realData: {
|
|
||||||
type: String,
|
|
||||||
default: () => ''
|
|
||||||
},
|
|
||||||
viewData: {
|
|
||||||
type: String,
|
|
||||||
default: () => ''
|
|
||||||
},
|
|
||||||
tag: {
|
|
||||||
tyle: String,
|
|
||||||
default: () => ''
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
tyle: Boolean,
|
|
||||||
default: () => false
|
|
||||||
},
|
|
||||||
defaultStyle: {
|
|
||||||
tyle: Boolean,
|
|
||||||
default: () => false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
tagHtml() {
|
|
||||||
return this.tag
|
|
||||||
? `<span contenteditable="false" style="
|
|
||||||
border: 1px solid #fa881a;
|
|
||||||
font-size: 0.18rem;
|
|
||||||
height: 0.4rem;
|
|
||||||
line-height: 0.4rem;
|
|
||||||
display: inline-block;
|
|
||||||
color: #fa881a;
|
|
||||||
padding: 0 0.16rem;
|
|
||||||
margin-left: 0.16rem;
|
|
||||||
border-radius: 0 0.06rem;
|
|
||||||
background: rgba(250,136,26,0.1);
|
|
||||||
">${this.tag}</span>`
|
|
||||||
: ''
|
|
||||||
},
|
|
||||||
getHtml() {
|
|
||||||
const title = filterXSS(this.viewData)
|
|
||||||
if (!this.tag) return title
|
|
||||||
let html = this.isRichText(title) ? title : `<p>${title}</p>`
|
|
||||||
const index = html.lastIndexOf('</p>')
|
|
||||||
if (this.viewData.indexOf(this.tagHtml) < 0) {
|
|
||||||
html = html.slice(0, index) + this.tagHtml + html.slice(index)
|
|
||||||
}
|
|
||||||
return html
|
|
||||||
},
|
|
||||||
getStyle() {
|
|
||||||
let style = ''
|
|
||||||
if (this.border) {
|
|
||||||
style += 'border:1px solid #c8c9cd;padding:10px;'
|
|
||||||
}
|
|
||||||
if (this.defaultStyle) {
|
|
||||||
style += 'color: #6e707c;font-size: 12px;'
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
this.$emit('onDestroy')
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isRichText(str) {
|
|
||||||
return /^<p[\s\S]*>[\s\S]*<\/p>$/.test(str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.read-only {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 20px;
|
|
||||||
padding: 0 10px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
12
web/src/common/logicEngine/BasicType.ts
Normal file
12
web/src/common/logicEngine/BasicType.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
|
139
web/src/common/logicEngine/RuleBuild.ts
Normal file
139
web/src/common/logicEngine/RuleBuild.ts
Normal file
@ -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())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
194
web/src/common/logicEngine/RulesMatch.ts
Normal file
194
web/src/common/logicEngine/RulesMatch.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
|
||||||
|
import { type BasicOperator, type FieldTypes, type Fact } from "./BasicType";
|
||||||
|
|
||||||
|
// 定义条件规则类
|
||||||
|
export class ConditionNode<F extends string, O extends BasicOperator> {
|
||||||
|
// 默认显示
|
||||||
|
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<string, ConditionNode<string, BasicOperator>>; // 使用哈希表存储条件规则对象
|
||||||
|
public result: boolean = false;
|
||||||
|
constructor(public target: string, public scope: string) {
|
||||||
|
this.conditions = new Map();
|
||||||
|
}
|
||||||
|
// 添加条件规则到规则引擎中
|
||||||
|
addCondition(condition: ConditionNode<string, BasicOperator>) {
|
||||||
|
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<string, RuleNode>;
|
||||||
|
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<string, BasicOperator>) => {
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
36
web/src/common/logicEngine/ruleConf.ts
Normal file
36
web/src/common/logicEngine/ruleConf.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
23
web/src/management/hooks/useQuestionInfo.ts
Normal file
23
web/src/management/hooks/useQuestionInfo.ts
Normal file
@ -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<string>) => {
|
||||||
|
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 }
|
||||||
|
}
|
29
web/src/management/hooks/useShowLogicInfo.ts
Normal file
29
web/src/management/hooks/useShowLogicInfo.ts
Normal file
@ -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 `<span>【 ${cleanRichText(getQuestionTitle.value())}】 选择了 【${getOptionTitle.value(unref(item.value)).join('、')}】</span> <br/>`
|
||||||
|
})
|
||||||
|
return conditions.length ? conditions.join('') + '<span> 满足以上全部,则显示本题</span>' :''
|
||||||
|
})
|
||||||
|
return { hasShowLogic, getShowLogicText }
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
@click="clickFormItem"
|
@click="clickFormItem"
|
||||||
>
|
>
|
||||||
<div><slot v-if="moduleConfig.type !== 'section'"></slot></div>
|
<div><slot v-if="moduleConfig.type !== 'section'"></slot></div>
|
||||||
|
|
||||||
<div :class="[showHover ? 'visibily' : 'hidden', 'hoverItem']">
|
<div :class="[showHover ? 'visibily' : 'hidden', 'hoverItem']">
|
||||||
<div class="item el-icon-rank" @click.stop.prevent="onMove">
|
<div class="item el-icon-rank" @click.stop.prevent="onMove">
|
||||||
<i-ep-rank />
|
<i-ep-rank />
|
||||||
@ -24,12 +24,14 @@
|
|||||||
<i-ep-close />
|
<i-ep-close />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="logic-text" v-html="getShowLogicText"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, unref } from 'vue'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
|
import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
qIndex: {
|
qIndex: {
|
||||||
@ -57,6 +59,8 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
const emit = defineEmits(['changeSeq', 'select'])
|
const emit = defineEmits(['changeSeq', 'select'])
|
||||||
|
|
||||||
|
const { getShowLogicText, hasShowLogic } = useShowLogicInfo(props.moduleConfig.field)
|
||||||
|
|
||||||
const isHover = ref(false)
|
const isHover = ref(false)
|
||||||
|
|
||||||
const itemClass = computed(() => {
|
const itemClass = computed(() => {
|
||||||
@ -111,6 +115,14 @@ const onMoveDown = () => {
|
|||||||
isHover.value = false
|
isHover.value = false
|
||||||
}
|
}
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
|
// const target = store.state.logic.showLogicEngine.findTargetsByFields(props.moduleConfig.field)
|
||||||
|
if(unref(hasShowLogic)) {
|
||||||
|
ElMessageBox.alert('该问题被逻辑依赖,请先删除逻辑依赖', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('本次操作会影响数据统计查看,是否确认删除?', '提示', {
|
await ElMessageBox.confirm('本次操作会影响数据统计查看,是否确认删除?', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@ -178,5 +190,11 @@ const onMove = () => {}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.logic-text{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c8c9cd;
|
||||||
|
padding: 0 .4rem;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -34,6 +34,7 @@ export default {
|
|||||||
this.$store.commit('edit/setSurveyId', this.$route.params.id)
|
this.$store.commit('edit/setSurveyId', this.$route.params.id)
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('edit/init')
|
await this.$store.dispatch('edit/init')
|
||||||
|
await this.$store.dispatch('logic/initShowLogic', this.$store.state.edit.schema.logicConf.showLogicConf || {})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error.message)
|
ElMessage.error(error.message)
|
||||||
// 自动跳转回列表页
|
// 自动跳转回列表页
|
||||||
|
@ -29,6 +29,12 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async onPublish() {
|
async onPublish() {
|
||||||
|
try {
|
||||||
|
this.updateLogicConf()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('请检查逻辑配置是否有误')
|
||||||
|
return
|
||||||
|
}
|
||||||
const saveData = buildData(this.$store.state.edit.schema)
|
const saveData = buildData(this.$store.state.edit.schema)
|
||||||
if (!saveData.surveyId) {
|
if (!saveData.surveyId) {
|
||||||
ElMessage.error('未获取到问卷id')
|
ElMessage.error('未获取到问卷id')
|
||||||
@ -37,6 +43,7 @@ export default {
|
|||||||
if (this.isPublishing) {
|
if (this.isPublishing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.isPublishing = true
|
this.isPublishing = true
|
||||||
const saveRes = await saveSurvey(saveData)
|
const saveRes = await saveSurvey(saveData)
|
||||||
@ -59,6 +66,20 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
this.isPublishing = false
|
this.isPublishing = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updateLogicConf() {
|
||||||
|
if(this.$store.state.logic.showLogicEngine) {
|
||||||
|
try {
|
||||||
|
this.$store.state.logic.showLogicEngine.validateSchema()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const showLogicConf = this.$store.state.logic.showLogicEngine.toJson()
|
||||||
|
// 更新逻辑配置
|
||||||
|
this.$store.dispatch('edit/changeSchema', { key: 'logicConf', value: { showLogicConf } })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,19 @@ export default {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateLogicConf() {
|
||||||
|
if(this.$store.state.logic.showLogicEngine) {
|
||||||
|
try {
|
||||||
|
this.$store.state.logic.showLogicEngine.validateSchema()
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const showLogicConf = this.$store.state.logic.showLogicEngine.toJson()
|
||||||
|
// 更新逻辑配置
|
||||||
|
this.$store.dispatch('edit/changeSchema', { key: 'logicConf', value: { showLogicConf } })
|
||||||
|
}
|
||||||
|
},
|
||||||
async saveData() {
|
async saveData() {
|
||||||
const saveData = buildData(this.$store.state.edit.schema)
|
const saveData = buildData(this.$store.state.edit.schema)
|
||||||
if (!saveData.surveyId) {
|
if (!saveData.surveyId) {
|
||||||
@ -102,6 +115,14 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isShowAutoSave = false
|
this.isShowAutoSave = false
|
||||||
|
try {
|
||||||
|
this.updateLogicConf()
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(error)
|
||||||
|
ElMessage.error('请检查逻辑配置是否有误')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.isSaving = true
|
this.isSaving = true
|
||||||
const res = await this.saveData()
|
const res = await this.saveData()
|
||||||
|
@ -9,7 +9,8 @@ export default function (schema) {
|
|||||||
'bottomConf',
|
'bottomConf',
|
||||||
'skinConf',
|
'skinConf',
|
||||||
'submitConf',
|
'submitConf',
|
||||||
'questionDataList'
|
'questionDataList',
|
||||||
|
'logicConf'
|
||||||
])
|
])
|
||||||
configData.dataConf = {
|
configData.dataConf = {
|
||||||
dataList: configData.questionDataList
|
dataList: configData.questionDataList
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'navbar-btn',
|
'navbar-btn',
|
||||||
(isActive && btnItem.key === 'skinsettings') || isExactActive
|
(isActive && ['skinsettings', 'edit'].includes(btnItem.key)) || isExactActive
|
||||||
? 'router-link-exact-active'
|
? 'router-link-exact-active'
|
||||||
: ''
|
: ''
|
||||||
]"
|
]"
|
||||||
@ -19,7 +19,6 @@
|
|||||||
<a :href="href" @click="navigate"
|
<a :href="href" @click="navigate"
|
||||||
><span>{{ btnItem.text }}</span></a
|
><span>{{ btnItem.text }}</span></a
|
||||||
>
|
>
|
||||||
<!-- <span>{{ btnItem.text }}</span> -->
|
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rule-list">
|
||||||
|
<RuleNodeView
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
ref="ruleWrappers"
|
||||||
|
:key="item.id"
|
||||||
|
:ruleNode="item"
|
||||||
|
@delete="handleDetele"
|
||||||
|
>
|
||||||
|
</RuleNodeView>
|
||||||
|
|
||||||
|
<div class="no-logic" v-if="list.length === 0">
|
||||||
|
<img src="/imgs/icons/unselected.webp" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button type="primary" plain class="add" @click="handleAdd">
|
||||||
|
<i-ep-plus class="plus-icon" /> 新增显示逻辑
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { shallowRef, computed } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import RuleNodeView from './components/RuleNodeView.vue'
|
||||||
|
|
||||||
|
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const list = computed(() => {
|
||||||
|
return store.state.logic.showLogicEngine?.rules || []
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
const condition = new ConditionNode()
|
||||||
|
const ruleNode = new RuleNode()
|
||||||
|
ruleNode.addCondition(condition)
|
||||||
|
store.state.logic.showLogicEngine.addRule(ruleNode)
|
||||||
|
}
|
||||||
|
const handleDetele = (id: string) => {
|
||||||
|
store.state.logic.showLogicEngine.removeRule(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ruleWrappers = shallowRef([])
|
||||||
|
|
||||||
|
const formValidate = () => {
|
||||||
|
return ruleWrappers.value.map((item: any) => {
|
||||||
|
return item?.submitForm()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const handleValide = () => {
|
||||||
|
const validPass = formValidate()
|
||||||
|
const result = !validPass.includes(false)
|
||||||
|
// result 为ture代表校验不通过
|
||||||
|
return !result
|
||||||
|
}
|
||||||
|
defineExpose({
|
||||||
|
handleValide
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.rule-list {
|
||||||
|
width: 824px;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 12px;
|
||||||
|
.add {
|
||||||
|
margin: 12px 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-logic {
|
||||||
|
padding: 100px 0 50px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<div class="condition-wrapper" data-content-before="且">
|
||||||
|
<span class="desc">如果</span>
|
||||||
|
<el-form-item
|
||||||
|
:prop="`conditions[${index}].field`"
|
||||||
|
:rules="[{ required: true, message: '请选择题目', trigger: 'change' }]"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
class="select field-select"
|
||||||
|
v-model="conditionNode.field"
|
||||||
|
placeholder="请选择题目"
|
||||||
|
@change="(val: any) => handleChange(conditionNode, 'field', val)"
|
||||||
|
>
|
||||||
|
<el-option v-for="{ label, value } in fieldList" :key="value" :label="label" :value="value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<span class="desc">选择了</span>
|
||||||
|
<el-form-item
|
||||||
|
class="select value-select"
|
||||||
|
:prop="`conditions[${index}].value`"
|
||||||
|
:rules="[{ required: true, message: '请选择选项', trigger: 'change' }]"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
v-model="conditionNode.value"
|
||||||
|
placeholder="请选择选项"
|
||||||
|
multiple
|
||||||
|
@change="(val: any) => handleChange(conditionNode, 'value', val)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="{ label, value } in getRelyOptions"
|
||||||
|
:key="value"
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<span class="desc">中的任一选项 </span>
|
||||||
|
<span class="opt">
|
||||||
|
<i-ep-plus class="opt-icon opt-icon-plus" @click="handleAdd" />
|
||||||
|
<i-ep-minus
|
||||||
|
class="opt-icon"
|
||||||
|
v-if="index > 0"
|
||||||
|
:size="14"
|
||||||
|
@click="() => handleDelete(conditionNode.id)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, computed, inject, ref, type ComputedRef } from 'vue'
|
||||||
|
import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild'
|
||||||
|
import { qAbleList } from '@/management/utils/constant.js'
|
||||||
|
import { cleanRichText } from '@/common/xss'
|
||||||
|
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||||
|
const props = defineProps({
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
ruleNode: {
|
||||||
|
type: RuleNode,
|
||||||
|
default: () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
conditionNode: {
|
||||||
|
type: ConditionNode,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
field: '',
|
||||||
|
operator: '',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const fieldList = computed(() => {
|
||||||
|
const currentIndex = renderData.value.findIndex((item) => item.field === props.ruleNode.target)
|
||||||
|
return renderData.value.slice(0, currentIndex)
|
||||||
|
.filter((question: any) => qAbleList.includes(question.type))
|
||||||
|
.map((item: any) => {
|
||||||
|
return {
|
||||||
|
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichText(item.title)}`,
|
||||||
|
value: item.field
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const getRelyOptions = computed(() => {
|
||||||
|
const { field } = props.conditionNode
|
||||||
|
if (!field) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const currentQuestion = renderData.value.find((item) => item.field === field)
|
||||||
|
return (
|
||||||
|
currentQuestion?.options.map((item: any) => {
|
||||||
|
return {
|
||||||
|
label: cleanRichText(item.text),
|
||||||
|
value: item.hash
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (conditionNode: ConditionNode, key: string, value: any) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'field':
|
||||||
|
conditionNode.setField(value)
|
||||||
|
// 前置题改变清空选项
|
||||||
|
conditionNode.setValue([])
|
||||||
|
break
|
||||||
|
case 'operator':
|
||||||
|
conditionNode.setOperator(value)
|
||||||
|
break
|
||||||
|
case 'value':
|
||||||
|
conditionNode.setValue(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleAdd = () => {
|
||||||
|
props.ruleNode.addCondition(new ConditionNode())
|
||||||
|
}
|
||||||
|
const emit = defineEmits(['delete'])
|
||||||
|
const handleDelete = (id: any) => {
|
||||||
|
emit('delete', id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.condition-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 24px 0;
|
||||||
|
&:not(:last-child)::before{
|
||||||
|
content: attr(data-content-before);
|
||||||
|
bottom: 0px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #FEF6E6;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #FAA600;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
}
|
||||||
|
&:not(:last-child)::after{
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
border-top: 1px dashed #e3e4e8;
|
||||||
|
position: absolute;
|
||||||
|
left: 32px;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.opt {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.opt-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-icon-plus {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-form-item {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top !important;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rule-wrapper">
|
||||||
|
<el-form
|
||||||
|
:hide-required-asterisk="true"
|
||||||
|
class="form"
|
||||||
|
ref="ruleForm"
|
||||||
|
:inline="true"
|
||||||
|
:model="ruleNode"
|
||||||
|
>
|
||||||
|
<conditionView
|
||||||
|
v-for="(conditionNode, index) in ruleNode.conditions"
|
||||||
|
:key="conditionNode.id"
|
||||||
|
:index="index"
|
||||||
|
:ruleNode="ruleNode"
|
||||||
|
:conditionNode="conditionNode"
|
||||||
|
@delete="handleDeleteCondition"
|
||||||
|
></conditionView>
|
||||||
|
<div class="target-wrapper">
|
||||||
|
<div class="line">
|
||||||
|
<span class="desc">则显示</span>
|
||||||
|
<el-form-item
|
||||||
|
prop="target"
|
||||||
|
:rules="[{ required: true, message: '请选择目标', trigger: 'change' }]"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
class="select field-select"
|
||||||
|
v-model="ruleNode.target"
|
||||||
|
placeholder="请选择"
|
||||||
|
@change="(val: any) => handleChange(ruleNode, 'target', val)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="{ label, value, disabled } in targetQuestionList"
|
||||||
|
:key="value"
|
||||||
|
:label="label"
|
||||||
|
:disabled="disabled && ruleNode.target !== value "
|
||||||
|
:value="value"
|
||||||
|
>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<i-ep-delete style="font-size: 14px" @click="() => handleDelete(ruleNode.id)" />
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, shallowRef, inject, type ComputedRef } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
|
|
||||||
|
import { RuleNode } from '@/common/logicEngine/RuleBuild'
|
||||||
|
import { cleanRichText } from '@/common/xss'
|
||||||
|
|
||||||
|
import conditionView from './ConditionView.vue'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
ruleNode: {
|
||||||
|
type: RuleNode,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['delete'])
|
||||||
|
|
||||||
|
const handleChange = (ruleNode: any, key: any, value: any) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'target':
|
||||||
|
ruleNode.setTarget(value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleDelete = async (id: any) => {
|
||||||
|
await ElMessageBox.confirm('是否确认删除规则?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
emit('delete', id)
|
||||||
|
}
|
||||||
|
const handleDeleteCondition = (id: any) => {
|
||||||
|
props.ruleNode.removeCondition(id)
|
||||||
|
}
|
||||||
|
const ruleForm = shallowRef<any>(null)
|
||||||
|
const submitForm = () => {
|
||||||
|
ruleForm.value?.validate((valid: any) => {
|
||||||
|
if (valid) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const targetQuestionList = computed(() => {
|
||||||
|
const currntIndexs: number[] = []
|
||||||
|
props.ruleNode.conditions.forEach((el) => {
|
||||||
|
currntIndexs.push(renderData.value.findIndex((item: { field: string }) => item.field === el.field))
|
||||||
|
})
|
||||||
|
const currntIndex = Math.max(...currntIndexs)
|
||||||
|
let questionList = cloneDeep(renderData.value.slice(currntIndex + 1))
|
||||||
|
return questionList.map((item: any) => {
|
||||||
|
return {
|
||||||
|
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichText(item.title)}`,
|
||||||
|
value: item.field,
|
||||||
|
disabled: store.state.logic.showLogicEngine
|
||||||
|
.findTargetsByScope('question')
|
||||||
|
.includes(item.field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
defineExpose({
|
||||||
|
submitForm
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.rule-wrapper {
|
||||||
|
width: 800px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border: 1px solid #e3e4e8;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
margin: 12px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
.target-wrapper {
|
||||||
|
padding: 24px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.el-form-item {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top !important;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -184,7 +184,6 @@ export default {
|
|||||||
.operation-wrapper {
|
.operation-wrapper {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
margin-bottom: 45px;
|
margin-bottom: 45px;
|
||||||
// min-height: 812px;
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
|
@ -95,25 +95,4 @@ export default {
|
|||||||
padding: 0 !important;
|
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
32
web/src/management/pages/edit/pages/edit/LogicEditPage.vue
Normal file
32
web/src/management/pages/edit/pages/edit/LogicEditPage.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="logic-wrapper">
|
||||||
|
<RuleListView></RuleListView>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, provide } from 'vue'
|
||||||
|
import RuleListView from '../../modules/logicModule/RulePanel.vue'
|
||||||
|
import { filterQuestionPreviewData } from '@/management/utils/index'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const questionDataList = computed(() => {
|
||||||
|
return store.state.edit.schema.questionDataList
|
||||||
|
})
|
||||||
|
const renderData = computed(() => {
|
||||||
|
return filterQuestionPreviewData(cloneDeep(questionDataList.value))
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('renderData', renderData)
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.logic-wrapper {
|
||||||
|
height: calc(100% - 120px);
|
||||||
|
width: 100%;
|
||||||
|
margin: 12px;
|
||||||
|
background: #fff;
|
||||||
|
text-align: center;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<commonTemplate>
|
||||||
|
<template #left>
|
||||||
|
<catalogPanel />
|
||||||
|
</template>
|
||||||
|
<template #center>
|
||||||
|
<previewPanel />
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<setterPanel />
|
||||||
|
</template>
|
||||||
|
</commonTemplate>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import commonTemplate from '../../components/CommonTemplate.vue';
|
||||||
|
import catalogPanel from '../../modules/questionModule/CatalogPanel.vue';
|
||||||
|
import previewPanel from '../../modules/questionModule/PreviewPanel.vue';
|
||||||
|
import setterPanel from '../../modules/questionModule/SetterPanel.vue';
|
||||||
|
export default {
|
||||||
|
name: 'editInde1111x',
|
||||||
|
components: {
|
||||||
|
commonTemplate,
|
||||||
|
catalogPanel,
|
||||||
|
previewPanel,
|
||||||
|
setterPanel,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.navbar {
|
||||||
|
border-bottom: 1px solid #e7e9eb;
|
||||||
|
}
|
||||||
|
</style>
|
68
web/src/management/pages/edit/pages/edit/index.vue
Normal file
68
web/src/management/pages/edit/pages/edit/index.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="question-content">
|
||||||
|
<div class="navbar-tab">
|
||||||
|
<el-radio-group v-model="activeRouter">
|
||||||
|
<el-radio-button
|
||||||
|
v-for="btnItem in btnList"
|
||||||
|
:key="btnItem.router"
|
||||||
|
:label="btnItem.text"
|
||||||
|
:value="btnItem.router"
|
||||||
|
/>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'QuestionPage',
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeRouter: this.$route.name,
|
||||||
|
btnList: [
|
||||||
|
{
|
||||||
|
text: '内容设置',
|
||||||
|
router: 'QuestionEditIndex',
|
||||||
|
key: 'questionEdit'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '逻辑设置',
|
||||||
|
router: 'LogicIndex',
|
||||||
|
key: 'logicEdit'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeRouter: {
|
||||||
|
handler(val) {
|
||||||
|
this.$router.push({ name: val })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.question-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
.navbar-tab {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
:deep(.el-radio-button__original-radio + .el-radio-button__inner) {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
||||||
|
color: $primary-color;
|
||||||
|
background-color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -21,15 +21,37 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
needLogin: true
|
needLogin: true
|
||||||
},
|
},
|
||||||
|
name: 'QuestionEdit',
|
||||||
component: () => import('../pages/edit/index.vue'),
|
component: () => import('../pages/edit/index.vue'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'QuestionEditIndex',
|
|
||||||
meta: {
|
meta: {
|
||||||
needLogin: true
|
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',
|
path: 'setting',
|
||||||
@ -41,7 +63,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'skin',
|
path: 'skin',
|
||||||
// name: 'SkinSetting',
|
|
||||||
meta: {
|
meta: {
|
||||||
needLogin: true
|
needLogin: true
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getBannerData } from '@/management/api/skin.js'
|
import { getBannerData } from '@/management/api/skin.js'
|
||||||
|
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async getBannerData({ state, commit }) {
|
async getBannerData({ state, commit }) {
|
||||||
@ -9,5 +10,9 @@ export default {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
commit('setBannerList', res.data)
|
commit('setBannerList', res.data)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
initShowLogic({ commit }, ruleConf) {
|
||||||
|
const showLogicEngine = new RuleBuild(ruleConf)
|
||||||
|
commit('setShowLogicEngine', showLogicEngine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,12 @@ export default {
|
|||||||
}
|
}
|
||||||
dispatch('resetState')
|
dispatch('resetState')
|
||||||
},
|
},
|
||||||
async getSchemaFromRemote({ commit, state }) {
|
async getSchemaFromRemote({ commit, state, dispatch }) {
|
||||||
const res = await getSurveyById(state.surveyId)
|
const res = await getSurveyById(state.surveyId)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const metaData = res.data.surveyMetaRes
|
const metaData = res.data.surveyMetaRes
|
||||||
document.title = metaData.title
|
document.title = metaData.title
|
||||||
const { bannerConf, bottomConf, skinConf, baseConf, submitConf, dataConf } =
|
const { bannerConf, bottomConf, skinConf, baseConf, submitConf, dataConf, logicConf = {} } =
|
||||||
res.data.surveyConfRes.code
|
res.data.surveyConfRes.code
|
||||||
commit('initSchema', {
|
commit('initSchema', {
|
||||||
metaData,
|
metaData,
|
||||||
@ -25,9 +25,11 @@ export default {
|
|||||||
skinConf,
|
skinConf,
|
||||||
baseConf,
|
baseConf,
|
||||||
submitConf,
|
submitConf,
|
||||||
questionDataList: dataConf.dataList
|
questionDataList: dataConf.dataList,
|
||||||
|
logicConf
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.errmsg || '问卷不存在')
|
throw new Error(res.errmsg || '问卷不存在')
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ export default {
|
|||||||
state.schema.baseConf = _merge({}, state.schema.baseConf, codeData.baseConf)
|
state.schema.baseConf = _merge({}, state.schema.baseConf, codeData.baseConf)
|
||||||
state.schema.submitConf = _merge({}, state.schema.submitConf, codeData.submitConf)
|
state.schema.submitConf = _merge({}, state.schema.submitConf, codeData.submitConf)
|
||||||
state.schema.questionDataList = codeData.questionDataList || []
|
state.schema.questionDataList = codeData.questionDataList || []
|
||||||
|
state.schema.logicConf = codeData.logicConf
|
||||||
},
|
},
|
||||||
setSurveyId(state, data) {
|
setSurveyId(state, data) {
|
||||||
state.surveyId = data
|
state.surveyId = data
|
||||||
|
@ -52,6 +52,9 @@ export default {
|
|||||||
},
|
},
|
||||||
link: ''
|
link: ''
|
||||||
},
|
},
|
||||||
questionDataList: []
|
questionDataList: [],
|
||||||
|
logicConf: {
|
||||||
|
showLogicConf: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createStore } from 'vuex'
|
import { createStore } from 'vuex'
|
||||||
import edit from './edit'
|
import edit from './edit'
|
||||||
import user from './user'
|
import user from './user'
|
||||||
|
import logic from './logic'
|
||||||
|
|
||||||
import actions from './actions'
|
import actions from './actions'
|
||||||
import mutations from './mutations'
|
import mutations from './mutations'
|
||||||
@ -13,6 +14,7 @@ export default createStore({
|
|||||||
actions,
|
actions,
|
||||||
modules: {
|
modules: {
|
||||||
edit,
|
edit,
|
||||||
user
|
user,
|
||||||
|
logic
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
19
web/src/management/store/logic/index.js
Normal file
19
web/src/management/store/logic/index.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
setBannerList(state, data) {
|
setBannerList(state, data) {
|
||||||
state.bannerList = data
|
state.bannerList = data
|
||||||
}
|
},
|
||||||
|
setShowLogicEngine(state, logicEngine) {
|
||||||
|
state.logicEngine = logicEngine
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
bannerList: []
|
bannerList: [],
|
||||||
|
logicEngine: null
|
||||||
}
|
}
|
||||||
|
@ -3,3 +3,19 @@ export const QOP_MAP = {
|
|||||||
COPY: 'copy',
|
COPY: 'copy',
|
||||||
EDIT: 'edit'
|
EDIT: 'edit'
|
||||||
}
|
}
|
||||||
|
export const qAbleList = [,
|
||||||
|
'radio',
|
||||||
|
'checkbox',
|
||||||
|
'binary-choice',
|
||||||
|
'vote',
|
||||||
|
]
|
||||||
|
export const operatorOptions = [
|
||||||
|
{
|
||||||
|
label: '选择了',
|
||||||
|
value: 'in',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '不选择',
|
||||||
|
value: 'nin',
|
||||||
|
},
|
||||||
|
]
|
@ -1,12 +1,11 @@
|
|||||||
import { defaultQuestionConfig } from '../config/questionConfig'
|
import { defaultQuestionConfig } from '../config/questionConfig'
|
||||||
import { cloneDeep as _cloneDeep, map as _map } from 'lodash-es'
|
import { cloneDeep as _cloneDeep, map as _map } from 'lodash-es'
|
||||||
|
|
||||||
const generateQuestionField = () => {
|
const generateQuestionField = () => {
|
||||||
const num = Math.floor(Math.random() * 1000)
|
const num = Math.floor(Math.random() * 1000)
|
||||||
return `data${num}`
|
return `data${num}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRandom(len) {
|
export function getRandom(len) {
|
||||||
return Math.random()
|
return Math.random()
|
||||||
.toString()
|
.toString()
|
||||||
.slice(len && typeof len === 'number' ? 0 - len : -6)
|
.slice(len && typeof len === 'number' ? 0 - len : -6)
|
||||||
|
@ -80,7 +80,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit('change', values)
|
emit('change', values)
|
||||||
// return values
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
slots,
|
slots,
|
||||||
|
@ -17,6 +17,7 @@ import AlertDialog from './components/AlertDialog.vue'
|
|||||||
|
|
||||||
import LogoIcon from './components/LogoIcon.vue'
|
import LogoIcon from './components/LogoIcon.vue'
|
||||||
import { get as _get } from 'lodash-es'
|
import { get as _get } from 'lodash-es'
|
||||||
|
import { ruleConf } from '@/common/logicEngine/ruleConf'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
@ -55,7 +56,7 @@ export default {
|
|||||||
const res = await getPublishedSurveyInfo({ surveyPath })
|
const res = await getPublishedSurveyInfo({ surveyPath })
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const data = res.data
|
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
|
document.title = data.title
|
||||||
const questionData = {
|
const questionData = {
|
||||||
bannerConf,
|
bannerConf,
|
||||||
@ -68,6 +69,7 @@ export default {
|
|||||||
this.setSkin(skinConf)
|
this.setSkin(skinConf)
|
||||||
this.$store.commit('setSurveyPath', surveyPath)
|
this.$store.commit('setSurveyPath', surveyPath)
|
||||||
this.$store.dispatch('init', questionData)
|
this.$store.dispatch('init', questionData)
|
||||||
|
this.$store.dispatch('initRuleEngine', logicConf?.showLogicConf);
|
||||||
this.$store.dispatch('getEncryptInfo')
|
this.$store.dispatch('getEncryptInfo')
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.errmsg)
|
throw new Error(res.errmsg)
|
||||||
@ -90,7 +92,6 @@ export default {
|
|||||||
root.style.setProperty('--primary-background-color', backgroundConf?.color) // 设置背景颜色
|
root.style.setProperty('--primary-background-color', backgroundConf?.color) // 设置背景颜色
|
||||||
}
|
}
|
||||||
if (contentConf?.opacity.toString()) {
|
if (contentConf?.opacity.toString()) {
|
||||||
console.log({ opacity: contentConf?.opacity / 100 })
|
|
||||||
root.style.setProperty('--opacity', contentConf?.opacity / 100) // 设置全局透明度
|
root.style.setProperty('--opacity', contentConf?.opacity / 100) // 设置全局透明度
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
ref="formGroup"
|
ref="formGroup"
|
||||||
:render-data="item"
|
:render-data="item"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:formModel="formModel"
|
:formValues="formValues"
|
||||||
@formChange="changeData"
|
@formChange="changeData"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -27,8 +27,8 @@ export default {
|
|||||||
rules() {
|
rules() {
|
||||||
return this.$store.state.rules
|
return this.$store.state.rules
|
||||||
},
|
},
|
||||||
formModel() {
|
formValues() {
|
||||||
return this.$store.getters.formModel
|
return this.$store.state.formValues
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<form ref="ruleForm" :model="formModel" :rules="rules">
|
<form ref="ruleForm" :model="formValues" :rules="rules">
|
||||||
<questionWrapper
|
<div v-for="(item) in renderData" :key="item.field">
|
||||||
v-for="item in renderData"
|
<QuestionWrapper
|
||||||
:key="item.field"
|
class="gap"
|
||||||
class="gap"
|
v-bind="$attrs"
|
||||||
v-bind="$attrs"
|
:moduleConfig="item"
|
||||||
:moduleConfig="item"
|
:qIndex="item.qIndex"
|
||||||
:qIndex="item.qIndex"
|
:indexNumber="item.indexNumber"
|
||||||
:indexNumber="item.indexNumber"
|
:showTitle="true"
|
||||||
:showTitle="true"
|
@change="handleChange"
|
||||||
@change="handleChange"
|
></QuestionWrapper>
|
||||||
></questionWrapper>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject, provide, computed, onBeforeMount } from 'vue'
|
import { inject, provide, computed, onBeforeMount } from 'vue'
|
||||||
import questionWrapper from '../../materials/questions/QuestionRuleContainer'
|
import QuestionWrapper from './QuestionWrapper.vue'
|
||||||
|
import store from '@/render/store'
|
||||||
const $bus = inject('$bus')
|
const $bus = inject('$bus')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rules: {
|
rules: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -26,7 +25,7 @@ const props = defineProps({
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formModel: {
|
formValues: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {
|
||||||
return {}
|
return {}
|
||||||
@ -39,6 +38,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['formChange', 'blur'])
|
const emit = defineEmits(['formChange', 'blur'])
|
||||||
|
|
||||||
// 这里不能直接使用change事件,否则父元素监听change的事件,会被绑定到里面的input上
|
// 这里不能直接使用change事件,否则父元素监听change的事件,会被绑定到里面的input上
|
||||||
@ -50,7 +50,7 @@ const handleChange = (data) => {
|
|||||||
const fields = []
|
const fields = []
|
||||||
provide('Form', {
|
provide('Form', {
|
||||||
model: computed(() => {
|
model: computed(() => {
|
||||||
return props.formModel
|
return props.formValues
|
||||||
}),
|
}),
|
||||||
rules: computed(() => {
|
rules: computed(() => {
|
||||||
return props.rules
|
return props.rules
|
||||||
@ -71,6 +71,13 @@ onBeforeMount(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
// const visible = computed(() => {
|
||||||
|
// return (field) => {
|
||||||
|
// console.log(field + '重新计算visible:'+store.state.ruleEngine.getResult(field, 'question'))
|
||||||
|
// // 显示逻辑-处理视图
|
||||||
|
// return store.state.ruleEngine.getResult(field, 'question')
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
const validate = (callback) => {
|
const validate = (callback) => {
|
||||||
const length = fields.length
|
const length = fields.length
|
||||||
|
114
web/src/render/components/QuestionWrapper.vue
Normal file
114
web/src/render/components/QuestionWrapper.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<QuestionRuleContainer
|
||||||
|
v-if="visible"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:moduleConfig="questionConfig"
|
||||||
|
:indexNumber="indexNumber"
|
||||||
|
:showTitle="true"
|
||||||
|
@change="handleChange"
|
||||||
|
></QuestionRuleContainer>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { unref, computed, watch } from 'vue'
|
||||||
|
import QuestionRuleContainer from '../../materials/questions/QuestionRuleContainer'
|
||||||
|
import { useVoteMap } from '@/render/hooks/useVoteMap'
|
||||||
|
import { useShowOthers } from '@/render/hooks/useShowOthers'
|
||||||
|
import { useShowInput } from '@/render/hooks/useShowInput'
|
||||||
|
import store from '@/render/store'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
const props = defineProps({
|
||||||
|
indexNumber: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
moduleConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
|
const formValues = computed(() => {
|
||||||
|
return store.state.formValues
|
||||||
|
})
|
||||||
|
const questionConfig = computed(() =>{
|
||||||
|
let moduleConfig = props.moduleConfig
|
||||||
|
const { type, field, options, ...rest } = cloneDeep(moduleConfig)
|
||||||
|
// console.log(field,'这里依赖的formValue,所以change时会触发重新计算')
|
||||||
|
let alloptions = options
|
||||||
|
if(type === 'vote') {
|
||||||
|
const { options, voteTotal } = useVoteMap(field)
|
||||||
|
const voteOptions = unref(options)
|
||||||
|
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
||||||
|
moduleConfig.voteTotal = unref(voteTotal)
|
||||||
|
}
|
||||||
|
if(['radio','checkbox'].includes(type) && options.filter(optionItem => optionItem.others).length > 0) {
|
||||||
|
let { options, othersValue } = useShowOthers(field)
|
||||||
|
const othersOptions = unref(options)
|
||||||
|
alloptions = alloptions.map((obj, index) => Object.assign(obj, othersOptions[index]))
|
||||||
|
moduleConfig.othersValue = unref(othersValue)
|
||||||
|
}
|
||||||
|
if(['radio-star','radio-nps'].includes(type) && Object.keys(rest.rangeConfig).filter(index => rest.rangeConfig[index].isShowInput).length > 0) {
|
||||||
|
let { rangeConfig, othersValue } = useShowInput(field)
|
||||||
|
// console.log({rangeConfig, othersValue})
|
||||||
|
moduleConfig.rangeConfig = unref(rangeConfig)
|
||||||
|
moduleConfig.othersValue = unref(othersValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...moduleConfig,
|
||||||
|
options: alloptions,
|
||||||
|
value: formValues.value[props.moduleConfig.field]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const visible = computed(() => {
|
||||||
|
const { field } = props.moduleConfig
|
||||||
|
const matchRule = store.state.ruleEngine.rules.get(field+'question')
|
||||||
|
if(matchRule) {
|
||||||
|
return matchRule.result
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(() => visible.value, (newVal, oldVal) => {
|
||||||
|
// 题目从显示到隐藏,需要清空值
|
||||||
|
const { field, type, innerType } = props.moduleConfig
|
||||||
|
if(!newVal && oldVal) {
|
||||||
|
let value = ''
|
||||||
|
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
||||||
|
if (/checkbox/.test(type) || innerType === 'checkbox') {
|
||||||
|
value = value ? [value] : []
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
key: field,
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
store.commit('changeFormData', data)
|
||||||
|
notifyMatch(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (data) => {
|
||||||
|
const { key } = data
|
||||||
|
// console.log(key, '触发change事件')
|
||||||
|
emit('change', data)
|
||||||
|
// 处理投票题
|
||||||
|
if(props.moduleConfig.type === 'vote') {
|
||||||
|
store.dispatch('updateVoteData', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理显示逻辑
|
||||||
|
notifyMatch(key)
|
||||||
|
}
|
||||||
|
const notifyMatch = (key) => {
|
||||||
|
let fact = unref(formValues)
|
||||||
|
const targets = store.state.ruleEngine.findTargetsByField(key) || []
|
||||||
|
// 前置题改变通知目标题更新规则匹配
|
||||||
|
targets.forEach((target) => {
|
||||||
|
store.state.ruleEngine.match(target, 'question', fact)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
30
web/src/render/hooks/useShowInput.js
Normal file
30
web/src/render/hooks/useShowInput.js
Normal file
@ -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 }
|
||||||
|
}
|
29
web/src/render/hooks/useShowOthers.js
Normal file
29
web/src/render/hooks/useShowOthers.js
Normal file
@ -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 }
|
||||||
|
}
|
18
web/src/render/hooks/useVoteMap.js
Normal file
18
web/src/render/hooks/useVoteMap.js
Normal file
@ -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 }
|
||||||
|
}
|
@ -27,6 +27,7 @@ import { submitForm } from '../api/survey'
|
|||||||
import encrypt from '../utils/encrypt'
|
import encrypt from '../utils/encrypt'
|
||||||
|
|
||||||
import useCommandComponent from '../hooks/useCommandComponent'
|
import useCommandComponent from '../hooks/useCommandComponent'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'indexPage',
|
name: 'indexPage',
|
||||||
@ -49,9 +50,6 @@ export default {
|
|||||||
LogoIcon
|
LogoIcon
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
formModel() {
|
|
||||||
return this.$store.getters.formModel
|
|
||||||
},
|
|
||||||
confirmAgain() {
|
confirmAgain() {
|
||||||
return this.$store.state.submitConf.confirmAgain
|
return this.$store.state.submitConf.confirmAgain
|
||||||
},
|
},
|
||||||
@ -94,9 +92,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSubmitData() {
|
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 = {
|
const result = {
|
||||||
surveyPath: this.surveyPath,
|
surveyPath: this.surveyPath,
|
||||||
data: JSON.stringify(this.formModel),
|
data: JSON.stringify(formModel),
|
||||||
difTime: Date.now() - this.$store.state.enterTime,
|
difTime: Date.now() - this.$store.state.enterTime,
|
||||||
clientTime: Date.now()
|
clientTime: Date.now()
|
||||||
}
|
}
|
||||||
@ -118,6 +130,7 @@ export default {
|
|||||||
async submitForm() {
|
async submitForm() {
|
||||||
try {
|
try {
|
||||||
const submitData = this.getSubmitData()
|
const submitData = this.getSubmitData()
|
||||||
|
|
||||||
const res = await submitForm(submitData)
|
const res = await submitForm(submitData)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
this.$store.commit('setRouter', 'successPage')
|
this.$store.commit('setRouter', 'successPage')
|
||||||
|
@ -5,6 +5,7 @@ import 'moment/locale/zh-cn'
|
|||||||
moment.locale('zh-cn')
|
moment.locale('zh-cn')
|
||||||
import adapter from '../adapter'
|
import adapter from '../adapter'
|
||||||
import { queryVote, getEncryptInfo } from '@/render/api/survey'
|
import { queryVote, getEncryptInfo } from '@/render/api/survey'
|
||||||
|
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
|
||||||
/**
|
/**
|
||||||
* CODE_MAP不从management引入,在dev阶段,会导致B端 router被加载,进而导致C端路由被添加 baseUrl: /management
|
* CODE_MAP不从management引入,在dev阶段,会导致B端 router被加载,进而导致C端路由被添加 baseUrl: /management
|
||||||
*/
|
*/
|
||||||
@ -13,6 +14,7 @@ const CODE_MAP = {
|
|||||||
ERROR: 500,
|
ERROR: 500,
|
||||||
NO_AUTH: 403
|
NO_AUTH: 403
|
||||||
}
|
}
|
||||||
|
const VOTE_INFO_KEY = 'voteinfo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// 初始化
|
// 初始化
|
||||||
@ -101,18 +103,63 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
localStorage.removeItem(VOTE_INFO_KEY)
|
||||||
const voteRes = await queryVote({
|
const voteRes = await queryVote({
|
||||||
surveyPath,
|
surveyPath,
|
||||||
fieldList: fieldList.join(',')
|
fieldList: fieldList.join(',')
|
||||||
})
|
})
|
||||||
|
|
||||||
if (voteRes.code === 200) {
|
if (voteRes.code === 200) {
|
||||||
|
localStorage.setItem(
|
||||||
|
VOTE_INFO_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
...voteRes.data
|
||||||
|
})
|
||||||
|
)
|
||||||
commit('setVoteMap', voteRes.data)
|
commit('setVoteMap', voteRes.data)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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 }) {
|
async getEncryptInfo({ commit }) {
|
||||||
try {
|
try {
|
||||||
const res = await getEncryptInfo()
|
const res = await getEncryptInfo()
|
||||||
@ -122,5 +169,9 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async initRuleEngine({ commit }, ruleConf) {
|
||||||
|
const ruleEngine = new RuleMatch(ruleConf)
|
||||||
|
commit('setRuleEgine', ruleEngine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,115 +3,92 @@ import { flatten } from 'lodash-es'
|
|||||||
export default {
|
export default {
|
||||||
// 题目列表
|
// 题目列表
|
||||||
renderData: (state) => {
|
renderData: (state) => {
|
||||||
const { questionSeq, questionData, formValues } = state
|
const { questionSeq, questionData } = state
|
||||||
|
|
||||||
let index = 1
|
let index = 1
|
||||||
return (
|
return (
|
||||||
questionSeq &&
|
questionSeq &&
|
||||||
questionSeq.reduce((pre, item) => {
|
questionSeq.reduce((pre, item) => {
|
||||||
const questionArr = []
|
const questionArr = []
|
||||||
for (const questionKey of item) {
|
|
||||||
|
item.forEach(questionKey => {
|
||||||
|
console.log('题目重新计算')
|
||||||
const question = { ...questionData[questionKey] }
|
const question = { ...questionData[questionKey] }
|
||||||
|
|
||||||
const { type, extraOptions, options, rangeConfig } = question
|
const { type, extraOptions, options, rangeConfig } = question
|
||||||
|
|
||||||
const questionVal = formValues[questionKey]
|
// const questionVal = formValues[questionKey]
|
||||||
|
|
||||||
question.value = questionVal
|
// question.value = questionVal
|
||||||
// 本题开启了
|
// 本题开启了
|
||||||
if (question.showIndex) {
|
if (question.showIndex) {
|
||||||
question.indexNumber = index++
|
question.indexNumber = index++
|
||||||
}
|
}
|
||||||
|
|
||||||
const allOptions = []
|
// const allOptions = []
|
||||||
if (Array.isArray(extraOptions)) {
|
// if (Array.isArray(extraOptions)) {
|
||||||
allOptions.push(...extraOptions)
|
// allOptions.push(...extraOptions)
|
||||||
}
|
// }
|
||||||
if (Array.isArray(options)) {
|
// if (Array.isArray(options)) {
|
||||||
allOptions.push(...options)
|
// allOptions.push(...options)
|
||||||
}
|
// }
|
||||||
|
|
||||||
let othersValue = {}
|
// let othersValue = {}
|
||||||
let voteTotal = 0
|
// let voteTotal = 0
|
||||||
const voteMap = state.voteMap
|
// const voteMap = state.voteMap
|
||||||
if (/vote/.test(type)) {
|
// if (/vote/.test(type)) {
|
||||||
voteTotal = voteMap?.[questionKey]?.total || 0
|
// voteTotal = voteMap?.[questionKey]?.total || 0
|
||||||
}
|
// }
|
||||||
// 遍历所有的选项
|
// 遍历所有的选项
|
||||||
for (const optionItem of allOptions) {
|
// for (const optionItem of allOptions) {
|
||||||
// 开启了更多输入框,生成othersValue的值
|
// 开启了更多输入框,生成othersValue的值
|
||||||
if (optionItem.others) {
|
// if (optionItem.others) {
|
||||||
const opKey = `${questionKey}_${optionItem.hash}`
|
// const opKey = `${questionKey}_${optionItem.hash}`
|
||||||
optionItem.othersKey = opKey
|
// optionItem.othersKey = opKey
|
||||||
optionItem.othersValue = formValues[opKey]
|
// optionItem.othersValue = formValues[opKey]
|
||||||
othersValue[opKey] = formValues[opKey]
|
// othersValue[opKey] = formValues[opKey]
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 投票题,用户手动选择选项后,要实时更新展示数据和进度
|
// 投票题,用户手动选择选项后,要实时更新展示数据和进度
|
||||||
if (/vote/.test(type)) {
|
// if (/vote/.test(type)) {
|
||||||
const voteCount = voteMap?.[questionKey]?.[optionItem.hash] || 0
|
// const voteCount = voteMap?.[questionKey]?.[optionItem.hash] || 0
|
||||||
if (
|
// if (
|
||||||
Array.isArray(questionVal)
|
// Array.isArray(questionVal)
|
||||||
? questionVal.includes(optionItem.hash)
|
// ? questionVal.includes(optionItem.hash)
|
||||||
: questionVal === optionItem.hash
|
// : questionVal === optionItem.hash
|
||||||
) {
|
// ) {
|
||||||
optionItem.voteCount = voteCount + 1
|
// optionItem.voteCount = voteCount + 1
|
||||||
voteTotal = voteTotal + 1
|
// voteTotal = voteTotal + 1
|
||||||
} else {
|
// } else {
|
||||||
optionItem.voteCount = voteCount
|
// optionItem.voteCount = voteCount
|
||||||
}
|
// }
|
||||||
question.voteTotal = voteTotal
|
// question.voteTotal = voteTotal
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 开启了更多输入框,要将当前的value赋值给question
|
// 开启了更多输入框,要将当前的value赋值给question
|
||||||
if (rangeConfig && Object.keys(rangeConfig).length > 0 && rangeConfig[questionVal]) {
|
// if (rangeConfig && Object.keys(rangeConfig).length > 0 && rangeConfig[questionVal]) {
|
||||||
const curRange = rangeConfig[questionVal]
|
// const curRange = rangeConfig[questionVal]
|
||||||
if (curRange?.isShowInput) {
|
// if (curRange?.isShowInput) {
|
||||||
const rangeKey = `${questionKey}_${questionVal}`
|
// const rangeKey = `${questionKey}_${questionVal}`
|
||||||
curRange.othersKey = rangeKey
|
// curRange.othersKey = rangeKey
|
||||||
curRange.othersValue = formValues[rangeKey]
|
// curRange.othersValue = formValues[rangeKey]
|
||||||
othersValue[rangeKey] = formValues[rangeKey]
|
// othersValue[rangeKey] = formValues[rangeKey]
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 将othersValue赋值给
|
// 将othersValue赋值给
|
||||||
question.othersValue = othersValue
|
// question.othersValue = othersValue
|
||||||
|
|
||||||
questionArr.push(question)
|
questionArr.push(question)
|
||||||
}
|
})
|
||||||
|
|
||||||
if (questionArr && questionArr.length) {
|
if (questionArr && questionArr.length) {
|
||||||
pre.push(questionArr)
|
pre.push(questionArr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pre
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,8 @@ export default {
|
|||||||
},
|
},
|
||||||
changeFormData(state, data) {
|
changeFormData(state, data) {
|
||||||
let { key, value } = data
|
let { key, value } = data
|
||||||
|
// console.log('formValues', key, value)
|
||||||
set(state, `formValues.${key}`, value)
|
set(state, `formValues.${key}`, value)
|
||||||
// set(state, `questionData.${key}.value`, value)
|
|
||||||
},
|
},
|
||||||
changeSelectMoreData(state, data) {
|
changeSelectMoreData(state, data) {
|
||||||
const { key, value, field } = data
|
const { key, value, field } = data
|
||||||
@ -36,10 +36,21 @@ export default {
|
|||||||
setVoteMap(state, data) {
|
setVoteMap(state, data) {
|
||||||
state.voteMap = 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) {
|
setQuestionSeq(state, data) {
|
||||||
state.questionSeq = data
|
state.questionSeq = data
|
||||||
},
|
},
|
||||||
setEncryptInfo(state, data) {
|
setEncryptInfo(state, data) {
|
||||||
state.encryptInfo = data
|
state.encryptInfo = data
|
||||||
}
|
},
|
||||||
|
setRuleEgine(state, ruleEngine) {
|
||||||
|
state.ruleEngine = ruleEngine
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -12,5 +12,6 @@ export default {
|
|||||||
enterTime: null,
|
enterTime: null,
|
||||||
questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
||||||
voteMap: {},
|
voteMap: {},
|
||||||
encryptInfo: null
|
encryptInfo: null,
|
||||||
|
ruleEngine: null
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user