feat: 显示逻辑稳定版 (#149)
This commit is contained in:
parent
21cba5946b
commit
142b3b7be9
@ -5,7 +5,7 @@
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"src/**/__test/*.ts\"",
|
||||
"local": "ts-node ./scripts/run-local.ts",
|
||||
"start": "nest start",
|
||||
"dev": "npm run start:dev",
|
||||
|
@ -48,5 +48,8 @@
|
||||
"contentConf": {
|
||||
"opacity": 100
|
||||
}
|
||||
},
|
||||
"logicConf": {
|
||||
"showLogicConf": []
|
||||
}
|
||||
}
|
||||
|
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@ -15,6 +15,7 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
17
web/components.d.ts
vendored
17
web/components.d.ts
vendored
@ -11,15 +11,13 @@ declare module 'vue' {
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
@ -27,24 +25,22 @@ declare module 'vue' {
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
'IEp-[]': typeof import('~icons/ep/[]')['default']
|
||||
'IEp-[test]': typeof import('~icons/ep/[test]')['default']
|
||||
'IEp-]': typeof import('~icons/ep/]')['default']
|
||||
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
||||
IEpCheck: typeof import('~icons/ep/check')['default']
|
||||
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']
|
||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
||||
@ -58,6 +54,5 @@ declare module 'vue' {
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
vPopover: typeof import('element-plus/es')['ElPopoverDirective']
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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>
|
32
web/src/common/logicEngine/BasicType.ts
Normal file
32
web/src/common/logicEngine/BasicType.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* in:包含, 选择了,任一
|
||||
* eq: 等于,选择了,全部
|
||||
* nin: 不包含,不选择,任一
|
||||
* neq:不等于,不选择,全部,可以实现“填写了”
|
||||
*/
|
||||
export enum Operator {
|
||||
Include = 'in',
|
||||
Equal = 'eq',
|
||||
NotEqual = 'neq',
|
||||
NotInclude = 'nin',
|
||||
}
|
||||
|
||||
|
||||
export enum PrefixID {
|
||||
Rule = 'r',
|
||||
Condition = 'c'
|
||||
}
|
||||
|
||||
export enum Scope {
|
||||
Question = 'question',
|
||||
Option = 'option'
|
||||
}
|
||||
|
||||
|
||||
export type FieldTypes = string | string[];
|
||||
|
||||
// 定义事实对象类型
|
||||
export type Fact = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
146
web/src/common/logicEngine/RuleBuild.ts
Normal file
146
web/src/common/logicEngine/RuleBuild.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import * as yup from 'yup'
|
||||
import { type FieldTypes, PrefixID, Operator, Scope } from './BasicType'
|
||||
|
||||
export function generateID(prefix = PrefixID.Rule) {
|
||||
return `${prefix}-${nanoid(5)}`
|
||||
}
|
||||
// 定义条件规则类
|
||||
export class ConditionNode {
|
||||
id: string = '';
|
||||
public field: string = '';
|
||||
public operator: Operator = Operator.Include;
|
||||
public value: FieldTypes = []
|
||||
constructor(field: string = '', operator: Operator = Operator.Include, value: FieldTypes = []) {
|
||||
this.field = field;
|
||||
this.operator = operator;
|
||||
this.value = value;
|
||||
this.id = generateID(PrefixID.Condition)
|
||||
}
|
||||
setField(field: string) {
|
||||
this.field = field;
|
||||
}
|
||||
setOperator(operator: Operator) {
|
||||
this.operator = operator;
|
||||
}
|
||||
setValue(value: FieldTypes) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class RuleNode {
|
||||
id: string = '';
|
||||
conditions: ConditionNode[] = []
|
||||
scope: string = Scope.Question
|
||||
target: string = ''
|
||||
constructor(scope:string = Scope.Question, target: string = '') {
|
||||
this.id = generateID(PrefixID.Rule)
|
||||
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[] = [];
|
||||
static instance: RuleBuild;
|
||||
constructor() {
|
||||
this.rules = [];
|
||||
if (!RuleBuild.instance) {
|
||||
RuleBuild.instance = this;
|
||||
}
|
||||
|
||||
return RuleBuild.instance;
|
||||
}
|
||||
|
||||
// 添加条件规则到规则引擎中
|
||||
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) {
|
||||
this.rules = []
|
||||
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())
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
201
web/src/common/logicEngine/RulesMatch.ts
Normal file
201
web/src/common/logicEngine/RulesMatch.ts
Normal file
@ -0,0 +1,201 @@
|
||||
|
||||
import { Operator, type FieldTypes, type Fact } from "./BasicType";
|
||||
|
||||
// 定义条件规则类
|
||||
export class ConditionNode<F extends string, O extends Operator> {
|
||||
// 默认显示
|
||||
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 Operator.Equal:
|
||||
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 Operator.Include:
|
||||
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 Operator.NotInclude:
|
||||
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 Operator.NotEqual:
|
||||
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, Operator>>; // 使用哈希表存储条件规则对象
|
||||
public result: boolean = false;
|
||||
constructor(public target: string, public scope: string) {
|
||||
this.conditions = new Map();
|
||||
}
|
||||
// 添加条件规则到规则引擎中
|
||||
addCondition(condition: ConditionNode<string, Operator>) {
|
||||
const hash = condition.calculateHash();
|
||||
this.conditions.set(hash, condition);
|
||||
}
|
||||
|
||||
// 匹配条件规则
|
||||
match(fact: Fact) {
|
||||
const res = Array.from(this.conditions.entries()).every(([, 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(([, 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>;
|
||||
static instance: any;
|
||||
constructor() {
|
||||
this.rules = new Map();
|
||||
if (!RuleMatch.instance) {
|
||||
RuleMatch.instance = this;
|
||||
}
|
||||
|
||||
return RuleMatch.instance;
|
||||
}
|
||||
fromJson(ruleConf:any) {
|
||||
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, Operator>) => {
|
||||
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(([, value]) => {
|
||||
return [...value.conditions.entries()].filter(([, value]) => {
|
||||
return value.field === field
|
||||
})
|
||||
}))
|
||||
return [...rules.values()].map(obj => obj.target);
|
||||
}
|
||||
toJson() {
|
||||
return Array.from(this.rules.entries()).map(([, 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.js
Normal file
23
web/src/management/hooks/useQuestionInfo.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { computed } from 'vue';
|
||||
import store from '@/management/store'
|
||||
import { cleanRichText } from '@/common/xss'
|
||||
export const useQuestionInfo = (field) => {
|
||||
const getQuestionTitle = computed(() => {
|
||||
const questionDataList = store.state.edit.schema.questionDataList
|
||||
return () => {
|
||||
return questionDataList.find((item) => item.field === field)?.title
|
||||
}
|
||||
})
|
||||
const getOptionTitle = computed(() => {
|
||||
const questionDataList = store.state.edit.schema.questionDataList
|
||||
return (value) => {
|
||||
const options = questionDataList.find((item) => item.field === field)?.options || []
|
||||
if(value instanceof Array) {
|
||||
return options.filter((item) => value.includes(item.hash)).map((item) => cleanRichText(item.text))
|
||||
} else {
|
||||
return options.filter((item) => item.hash === value).map((item) => cleanRichText(item.text))
|
||||
}
|
||||
}
|
||||
})
|
||||
return { getQuestionTitle, getOptionTitle }
|
||||
}
|
7
web/src/management/hooks/useShowLogicEngine.js
Normal file
7
web/src/management/hooks/useShowLogicEngine.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
|
||||
|
||||
export const showLogicEngine = ref()
|
||||
export const initShowLogicEngine = (ruleConf) => {
|
||||
showLogicEngine.value = new RuleBuild().fromJson(ruleConf)
|
||||
}
|
29
web/src/management/hooks/useShowLogicInfo.js
Normal file
29
web/src/management/hooks/useShowLogicInfo.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { computed, unref } from 'vue';
|
||||
import { useQuestionInfo } from './useQuestionInfo'
|
||||
import { flatten } from 'lodash-es'
|
||||
import { cleanRichText } from '@/common/xss'
|
||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
|
||||
// 目标题的显示逻辑提示文案
|
||||
export const useShowLogicInfo = (field) => {
|
||||
const hasShowLogic = computed(() => {
|
||||
const logicEngine = showLogicEngine.value
|
||||
// 判断该题是否作为了显示逻辑前置题
|
||||
const isField = logicEngine?.findTargetsByFields(field)?.length > 0
|
||||
// 判断该题是否作为了显示逻辑目标题
|
||||
const isTarget = logicEngine?.findTargetsByScope(field)?.length > 0
|
||||
return isField || isTarget
|
||||
})
|
||||
const getShowLogicText = computed(() => {
|
||||
const logicEngine = showLogicEngine.value
|
||||
// 获取目标题的规则
|
||||
const rules = logicEngine?.findConditionByTarget(field) || []
|
||||
|
||||
const conditions = flatten(rules).map((item) => {
|
||||
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 }
|
||||
}
|
@ -24,12 +24,14 @@
|
||||
<i-ep-close />
|
||||
</div>
|
||||
</div>
|
||||
<div class="logic-text" v-html="getShowLogicText"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, unref } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||
import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo'
|
||||
|
||||
const props = defineProps({
|
||||
qIndex: {
|
||||
@ -57,6 +59,8 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['changeSeq', 'select'])
|
||||
|
||||
const { getShowLogicText, hasShowLogic } = useShowLogicInfo(props.moduleConfig.field)
|
||||
|
||||
const isHover = ref(false)
|
||||
|
||||
const itemClass = computed(() => {
|
||||
@ -111,6 +115,13 @@ const onMoveDown = () => {
|
||||
isHover.value = false
|
||||
}
|
||||
const onDelete = async () => {
|
||||
if(unref(hasShowLogic)) {
|
||||
ElMessageBox.alert('该问题被逻辑依赖,请先删除逻辑依赖', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('本次操作会影响数据统计查看,是否确认删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@ -182,5 +193,11 @@ const onMove = () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
.logic-text{
|
||||
font-size: 12px;
|
||||
color: #c8c9cd;
|
||||
padding: 0 .4rem;
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -22,7 +22,7 @@ import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||
|
||||
import CommonTemplate from './components/CommonTemplate.vue'
|
||||
import Navbar from './components/ModuleNavbar.vue'
|
||||
|
||||
import { initShowLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
export default {
|
||||
name: 'questionEditPage',
|
||||
components: {
|
||||
@ -34,6 +34,8 @@ export default {
|
||||
this.$store.commit('edit/setSurveyId', this.$route.params.id)
|
||||
try {
|
||||
await this.$store.dispatch('edit/init')
|
||||
// await this.$store.dispatch('logic/initShowLogic', this.$store.state.edit.schema.logicConf.showLogicConf || {})
|
||||
await initShowLogicEngine(this.$store.state.edit.schema.logicConf.showLogicConf || {})
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message)
|
||||
// 自动跳转回列表页
|
||||
|
@ -5,14 +5,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get as _get } from 'lodash-es'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
import { get as _get } from 'lodash-es'
|
||||
|
||||
import { publishSurvey, saveSurvey } from '@/management/api/survey'
|
||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
import buildData from './buildData'
|
||||
|
||||
export default {
|
||||
@ -29,6 +27,12 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async onPublish() {
|
||||
try {
|
||||
this.updateLogicConf()
|
||||
} catch (error) {
|
||||
ElMessage.error('请检查逻辑配置是否有误')
|
||||
return
|
||||
}
|
||||
const saveData = buildData(this.$store.state.edit.schema)
|
||||
if (!saveData.surveyId) {
|
||||
ElMessage.error('未获取到问卷id')
|
||||
@ -37,6 +41,7 @@ export default {
|
||||
if (this.isPublishing) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.isPublishing = true
|
||||
const saveRes = await saveSurvey(saveData)
|
||||
@ -59,6 +64,14 @@ export default {
|
||||
} finally {
|
||||
this.isPublishing = false
|
||||
}
|
||||
},
|
||||
updateLogicConf() {
|
||||
if(showLogicEngine.value) {
|
||||
showLogicEngine.value.validateSchema()
|
||||
const showLogicConf = showLogicEngine.value.toJson()
|
||||
// 更新逻辑配置
|
||||
this.$store.dispatch('edit/changeSchema', { key: 'logicConf', value: { showLogicConf } })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import 'element-plus/theme-chalk/src/message.scss'
|
||||
|
||||
import { saveSurvey } from '@/management/api/survey'
|
||||
import buildData from './buildData'
|
||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
@ -88,6 +89,14 @@ export default {
|
||||
}, 2000)
|
||||
}
|
||||
},
|
||||
updateLogicConf() {
|
||||
if(showLogicEngine.value) {
|
||||
showLogicEngine.value.validateSchema()
|
||||
const showLogicConf = showLogicEngine.value.toJson()
|
||||
// 更新逻辑配置
|
||||
this.$store.dispatch('edit/changeSchema', { key: 'logicConf', value: { showLogicConf } })
|
||||
}
|
||||
},
|
||||
async saveData() {
|
||||
const saveData = buildData(this.$store.state.edit.schema)
|
||||
if (!saveData.surveyId) {
|
||||
@ -102,6 +111,14 @@ export default {
|
||||
return
|
||||
}
|
||||
this.isShowAutoSave = false
|
||||
try {
|
||||
this.updateLogicConf()
|
||||
} catch (error) {
|
||||
// console.error(error)
|
||||
ElMessage.error('请检查逻辑配置是否有误')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.isSaving = true
|
||||
const res = await this.saveData()
|
||||
|
@ -9,7 +9,8 @@ export default function (schema) {
|
||||
'bottomConf',
|
||||
'skinConf',
|
||||
'submitConf',
|
||||
'questionDataList'
|
||||
'questionDataList',
|
||||
'logicConf'
|
||||
])
|
||||
configData.dataConf = {
|
||||
dataList: configData.questionDataList
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div
|
||||
:class="[
|
||||
'navbar-btn',
|
||||
(isActive && btnItem.key === 'skinsettings') || isExactActive
|
||||
(isActive && ['skinsettings', 'edit'].includes(btnItem.key)) || isExactActive
|
||||
? 'router-link-exact-active'
|
||||
: ''
|
||||
]"
|
||||
@ -19,7 +19,6 @@
|
||||
<a :href="href" @click="navigate"
|
||||
><span>{{ btnItem.text }}</span></a
|
||||
>
|
||||
<!-- <span>{{ btnItem.text }}</span> -->
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="rule-list">
|
||||
<RuleNodeView
|
||||
v-for="(item) 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 { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
import RuleNodeView from './components/RuleNodeView.vue'
|
||||
|
||||
const list = computed(() => {
|
||||
return showLogicEngine.value?.rules || []
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
const condition = new ConditionNode()
|
||||
const ruleNode = new RuleNode()
|
||||
ruleNode.addCondition(condition)
|
||||
showLogicEngine.value.addRule(ruleNode)
|
||||
}
|
||||
const handleDetele = (id: string) => {
|
||||
showLogicEngine.value.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,195 @@
|
||||
<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="conditionField"
|
||||
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="conditionValue"
|
||||
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 conditionField = computed(() => {
|
||||
return props.conditionNode.field
|
||||
})
|
||||
|
||||
const conditionValue = computed(() => {
|
||||
return props.conditionNode.value
|
||||
})
|
||||
|
||||
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,147 @@
|
||||
<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="ruleTarget"
|
||||
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 { 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 { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
import ConditionView from './ConditionView.vue'
|
||||
|
||||
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||
|
||||
const props = defineProps({
|
||||
ruleNode: {
|
||||
type: RuleNode,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['delete'])
|
||||
const ruleTarget = computed(() => {
|
||||
return props.ruleNode.target
|
||||
})
|
||||
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: showLogicEngine.value
|
||||
.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 {
|
||||
margin-top: 50px;
|
||||
margin-bottom: 45px;
|
||||
// min-height: 812px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-right: 30px;
|
||||
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<commonTemplate>
|
||||
<template #left>
|
||||
<CatalogPanel></CatalogPanel>
|
||||
</template>
|
||||
<template #center>
|
||||
<PreviewPanel></PreviewPanel>
|
||||
</template>
|
||||
<template #right>
|
||||
<SetterPanel></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: 'EditPage',
|
||||
components: {
|
||||
commonTemplate,
|
||||
CatalogPanel,
|
||||
PreviewPanel,
|
||||
SetterPanel
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
border-bottom: 1px solid #e7e9eb;
|
||||
}
|
||||
</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">
|
||||
<RulePanel></RulePanel>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, provide } from 'vue'
|
||||
import RulePanel 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: 'editIndex',
|
||||
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>
|
@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingPanel from '../modules/settingModule/SettingPanel.vue'
|
||||
import SettingPanel from '../../modules/settingModule/SettingPanel.vue'
|
||||
export default {
|
||||
name: 'SettingPage',
|
||||
components: {
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<commonTemplate>
|
||||
<CommonTemplate>
|
||||
<template #left>
|
||||
<CatalogPanel />
|
||||
</template>
|
||||
@ -9,10 +9,10 @@
|
||||
<template #right>
|
||||
<SetterPanel />
|
||||
</template>
|
||||
</commonTemplate>
|
||||
</CommonTemplate>
|
||||
</template>
|
||||
<script>
|
||||
import commonTemplate from '../../components/CommonTemplate.vue'
|
||||
import CommonTemplate from '../../components/CommonTemplate.vue'
|
||||
import CatalogPanel from '../../modules/settingModule/skin/CatalogPanel.vue'
|
||||
import PreviewPanel from '../../modules/settingModule/skin/PreviewPanel.vue'
|
||||
import SetterPanel from '../../modules/settingModule/skin/SetterPanel.vue'
|
||||
@ -20,7 +20,7 @@ import SetterPanel from '../../modules/settingModule/skin/SetterPanel.vue'
|
||||
export default {
|
||||
name: 'ContentPage',
|
||||
components: {
|
||||
commonTemplate,
|
||||
CommonTemplate,
|
||||
CatalogPanel,
|
||||
PreviewPanel,
|
||||
SetterPanel
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<commonTemplate>
|
||||
<CommonTemplate>
|
||||
<template #left>
|
||||
<ResultCatalog />
|
||||
</template>
|
||||
@ -9,11 +9,11 @@
|
||||
<template #right>
|
||||
<ResultSetter />
|
||||
</template>
|
||||
</commonTemplate>
|
||||
</CommonTemplate>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import commonTemplate from '../../components/CommonTemplate.vue'
|
||||
import CommonTemplate from '../../components/CommonTemplate.vue'
|
||||
import ResultCatalog from '../../modules/settingModule/result/CatalogPanel.vue'
|
||||
import ResultPreview from '../../modules/settingModule/result/PreviewPanel.vue'
|
||||
import ResultSetter from '../../modules/settingModule/result/SetterPanel.vue'
|
||||
@ -21,7 +21,7 @@ import ResultSetter from '../../modules/settingModule/result/SetterPanel.vue'
|
||||
export default {
|
||||
name: 'ResultPage',
|
||||
components: {
|
||||
commonTemplate,
|
||||
CommonTemplate,
|
||||
ResultCatalog,
|
||||
ResultPreview,
|
||||
ResultSetter
|
||||
|
@ -1,5 +1,5 @@
|
||||
export const type = {
|
||||
normal: '调查问卷',
|
||||
normal: '基础调查',
|
||||
vote: '投票评选',
|
||||
nps: 'NPS评分',
|
||||
register: '在线报名'
|
||||
|
@ -21,7 +21,17 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
needLogin: true
|
||||
},
|
||||
name: 'QuestionEdit',
|
||||
component: () => import('../pages/edit/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
meta: {
|
||||
needLogin: true
|
||||
},
|
||||
name: 'QuestionEditPage',
|
||||
component: () =>
|
||||
import('../pages/edit/pages/edit/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@ -29,7 +39,19 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
needLogin: true
|
||||
},
|
||||
component: () => import('../pages/edit/pages/EditPage.vue')
|
||||
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',
|
||||
@ -37,11 +59,10 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
needLogin: true
|
||||
},
|
||||
component: () => import('../pages/edit/pages/SettingPage.vue')
|
||||
component: () => import('../pages/edit/pages/setting/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'skin',
|
||||
// name: 'SkinSetting',
|
||||
meta: {
|
||||
needLogin: true
|
||||
},
|
||||
@ -97,7 +118,7 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'login',
|
||||
component: () => import('../pages/login/LoginPage.vue'),
|
||||
meta: {
|
||||
title: '登陆'
|
||||
title: '登录'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -15,7 +15,7 @@ export default {
|
||||
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 || '问卷不存在')
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -52,6 +52,9 @@ export default {
|
||||
},
|
||||
link: ''
|
||||
},
|
||||
questionDataList: []
|
||||
questionDataList: [],
|
||||
logicConf: {
|
||||
showLogicConf: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
setBannerList(state, data) {
|
||||
state.bannerList = data
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
]
|
@ -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)
|
||||
|
@ -80,7 +80,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
emit('change', values)
|
||||
// return values
|
||||
}
|
||||
return {
|
||||
slots,
|
||||
|
@ -27,6 +27,7 @@ import AlertDialog from './components/AlertDialog.vue'
|
||||
|
||||
import LogoIcon from './components/LogoIcon.vue'
|
||||
import { get as _get, upperFirst } from 'lodash-es'
|
||||
import { initRuleEngine } from '@/render/hooks/useRuleEngine.js'
|
||||
|
||||
const store = useStore()
|
||||
const skinConf = computed(() => _get(store, 'state.skinConf', {}))
|
||||
@ -76,14 +77,15 @@ onMounted(async () => {
|
||||
|
||||
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
|
||||
const questionData = {
|
||||
bannerConf,
|
||||
baseConf,
|
||||
bottomConf,
|
||||
dataConf,
|
||||
skinConf,
|
||||
submitConf
|
||||
submitConf,
|
||||
|
||||
}
|
||||
|
||||
document.title = data.title
|
||||
@ -93,6 +95,7 @@ onMounted(async () => {
|
||||
store.commit('setSurveyPath', surveyPath)
|
||||
store.dispatch('init', questionData)
|
||||
store.dispatch('getEncryptInfo')
|
||||
initRuleEngine(logicConf?.showLogicConf)
|
||||
} else {
|
||||
throw new Error(res.errmsg)
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
ref="formGroup"
|
||||
:render-data="item"
|
||||
:rules="rules"
|
||||
:formModel="formModel"
|
||||
:formValues="formValues"
|
||||
@formChange="handleChangeData"
|
||||
/>
|
||||
</template>
|
||||
@ -21,7 +21,7 @@ const store = useStore()
|
||||
|
||||
const renderData = computed(() => store.getters?.renderData)
|
||||
const rules = computed(() => store.state.rules)
|
||||
const formModel = computed(() => store.getters.formModel)
|
||||
const formValues = computed(() => store.state.formValues)
|
||||
|
||||
const handleChangeData = (data: any) => {
|
||||
store.dispatch('changeData', data)
|
||||
|
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<form ref="ruleForm" :model="formModel" :rules="rules">
|
||||
<questionWrapper
|
||||
v-for="item in renderData"
|
||||
:key="item.field"
|
||||
<form ref="ruleForm" :model="formValues" :rules="rules">
|
||||
<div v-for="(item) in renderData" :key="item.field">
|
||||
<QuestionWrapper
|
||||
class="gap"
|
||||
v-bind="$attrs"
|
||||
:moduleConfig="item"
|
||||
@ -10,15 +9,15 @@
|
||||
:indexNumber="item.indexNumber"
|
||||
:showTitle="true"
|
||||
@change="handleChange"
|
||||
></questionWrapper>
|
||||
></QuestionWrapper>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<script setup>
|
||||
import { inject, provide, computed, onBeforeMount } from 'vue'
|
||||
import questionWrapper from '../../materials/questions/QuestionRuleContainer'
|
||||
import QuestionWrapper from './QuestionWrapper.vue'
|
||||
|
||||
const $bus = inject('$bus')
|
||||
|
||||
const props = defineProps({
|
||||
rules: {
|
||||
type: Object,
|
||||
@ -26,7 +25,7 @@ const props = defineProps({
|
||||
return {}
|
||||
}
|
||||
},
|
||||
formModel: {
|
||||
formValues: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
@ -39,6 +38,7 @@ const props = defineProps({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['formChange', 'blur'])
|
||||
|
||||
// 这里不能直接使用change事件,否则父元素监听change的事件,会被绑定到里面的input上
|
||||
@ -50,7 +50,7 @@ const handleChange = (data) => {
|
||||
const fields = []
|
||||
provide('Form', {
|
||||
model: computed(() => {
|
||||
return props.formModel
|
||||
return props.formValues
|
||||
}),
|
||||
rules: computed(() => {
|
||||
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 length = fields.length
|
||||
|
100
web/src/render/components/QuestionWrapper.vue
Normal file
100
web/src/render/components/QuestionWrapper.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<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'
|
||||
import { ruleEngine } from '@/render/hooks/useRuleEngine.js'
|
||||
import { QUESTION_TYPE } from '@/render/constant/index'
|
||||
|
||||
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 === QUESTION_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(QUESTION_TYPE.CHOICES.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(QUESTION_TYPE.RATES.includes(type) && Object.keys(rest.rangeConfig).filter(index => rest.rangeConfig[index].isShowInput).length > 0) {
|
||||
let { rangeConfig, othersValue } = useShowInput(field)
|
||||
moduleConfig.rangeConfig = unref(rangeConfig)
|
||||
moduleConfig.othersValue = unref(othersValue)
|
||||
}
|
||||
|
||||
return {
|
||||
...moduleConfig,
|
||||
options: alloptions,
|
||||
value: formValues.value[props.moduleConfig.field]
|
||||
}
|
||||
})
|
||||
|
||||
const { field } = props.moduleConfig
|
||||
|
||||
const visible = computed(() => {
|
||||
// computed有计算缓存,当match有变化的时候触发重新计算
|
||||
return ruleEngine.match(field, 'question', formValues.value)
|
||||
})
|
||||
|
||||
watch(() => visible.value, (newVal, oldVal) => {
|
||||
// 题目从显示到隐藏,需要清空值
|
||||
const { field, type, innerType } = props.moduleConfig
|
||||
if(!newVal && oldVal) {
|
||||
let value = ''
|
||||
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
||||
if (type === QUESTION_TYPE.CHECKBOX || innerType === QUESTION_TYPE.CHECKBOX) {
|
||||
value = value ? [value] : []
|
||||
}
|
||||
const data = {
|
||||
key: field,
|
||||
value: value
|
||||
}
|
||||
store.commit('changeFormData', data)
|
||||
}
|
||||
})
|
||||
|
||||
const handleChange = (data) => {
|
||||
emit('change', data)
|
||||
// 处理投票题
|
||||
if(props.moduleConfig.type === QUESTION_TYPE.VOTE) {
|
||||
store.dispatch('updateVoteData', data)
|
||||
}
|
||||
}
|
||||
</script>
|
12
web/src/render/constant/index.js
Normal file
12
web/src/render/constant/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
export const QUESTION_TYPE = {
|
||||
VOTE: 'vote',
|
||||
CHECKBOX: 'checkbox',
|
||||
CHOICES: [ // 选择类题型分类
|
||||
'radio',
|
||||
'checkbox',
|
||||
],
|
||||
RATES: [ // 评分题题型分类
|
||||
'radio-star',
|
||||
'radio-nps'
|
||||
]
|
||||
}
|
6
web/src/render/hooks/useRuleEngine.js
Normal file
6
web/src/render/hooks/useRuleEngine.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
|
||||
|
||||
export const ruleEngine = new RuleMatch()
|
||||
export const initRuleEngine = (ruleConf) => {
|
||||
ruleEngine.fromJson(ruleConf)
|
||||
}
|
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 }
|
||||
}
|
17
web/src/render/hooks/useVoteMap.js
Normal file
17
web/src/render/hooks/useVoteMap.js
Normal file
@ -0,0 +1,17 @@
|
||||
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 }
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="index">
|
||||
<progressBar />
|
||||
<ProgressBar />
|
||||
<div class="wrapper" ref="boxRef">
|
||||
<HeaderSetter></HeaderSetter>
|
||||
<div class="content">
|
||||
<MainTitle></MainTitle>
|
||||
<MainRenderer ref="mainRef"></MainRenderer>
|
||||
<submit :validate="validate" :renderData="renderData" @submit="handleSubmit"></submit>
|
||||
<Submit :validate="validate" :renderData="renderData" @submit="handleSubmit"></Submit>
|
||||
<LogoIcon />
|
||||
</div>
|
||||
</div>
|
||||
@ -18,11 +18,11 @@ import { useStore } from 'vuex'
|
||||
|
||||
import HeaderSetter from '../components/HeaderSetter.vue'
|
||||
import MainTitle from '../components/MainTitle.vue'
|
||||
import submit from '../components/SubmitSetter.vue'
|
||||
import Submit from '../components/SubmitSetter.vue'
|
||||
import MainRenderer from '../components/MainRenderer.vue'
|
||||
import AlertDialog from '../components/AlertDialog.vue'
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||
import progressBar from '../components/ProgressBar.vue'
|
||||
import ProgressBar from '../components/ProgressBar.vue'
|
||||
import LogoIcon from '../components/LogoIcon.vue'
|
||||
|
||||
import { submitForm } from '../api/survey'
|
||||
@ -58,12 +58,12 @@ const validate = (cbk: (v: boolean) => void) => {
|
||||
const normalizationRequestBody = () => {
|
||||
const enterTime = store.state.enterTime
|
||||
const encryptInfo = store.state.encryptInfo
|
||||
const formModel = store.getters.formModel
|
||||
const formValues = store.state.formValues
|
||||
const surveyPath = store.state.surveyPath
|
||||
|
||||
const result: any = {
|
||||
surveyPath,
|
||||
data: JSON.stringify(formModel),
|
||||
data: JSON.stringify(formValues),
|
||||
difTime: Date.now() - enterTime,
|
||||
clientTime: Date.now()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -1,117 +1,31 @@
|
||||
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]
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
let othersValue = {}
|
||||
let voteTotal = 0
|
||||
const voteMap = state.voteMap
|
||||
if (/vote/.test(type)) {
|
||||
voteTotal = voteMap?.[questionKey]?.total || 0
|
||||
}
|
||||
// 遍历所有的选项
|
||||
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 (/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]
|
||||
}
|
||||
}
|
||||
|
||||
// 将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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
|
@ -12,5 +12,6 @@ export default {
|
||||
enterTime: null,
|
||||
questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
||||
voteMap: {},
|
||||
encryptInfo: null
|
||||
encryptInfo: null,
|
||||
ruleEngine: null
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user