feat: 显示逻辑稳定版 (#149)
This commit is contained in:
parent
bf5750c634
commit
58ab49e974
@ -5,7 +5,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"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",
|
"local": "ts-node ./scripts/run-local.ts",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"dev": "npm run start:dev",
|
"dev": "npm run start:dev",
|
||||||
|
@ -48,5 +48,8 @@
|
|||||||
"contentConf": {
|
"contentConf": {
|
||||||
"opacity": 100
|
"opacity": 100
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"logicConf": {
|
||||||
|
"showLogicConf": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@ -15,6 +15,7 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.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']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
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']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
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']
|
|
||||||
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']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
@ -27,24 +25,22 @@ declare module 'vue' {
|
|||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
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']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
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']
|
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']
|
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
||||||
IEpCheck: typeof import('~icons/ep/check')['default']
|
IEpCheck: typeof import('~icons/ep/check')['default']
|
||||||
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']
|
||||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
IEpRemove: typeof import('~icons/ep/remove')['default']
|
||||||
@ -58,6 +54,5 @@ declare module 'vue' {
|
|||||||
}
|
}
|
||||||
export interface ComponentCustomProperties {
|
export interface ComponentCustomProperties {
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
vPopover: typeof import('element-plus/es')['ElPopoverDirective']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
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 }
|
||||||
|
}
|
@ -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,13 @@ const onMoveDown = () => {
|
|||||||
isHover.value = false
|
isHover.value = false
|
||||||
}
|
}
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
|
if(unref(hasShowLogic)) {
|
||||||
|
ElMessageBox.alert('该问题被逻辑依赖,请先删除逻辑依赖', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('本次操作会影响数据统计查看,是否确认删除?', '提示', {
|
await ElMessageBox.confirm('本次操作会影响数据统计查看,是否确认删除?', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@ -182,5 +193,11 @@ const onMove = () => {}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.logic-text{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c8c9cd;
|
||||||
|
padding: 0 .4rem;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -22,7 +22,7 @@ import LeftMenu from '@/management/components/LeftMenu.vue'
|
|||||||
|
|
||||||
import CommonTemplate from './components/CommonTemplate.vue'
|
import CommonTemplate from './components/CommonTemplate.vue'
|
||||||
import Navbar from './components/ModuleNavbar.vue'
|
import Navbar from './components/ModuleNavbar.vue'
|
||||||
|
import { initShowLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||||
export default {
|
export default {
|
||||||
name: 'questionEditPage',
|
name: 'questionEditPage',
|
||||||
components: {
|
components: {
|
||||||
@ -34,6 +34,8 @@ 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 || {})
|
||||||
|
await initShowLogicEngine(this.$store.state.edit.schema.logicConf.showLogicConf || {})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error.message)
|
ElMessage.error(error.message)
|
||||||
// 自动跳转回列表页
|
// 自动跳转回列表页
|
||||||
|
@ -5,14 +5,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { get as _get } from 'lodash-es'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import 'element-plus/theme-chalk/src/message.scss'
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
|
||||||
import { get as _get } from 'lodash-es'
|
|
||||||
|
|
||||||
import { publishSurvey, saveSurvey } from '@/management/api/survey'
|
import { publishSurvey, saveSurvey } from '@/management/api/survey'
|
||||||
|
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||||
import buildData from './buildData'
|
import buildData from './buildData'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -29,6 +27,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 +41,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 +64,14 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
this.isPublishing = false
|
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 { saveSurvey } from '@/management/api/survey'
|
||||||
import buildData from './buildData'
|
import buildData from './buildData'
|
||||||
|
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {},
|
components: {},
|
||||||
@ -88,6 +89,14 @@ export default {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateLogicConf() {
|
||||||
|
if(showLogicEngine.value) {
|
||||||
|
showLogicEngine.value.validateSchema()
|
||||||
|
const showLogicConf = showLogicEngine.value.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 +111,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,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 {
|
.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>
|
||||||
|
@ -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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SettingPanel from '../modules/settingModule/SettingPanel.vue'
|
import SettingPanel from '../../modules/settingModule/SettingPanel.vue'
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingPage',
|
name: 'SettingPage',
|
||||||
components: {
|
components: {
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<commonTemplate>
|
<CommonTemplate>
|
||||||
<template #left>
|
<template #left>
|
||||||
<CatalogPanel />
|
<CatalogPanel />
|
||||||
</template>
|
</template>
|
||||||
@ -9,10 +9,10 @@
|
|||||||
<template #right>
|
<template #right>
|
||||||
<SetterPanel />
|
<SetterPanel />
|
||||||
</template>
|
</template>
|
||||||
</commonTemplate>
|
</CommonTemplate>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import commonTemplate from '../../components/CommonTemplate.vue'
|
import CommonTemplate from '../../components/CommonTemplate.vue'
|
||||||
import CatalogPanel from '../../modules/settingModule/skin/CatalogPanel.vue'
|
import CatalogPanel from '../../modules/settingModule/skin/CatalogPanel.vue'
|
||||||
import PreviewPanel from '../../modules/settingModule/skin/PreviewPanel.vue'
|
import PreviewPanel from '../../modules/settingModule/skin/PreviewPanel.vue'
|
||||||
import SetterPanel from '../../modules/settingModule/skin/SetterPanel.vue'
|
import SetterPanel from '../../modules/settingModule/skin/SetterPanel.vue'
|
||||||
@ -20,7 +20,7 @@ import SetterPanel from '../../modules/settingModule/skin/SetterPanel.vue'
|
|||||||
export default {
|
export default {
|
||||||
name: 'ContentPage',
|
name: 'ContentPage',
|
||||||
components: {
|
components: {
|
||||||
commonTemplate,
|
CommonTemplate,
|
||||||
CatalogPanel,
|
CatalogPanel,
|
||||||
PreviewPanel,
|
PreviewPanel,
|
||||||
SetterPanel
|
SetterPanel
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<commonTemplate>
|
<CommonTemplate>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ResultCatalog />
|
<ResultCatalog />
|
||||||
</template>
|
</template>
|
||||||
@ -9,11 +9,11 @@
|
|||||||
<template #right>
|
<template #right>
|
||||||
<ResultSetter />
|
<ResultSetter />
|
||||||
</template>
|
</template>
|
||||||
</commonTemplate>
|
</CommonTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import commonTemplate from '../../components/CommonTemplate.vue'
|
import CommonTemplate from '../../components/CommonTemplate.vue'
|
||||||
import ResultCatalog from '../../modules/settingModule/result/CatalogPanel.vue'
|
import ResultCatalog from '../../modules/settingModule/result/CatalogPanel.vue'
|
||||||
import ResultPreview from '../../modules/settingModule/result/PreviewPanel.vue'
|
import ResultPreview from '../../modules/settingModule/result/PreviewPanel.vue'
|
||||||
import ResultSetter from '../../modules/settingModule/result/SetterPanel.vue'
|
import ResultSetter from '../../modules/settingModule/result/SetterPanel.vue'
|
||||||
@ -21,7 +21,7 @@ import ResultSetter from '../../modules/settingModule/result/SetterPanel.vue'
|
|||||||
export default {
|
export default {
|
||||||
name: 'ResultPage',
|
name: 'ResultPage',
|
||||||
components: {
|
components: {
|
||||||
commonTemplate,
|
CommonTemplate,
|
||||||
ResultCatalog,
|
ResultCatalog,
|
||||||
ResultPreview,
|
ResultPreview,
|
||||||
ResultSetter
|
ResultSetter
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export const type = {
|
export const type = {
|
||||||
normal: '调查问卷',
|
normal: '基础调查',
|
||||||
vote: '投票评选',
|
vote: '投票评选',
|
||||||
nps: 'NPS评分',
|
nps: 'NPS评分',
|
||||||
register: '在线报名'
|
register: '在线报名'
|
||||||
|
@ -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',
|
||||||
@ -37,11 +59,10 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
needLogin: true
|
needLogin: true
|
||||||
},
|
},
|
||||||
component: () => import('../pages/edit/pages/SettingPage.vue')
|
component: () => import('../pages/edit/pages/setting/index.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'skin',
|
path: 'skin',
|
||||||
// name: 'SkinSetting',
|
|
||||||
meta: {
|
meta: {
|
||||||
needLogin: true
|
needLogin: true
|
||||||
},
|
},
|
||||||
@ -97,7 +118,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'login',
|
name: 'login',
|
||||||
component: () => import('../pages/login/LoginPage.vue'),
|
component: () => import('../pages/login/LoginPage.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '登陆'
|
title: '登录'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -15,7 +15,7 @@ export default {
|
|||||||
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,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
setBannerList(state, data) {
|
setBannerList(state, data) {
|
||||||
state.bannerList = data
|
state.bannerList = data
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -27,6 +27,7 @@ import AlertDialog from './components/AlertDialog.vue'
|
|||||||
|
|
||||||
import LogoIcon from './components/LogoIcon.vue'
|
import LogoIcon from './components/LogoIcon.vue'
|
||||||
import { get as _get, upperFirst } from 'lodash-es'
|
import { get as _get, upperFirst } from 'lodash-es'
|
||||||
|
import { initRuleEngine } from '@/render/hooks/useRuleEngine.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const skinConf = computed(() => _get(store, 'state.skinConf', {}))
|
const skinConf = computed(() => _get(store, 'state.skinConf', {}))
|
||||||
@ -76,14 +77,15 @@ onMounted(async () => {
|
|||||||
|
|
||||||
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
|
||||||
const questionData = {
|
const questionData = {
|
||||||
bannerConf,
|
bannerConf,
|
||||||
baseConf,
|
baseConf,
|
||||||
bottomConf,
|
bottomConf,
|
||||||
dataConf,
|
dataConf,
|
||||||
skinConf,
|
skinConf,
|
||||||
submitConf
|
submitConf,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.title = data.title
|
document.title = data.title
|
||||||
@ -93,6 +95,7 @@ onMounted(async () => {
|
|||||||
store.commit('setSurveyPath', surveyPath)
|
store.commit('setSurveyPath', surveyPath)
|
||||||
store.dispatch('init', questionData)
|
store.dispatch('init', questionData)
|
||||||
store.dispatch('getEncryptInfo')
|
store.dispatch('getEncryptInfo')
|
||||||
|
initRuleEngine(logicConf?.showLogicConf)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.errmsg)
|
throw new Error(res.errmsg)
|
||||||
}
|
}
|
||||||
@ -123,4 +126,4 @@ html {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -5,7 +5,7 @@
|
|||||||
ref="formGroup"
|
ref="formGroup"
|
||||||
:render-data="item"
|
:render-data="item"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:formModel="formModel"
|
:formValues="formValues"
|
||||||
@formChange="handleChangeData"
|
@formChange="handleChangeData"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -21,7 +21,7 @@ const store = useStore()
|
|||||||
|
|
||||||
const renderData = computed(() => store.getters?.renderData)
|
const renderData = computed(() => store.getters?.renderData)
|
||||||
const rules = computed(() => store.state.rules)
|
const rules = computed(() => store.state.rules)
|
||||||
const formModel = computed(() => store.getters.formModel)
|
const formValues = computed(() => store.state.formValues)
|
||||||
|
|
||||||
const handleChangeData = (data: any) => {
|
const handleChangeData = (data: any) => {
|
||||||
store.dispatch('changeData', data)
|
store.dispatch('changeData', data)
|
||||||
|
@ -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'
|
||||||
|
|
||||||
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
|
||||||
|
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>
|
<template>
|
||||||
<div class="index">
|
<div class="index">
|
||||||
<progressBar />
|
<ProgressBar />
|
||||||
<div class="wrapper" ref="boxRef">
|
<div class="wrapper" ref="boxRef">
|
||||||
<HeaderSetter></HeaderSetter>
|
<HeaderSetter></HeaderSetter>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<MainTitle></MainTitle>
|
<MainTitle></MainTitle>
|
||||||
<MainRenderer ref="mainRef"></MainRenderer>
|
<MainRenderer ref="mainRef"></MainRenderer>
|
||||||
<submit :validate="validate" :renderData="renderData" @submit="handleSubmit"></submit>
|
<Submit :validate="validate" :renderData="renderData" @submit="handleSubmit"></Submit>
|
||||||
<LogoIcon />
|
<LogoIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -18,11 +18,11 @@ import { useStore } from 'vuex'
|
|||||||
|
|
||||||
import HeaderSetter from '../components/HeaderSetter.vue'
|
import HeaderSetter from '../components/HeaderSetter.vue'
|
||||||
import MainTitle from '../components/MainTitle.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 MainRenderer from '../components/MainRenderer.vue'
|
||||||
import AlertDialog from '../components/AlertDialog.vue'
|
import AlertDialog from '../components/AlertDialog.vue'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.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 LogoIcon from '../components/LogoIcon.vue'
|
||||||
|
|
||||||
import { submitForm } from '../api/survey'
|
import { submitForm } from '../api/survey'
|
||||||
@ -58,12 +58,12 @@ const validate = (cbk: (v: boolean) => void) => {
|
|||||||
const normalizationRequestBody = () => {
|
const normalizationRequestBody = () => {
|
||||||
const enterTime = store.state.enterTime
|
const enterTime = store.state.enterTime
|
||||||
const encryptInfo = store.state.encryptInfo
|
const encryptInfo = store.state.encryptInfo
|
||||||
const formModel = store.getters.formModel
|
const formValues = store.state.formValues
|
||||||
const surveyPath = store.state.surveyPath
|
const surveyPath = store.state.surveyPath
|
||||||
|
|
||||||
const result: any = {
|
const result: any = {
|
||||||
surveyPath,
|
surveyPath,
|
||||||
data: JSON.stringify(formModel),
|
data: JSON.stringify(formValues),
|
||||||
difTime: Date.now() - enterTime,
|
difTime: Date.now() - enterTime,
|
||||||
clientTime: Date.now()
|
clientTime: Date.now()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,117 +1,31 @@
|
|||||||
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 questionVal = formValues[questionKey]
|
|
||||||
|
|
||||||
question.value = questionVal
|
|
||||||
// 本题开启了
|
|
||||||
if (question.showIndex) {
|
if (question.showIndex) {
|
||||||
question.indexNumber = index++
|
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)
|
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