feat: 跳转逻辑稳定版 (#399)
* feat: 跳转逻辑 (#388) * fix: 跳转逻辑优化 (#397) * fix: 跳转逻辑优化 * fix: processJumpSkip逻辑放在题目组件中
This commit is contained in:
parent
e7adb05c3d
commit
c3f8b2a938
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ pnpm-debug.log*
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.history
|
.history
|
||||||
|
components.d.ts
|
||||||
|
|
||||||
# 默认的上传文件夹
|
# 默认的上传文件夹
|
||||||
userUpload
|
userUpload
|
||||||
|
78
web/components.d.ts
vendored
78
web/components.d.ts
vendored
@ -1,78 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
|
||||||
// Generated by unplugin-vue-components
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
|
||||||
export {}
|
|
||||||
|
|
||||||
declare module 'vue' {
|
|
||||||
export interface GlobalComponents {
|
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
|
||||||
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
|
||||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
|
||||||
IEpArrowLeft: typeof import('~icons/ep/arrow-left')['default']
|
|
||||||
IEpArrowRight: typeof import('~icons/ep/arrow-right')['default']
|
|
||||||
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
|
||||||
IEpCheck: typeof import('~icons/ep/check')['default']
|
|
||||||
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
|
|
||||||
IEpClose: typeof import('~icons/ep/close')['default']
|
|
||||||
IEpConnection: typeof import('~icons/ep/connection')['default']
|
|
||||||
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
|
||||||
IEpDelete: typeof import('~icons/ep/delete')['default']
|
|
||||||
IEpIphone: typeof import('~icons/ep/iphone')['default']
|
|
||||||
IEpLoading: typeof import('~icons/ep/loading')['default']
|
|
||||||
IEpMinus: typeof import('~icons/ep/minus')['default']
|
|
||||||
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
|
||||||
IEpMore: typeof import('~icons/ep/more')['default']
|
|
||||||
IEpMoreFilled: typeof import('~icons/ep/more-filled')['default']
|
|
||||||
IEpPlus: typeof import('~icons/ep/plus')['default']
|
|
||||||
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
|
|
||||||
IEpRank: typeof import('~icons/ep/rank')['default']
|
|
||||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
|
||||||
IEpSearch: typeof import('~icons/ep/search')['default']
|
|
||||||
IEpSort: typeof import('~icons/ep/sort')['default']
|
|
||||||
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
|
|
||||||
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
|
|
||||||
IEpTop: typeof import('~icons/ep/top')['default']
|
|
||||||
IEpView: typeof import('~icons/ep/view')['default']
|
|
||||||
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
|
||||||
}
|
|
||||||
export interface ComponentCustomProperties {
|
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,6 +14,8 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logicflow/core": "2.0.0",
|
||||||
|
"@logicflow/extension": "2.0.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
|
@ -33,8 +33,8 @@ export class RuleNode {
|
|||||||
conditions: ConditionNode[] = []
|
conditions: ConditionNode[] = []
|
||||||
scope: string = Scope.Question
|
scope: string = Scope.Question
|
||||||
target: string = ''
|
target: string = ''
|
||||||
constructor(scope: string = Scope.Question, target: string = '') {
|
constructor(target: string = '', scope: string = Scope.Question, id?: string) {
|
||||||
this.id = generateID(PrefixID.Rule)
|
this.id = id || generateID(PrefixID.Rule)
|
||||||
this.scope = scope
|
this.scope = scope
|
||||||
this.target = target
|
this.target = target
|
||||||
}
|
}
|
||||||
@ -54,14 +54,8 @@ export class RuleNode {
|
|||||||
|
|
||||||
export class RuleBuild {
|
export class RuleBuild {
|
||||||
rules: RuleNode[] = []
|
rules: RuleNode[] = []
|
||||||
static instance: RuleBuild
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.rules = []
|
this.rules = []
|
||||||
if (!RuleBuild.instance) {
|
|
||||||
RuleBuild.instance = this
|
|
||||||
}
|
|
||||||
|
|
||||||
return RuleBuild.instance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加条件规则到规则引擎中
|
// 添加条件规则到规则引擎中
|
||||||
@ -71,6 +65,9 @@ export class RuleBuild {
|
|||||||
removeRule(ruleId: string) {
|
removeRule(ruleId: string) {
|
||||||
this.rules = this.rules.filter((rule) => rule.id !== ruleId)
|
this.rules = this.rules.filter((rule) => rule.id !== ruleId)
|
||||||
}
|
}
|
||||||
|
clear() {
|
||||||
|
this.rules = []
|
||||||
|
}
|
||||||
findRule(ruleId: string) {
|
findRule(ruleId: string) {
|
||||||
return this.rules.find((rule) => rule.id === ruleId)
|
return this.rules.find((rule) => rule.id === ruleId)
|
||||||
}
|
}
|
||||||
@ -94,7 +91,7 @@ export class RuleBuild {
|
|||||||
if (ruleConf instanceof Array) {
|
if (ruleConf instanceof Array) {
|
||||||
ruleConf.forEach((rule: any) => {
|
ruleConf.forEach((rule: any) => {
|
||||||
const { scope, target } = rule
|
const { scope, target } = rule
|
||||||
const ruleNode = new RuleNode(scope, target)
|
const ruleNode = new RuleNode(target, scope)
|
||||||
rule.conditions.forEach((condition: any) => {
|
rule.conditions.forEach((condition: any) => {
|
||||||
const { field, operator, value } = condition
|
const { field, operator, value } = condition
|
||||||
const conditionNode = new ConditionNode(field, operator, value)
|
const conditionNode = new ConditionNode(field, operator, value)
|
||||||
@ -112,19 +109,19 @@ export class RuleBuild {
|
|||||||
findTargetsByScope(scope: string) {
|
findTargetsByScope(scope: string) {
|
||||||
return this.rules.filter((rule) => rule.scope === scope).map((rule) => rule.target)
|
return this.rules.filter((rule) => rule.scope === scope).map((rule) => rule.target)
|
||||||
}
|
}
|
||||||
// 实现前置题删除校验
|
findRulesByField(field: string) {
|
||||||
findTargetsByFields(field: string) {
|
return this.rules.filter((rule) => {
|
||||||
const nodes = this.rules.filter((rule: RuleNode) => {
|
return rule.conditions.filter((condition) => condition.field === field).length
|
||||||
const conditions = rule.conditions.filter((item: any) => {
|
|
||||||
return item.field === field
|
|
||||||
})
|
|
||||||
return conditions.length > 0
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
// 实现前置题删除校验
|
||||||
|
findTargetsByField(field: string) {
|
||||||
|
const nodes = this.findRulesByField(field)
|
||||||
return nodes.map((item: any) => {
|
return nodes.map((item: any) => {
|
||||||
return item.target
|
return item.target
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 根据目标题获取显示逻辑
|
// 根据目标题获取关联的逻辑条件
|
||||||
findConditionByTarget(target: string) {
|
findConditionByTarget(target: string) {
|
||||||
return this.rules.filter((rule) => rule.target === target).map((item) => item.conditions)
|
return this.rules.filter((rule) => rule.target === target).map((item) => item.conditions)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Operator, type FieldTypes, type Fact } from './BasicType'
|
|||||||
// 定义条件规则类
|
// 定义条件规则类
|
||||||
export class ConditionNode<F extends string, O extends Operator> {
|
export class ConditionNode<F extends string, O extends Operator> {
|
||||||
// 默认显示
|
// 默认显示
|
||||||
public result: boolean = false
|
public result: boolean | undefined = undefined
|
||||||
constructor(
|
constructor(
|
||||||
public field: F,
|
public field: F,
|
||||||
public operator: O,
|
public operator: O,
|
||||||
@ -16,7 +16,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
return this.field + this.operator + this.value
|
return this.field + this.operator + this.value
|
||||||
}
|
}
|
||||||
|
|
||||||
match(facts: Fact): boolean {
|
match(facts: Fact): boolean | undefined {
|
||||||
// console.log(this.calculateHash())
|
// console.log(this.calculateHash())
|
||||||
// 如果该特征在事实对象中不存在,则直接返回false
|
// 如果该特征在事实对象中不存在,则直接返回false
|
||||||
if (!facts[this.field]) {
|
if (!facts[this.field]) {
|
||||||
@ -45,7 +45,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
this.result = this.value.some((v) => !facts[this.field].includes(v))
|
this.result = this.value.some((v) => !facts[this.field].includes(v))
|
||||||
return this.result
|
return this.result
|
||||||
} else {
|
} else {
|
||||||
this.result = facts[this.field].includes(this.value)
|
this.result = !facts[this.field].includes(this.value)
|
||||||
return this.result
|
return this.result
|
||||||
}
|
}
|
||||||
case Operator.NotEqual:
|
case Operator.NotEqual:
|
||||||
@ -53,7 +53,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
this.result = this.value.every((v) => !facts[this.field].includes(v))
|
this.result = this.value.every((v) => !facts[this.field].includes(v))
|
||||||
return this.result
|
return this.result
|
||||||
} else {
|
} else {
|
||||||
this.result = facts[this.field].includes(this.value)
|
this.result = facts[this.field].toString() !== this.value
|
||||||
return this.result
|
return this.result
|
||||||
}
|
}
|
||||||
// 其他比较操作符的判断逻辑
|
// 其他比较操作符的判断逻辑
|
||||||
@ -69,7 +69,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
|||||||
|
|
||||||
export class RuleNode {
|
export class RuleNode {
|
||||||
conditions: Map<string, ConditionNode<string, Operator>> // 使用哈希表存储条件规则对象
|
conditions: Map<string, ConditionNode<string, Operator>> // 使用哈希表存储条件规则对象
|
||||||
public result: boolean = false
|
public result: boolean | undefined
|
||||||
constructor(
|
constructor(
|
||||||
public target: string,
|
public target: string,
|
||||||
public scope: string
|
public scope: string
|
||||||
@ -83,15 +83,28 @@ export class RuleNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 匹配条件规则
|
// 匹配条件规则
|
||||||
match(fact: Fact) {
|
match(fact: Fact, comparor?: any) {
|
||||||
const res = Array.from(this.conditions.entries()).every(([, value]) => {
|
let res: boolean | undefined = undefined
|
||||||
const res = value.match(fact)
|
if (comparor === 'or') {
|
||||||
if (res) {
|
res = Array.from(this.conditions.entries()).some(([, value]) => {
|
||||||
return true
|
const res = value.match(fact)
|
||||||
} else {
|
if (res) {
|
||||||
return false
|
return true
|
||||||
}
|
} else {
|
||||||
})
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res = Array.from(this.conditions.entries()).every(([, value]) => {
|
||||||
|
const res = value.match(fact)
|
||||||
|
if (res) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.result = res
|
this.result = res
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -121,14 +134,14 @@ export class RuleNode {
|
|||||||
|
|
||||||
export class RuleMatch {
|
export class RuleMatch {
|
||||||
rules: Map<string, RuleNode>
|
rules: Map<string, RuleNode>
|
||||||
static instance: any
|
// static instance: any
|
||||||
constructor() {
|
constructor() {
|
||||||
this.rules = new Map()
|
this.rules = new Map()
|
||||||
if (!RuleMatch.instance) {
|
// if (!RuleMatch.instance) {
|
||||||
RuleMatch.instance = this
|
// RuleMatch.instance = this
|
||||||
}
|
// }
|
||||||
|
|
||||||
return RuleMatch.instance
|
// return RuleMatch.instance
|
||||||
}
|
}
|
||||||
fromJson(ruleConf: any) {
|
fromJson(ruleConf: any) {
|
||||||
if (ruleConf instanceof Array) {
|
if (ruleConf instanceof Array) {
|
||||||
@ -145,6 +158,7 @@ export class RuleMatch {
|
|||||||
this.addRule(ruleNode)
|
this.addRule(ruleNode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加条件规则到规则引擎中
|
// 添加条件规则到规则引擎中
|
||||||
@ -160,22 +174,31 @@ export class RuleMatch {
|
|||||||
this.rules.set(hash, rule)
|
this.rules.set(hash, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 匹配条件规则
|
// 特定目标题规则匹配
|
||||||
match(target: string, scope: string, fact: Fact) {
|
match(target: string, scope: string, fact: Fact, comparor?: any) {
|
||||||
const hash = this.calculateHash(target, scope)
|
const hash = this.calculateHash(target, scope)
|
||||||
|
|
||||||
const rule = this.rules.get(hash)
|
const rule = this.rules.get(hash)
|
||||||
if (rule) {
|
if (rule) {
|
||||||
const result = rule.match(fact)
|
const result = rule.match(fact, comparor)
|
||||||
// this.matchCache.set(hash, result);
|
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
// 默认显示
|
// 默认显示
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 获取条件题关联的多个目标题匹配情况 */
|
||||||
getResult(target: string, scope: string) {
|
getResultsByField(field: string, fact: Fact) {
|
||||||
|
const rules = this.findRulesByField(field)
|
||||||
|
return rules.map(([, rule]) => {
|
||||||
|
return {
|
||||||
|
target: rule.target,
|
||||||
|
result: this.match(rule.target, 'question', fact, 'or')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/* 获取目标题的规则是否匹配 */
|
||||||
|
getResultByTarget(target: string, scope: string) {
|
||||||
const hash = this.calculateHash(target, scope)
|
const hash = this.calculateHash(target, scope)
|
||||||
const rule = this.rules.get(hash)
|
const rule = this.rules.get(hash)
|
||||||
if (rule) {
|
if (rule) {
|
||||||
@ -191,15 +214,18 @@ export class RuleMatch {
|
|||||||
// 假设哈希值计算方法为简单的字符串拼接或其他哈希算法
|
// 假设哈希值计算方法为简单的字符串拼接或其他哈希算法
|
||||||
return target + scope
|
return target + scope
|
||||||
}
|
}
|
||||||
findTargetsByField(field: string) {
|
// 查找条件题的规则
|
||||||
const rules = new Map(
|
findRulesByField(field: string) {
|
||||||
[...this.rules.entries()].filter(([, value]) => {
|
const list = [...this.rules.entries()]
|
||||||
return [...value.conditions.entries()].filter(([, value]) => {
|
const match = list.filter(([, ruleValue]) => {
|
||||||
return value.field === field
|
const list = [...ruleValue.conditions.entries()]
|
||||||
})
|
const res = list.filter(([, conditionValue]) => {
|
||||||
|
const hit = conditionValue.field === field
|
||||||
|
return hit
|
||||||
})
|
})
|
||||||
)
|
return res.length
|
||||||
return [...rules.values()].map((obj) => obj.target)
|
})
|
||||||
|
return match
|
||||||
}
|
}
|
||||||
toJson() {
|
toJson() {
|
||||||
return Array.from(this.rules.entries()).map(([, value]) => {
|
return Array.from(this.rules.entries()).map(([, value]) => {
|
||||||
|
@ -53,7 +53,7 @@ export const defaultQuestionConfig = {
|
|||||||
star: 5,
|
star: 5,
|
||||||
optionOrigin: '',
|
optionOrigin: '',
|
||||||
originType: 'selected',
|
originType: 'selected',
|
||||||
innerType:'',
|
innerType: '',
|
||||||
matrixOptionsRely: '',
|
matrixOptionsRely: '',
|
||||||
numberRange: {
|
numberRange: {
|
||||||
min: {
|
min: {
|
||||||
|
171
web/src/management/hooks/useJumpLogicFlow.ts
Normal file
171
web/src/management/hooks/useJumpLogicFlow.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { useEditStore } from '../stores/edit'
|
||||||
|
import { Operator } from '@/common/logicEngine/BasicType'
|
||||||
|
import { cleanRichText } from '@/common/xss'
|
||||||
|
import { CHOICES } from '@/common/typeEnum'
|
||||||
|
|
||||||
|
export const generateNodes = (questionDataList: [any]) => {
|
||||||
|
let x = 50
|
||||||
|
const y = 300
|
||||||
|
const startNode = [
|
||||||
|
{
|
||||||
|
id: 'start',
|
||||||
|
type: 'start-node',
|
||||||
|
x: 50,
|
||||||
|
y,
|
||||||
|
text: '开始'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const nodes: any[] = questionDataList.map((item) => {
|
||||||
|
x = x + 300
|
||||||
|
let options = []
|
||||||
|
if (CHOICES.includes(item.type)) {
|
||||||
|
options = item?.options.map((option: any) => {
|
||||||
|
return {
|
||||||
|
key: option?.hash,
|
||||||
|
type: cleanRichText(option?.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: item?.field,
|
||||||
|
type: 'q-node',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
properties: {
|
||||||
|
questionType: item?.type,
|
||||||
|
field: item.field,
|
||||||
|
title: cleanRichText(item?.title),
|
||||||
|
options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const endNode = [
|
||||||
|
{
|
||||||
|
id: 'end',
|
||||||
|
type: 'end-node',
|
||||||
|
x: x + 200,
|
||||||
|
y,
|
||||||
|
text: '结束'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return startNode.concat(nodes).concat(endNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 跳转逻辑的初始化 */
|
||||||
|
export const generateLine = (models: Array<any>) => {
|
||||||
|
const acc: Array<any> = []
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const jumpLogicRule = editStore.jumpLogicEngine?.toJson()
|
||||||
|
|
||||||
|
const edges = models.reduce((prev: any, point: any, index: number, array: any[]) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
const previousPoint: any = array[index - 1]
|
||||||
|
if (!previousPoint) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
let edge
|
||||||
|
if (previousPoint?.type === 'start-node') {
|
||||||
|
// 开始节点连接线
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[0].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`,
|
||||||
|
// properties: {
|
||||||
|
draggable: false
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
} else if (previousPoint?.type === 'q-node') {
|
||||||
|
// 生成题目节点连接线
|
||||||
|
// 方案1:以条件节点为主体
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const rules = editStore.jumpLogicEngine.findRulesByField(previousPoint.id)
|
||||||
|
if (!jumpLogicRule.length || !rules.length) {
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[1].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
} else {
|
||||||
|
const hasDefault = rules.filter((i: any) => {
|
||||||
|
return i.conditions.filter((item: any) => item.operator === Operator.NotEqual).length
|
||||||
|
})
|
||||||
|
if (!hasDefault.length) {
|
||||||
|
// 如果规则中没有默认答题跳转则生成一条默认的题目答完链接线
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[1].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
}
|
||||||
|
rules.forEach((rule: any) => {
|
||||||
|
const condition = rule.conditions[0]
|
||||||
|
let sourceAnchorId = `${condition.field}_right`
|
||||||
|
if (condition.operator === 'in') {
|
||||||
|
sourceAnchorId = `${condition.value}_right`
|
||||||
|
}
|
||||||
|
const targetAnchorId = `${rule.target}_left`
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: rule.target,
|
||||||
|
sourceAnchorId: `${sourceAnchorId}`,
|
||||||
|
targetAnchorId: `${targetAnchorId}`,
|
||||||
|
properties: {
|
||||||
|
ruleId: rule.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edge = {
|
||||||
|
type: 'q-edge',
|
||||||
|
sourceNodeId: previousPoint?.id,
|
||||||
|
targetNodeId: point?.id,
|
||||||
|
sourceAnchorId: `${previousPoint.anchors[1].id}`,
|
||||||
|
targetAnchorId: `${point?.anchors[0].id}`,
|
||||||
|
draggable: false
|
||||||
|
}
|
||||||
|
acc.push(edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
})
|
||||||
|
return edges
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNodesStep = (source: string, target: string, questionDataList: any[]) => {
|
||||||
|
const sourceIndex = questionDataList.findIndex((item: any) => item.field === source)
|
||||||
|
const targetIndex = questionDataList.findIndex((item: any) => item.field === target)
|
||||||
|
return targetIndex - sourceIndex
|
||||||
|
}
|
||||||
|
export const getCondition = (sourceInfo: any): any => {
|
||||||
|
const { nodeId, anchorId } = sourceInfo
|
||||||
|
const anchorKey = anchorId.split('_right')[0]
|
||||||
|
if (nodeId === anchorKey) {
|
||||||
|
// 答完跳转
|
||||||
|
return {
|
||||||
|
field: nodeId,
|
||||||
|
operator: Operator.NotEqual,
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 选中optionhash跳转
|
||||||
|
return {
|
||||||
|
field: nodeId,
|
||||||
|
operator: Operator.Include,
|
||||||
|
value: anchorKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
web/src/management/hooks/useJumpLogicInfo.js
Normal file
42
web/src/management/hooks/useJumpLogicInfo.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { computed, unref } from 'vue'
|
||||||
|
import { useQuestionInfo } from './useQuestionInfo'
|
||||||
|
import { useEditStore } from '../stores/edit'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { jumpLogicEngine } = storeToRefs(editStore)
|
||||||
|
|
||||||
|
// 目标题的显示逻辑提示文案
|
||||||
|
export const useJumpLogicInfo = (field) => {
|
||||||
|
const hasJumpLogic = computed(() => {
|
||||||
|
const logicEngine = jumpLogicEngine.value
|
||||||
|
// 判断该题是否作为了跳转逻辑条件
|
||||||
|
const isField = logicEngine?.findTargetsByField(field)?.length > 0
|
||||||
|
// 判断该题是否作为了跳转目标
|
||||||
|
const isTarget = logicEngine?.findConditionByTarget(field)?.length > 0
|
||||||
|
return isField || isTarget
|
||||||
|
})
|
||||||
|
const getJumpLogicText = computed(() => {
|
||||||
|
const logicEngine = jumpLogicEngine.value
|
||||||
|
if (!logicEngine) return
|
||||||
|
// 获取跳转
|
||||||
|
const rules = logicEngine?.findRulesByField(field) || []
|
||||||
|
if (!rules) return
|
||||||
|
const ruleText = rules.map((rule) => {
|
||||||
|
const conditions = rule.conditions.map((condition) => {
|
||||||
|
const { getOptionTitle } = useQuestionInfo(condition.field)
|
||||||
|
if (condition.operator === 'in') {
|
||||||
|
return `<span> 选择了 【${getOptionTitle.value(unref(condition.value)).join('')}】</span>`
|
||||||
|
} else if (condition.operator === 'neq') {
|
||||||
|
return `<span> 答完题目 </span>`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
const { getQuestionTitle } = useQuestionInfo(rule.target)
|
||||||
|
return (
|
||||||
|
conditions.join('') + `<span> 则跳转到 【${getQuestionTitle.value()}】</span> </br>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return ruleText.join('')
|
||||||
|
})
|
||||||
|
return { hasJumpLogic, getJumpLogicText }
|
||||||
|
}
|
@ -8,7 +8,8 @@ export const useQuestionInfo = (field) => {
|
|||||||
|
|
||||||
const getQuestionTitle = computed(() => {
|
const getQuestionTitle = computed(() => {
|
||||||
return () => {
|
return () => {
|
||||||
return questionDataList.value.find((item) => item.field === field)?.title
|
if (field === 'end') return '问卷末尾'
|
||||||
|
return cleanRichText(questionDataList.value.find((item) => item.field === field)?.title)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const getOptionTitle = computed(() => {
|
const getOptionTitle = computed(() => {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
|
|
||||||
|
|
||||||
export const showLogicEngine = ref()
|
|
||||||
export const initShowLogicEngine = (ruleConf) => {
|
|
||||||
showLogicEngine.value = new RuleBuild().fromJson(ruleConf)
|
|
||||||
}
|
|
@ -2,16 +2,19 @@ import { computed, unref } from 'vue'
|
|||||||
import { useQuestionInfo } from './useQuestionInfo'
|
import { useQuestionInfo } from './useQuestionInfo'
|
||||||
import { flatten } from 'lodash-es'
|
import { flatten } from 'lodash-es'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
import { useEditStore } from '../stores/edit'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { showLogicEngine } = storeToRefs(editStore)
|
||||||
|
|
||||||
// 目标题的显示逻辑提示文案
|
// 目标题的显示逻辑提示文案
|
||||||
export const useShowLogicInfo = (field) => {
|
export const useShowLogicInfo = (field) => {
|
||||||
const hasShowLogic = computed(() => {
|
const hasShowLogic = computed(() => {
|
||||||
const logicEngine = showLogicEngine.value
|
const logicEngine = showLogicEngine.value
|
||||||
// 判断该题是否作为了显示逻辑前置题
|
// 判断该题是否作为了显示逻辑前置题
|
||||||
const isField = logicEngine?.findTargetsByFields(field)?.length > 0
|
const isField = logicEngine?.findTargetsByField(field)?.length > 0
|
||||||
// 判断该题是否作为了显示逻辑目标题
|
// 判断该题是否作为了显示逻辑目标题
|
||||||
const isTarget = logicEngine?.findTargetsByScope(field)?.length > 0
|
const isTarget = logicEngine?.findConditionByTarget(field)?.length > 0
|
||||||
return isField || isTarget
|
return isField || isTarget
|
||||||
})
|
})
|
||||||
const getShowLogicText = computed(() => {
|
const getShowLogicText = computed(() => {
|
||||||
|
@ -29,8 +29,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useEditStore } from '@/management/stores/edit'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
|
||||||
|
|
||||||
import BackPanel from '../modules/generalModule/BackPanel.vue'
|
import BackPanel from '../modules/generalModule/BackPanel.vue'
|
||||||
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
|
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
|
||||||
@ -44,6 +43,8 @@ import CooperationPanel from '../modules/contentModule/CooperationPanel.vue'
|
|||||||
const editStore = useEditStore()
|
const editStore = useEditStore()
|
||||||
const { schema, changeSchema } = editStore
|
const { schema, changeSchema } = editStore
|
||||||
const title = computed(() => (editStore.schema?.metaData as any)?.title || '')
|
const title = computed(() => (editStore.schema?.metaData as any)?.title || '')
|
||||||
|
|
||||||
|
const { showLogicEngine, jumpLogicEngine } = storeToRefs(editStore)
|
||||||
// 校验 - 逻辑
|
// 校验 - 逻辑
|
||||||
const updateLogicConf = () => {
|
const updateLogicConf = () => {
|
||||||
let res = {
|
let res = {
|
||||||
@ -67,12 +68,16 @@ const updateLogicConf = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showLogicConf = showLogicEngine.value.toJson()
|
const showLogicConf = showLogicEngine.value.toJson()
|
||||||
|
|
||||||
// 更新逻辑配置
|
// 更新逻辑配置
|
||||||
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jumpLogicConf = jumpLogicEngine.value.toJson()
|
||||||
|
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
<i-ep-close />
|
<i-ep-close />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="logic-text" v-html="getShowLogicText"></div>
|
<div class="logic-text showText" v-html="getShowLogicText"></div>
|
||||||
|
<div class="logic-text jumpText" v-html="getJumpLogicText"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -37,6 +38,7 @@ 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'
|
import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo'
|
||||||
|
import { useJumpLogicInfo } from '@/management/hooks/useJumpLogicInfo'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
qIndex: {
|
qIndex: {
|
||||||
@ -69,6 +71,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['changeSeq', 'select'])
|
const emit = defineEmits(['changeSeq', 'select'])
|
||||||
|
|
||||||
const { getShowLogicText, hasShowLogic } = useShowLogicInfo(props.moduleConfig.field)
|
const { getShowLogicText, hasShowLogic } = useShowLogicInfo(props.moduleConfig.field)
|
||||||
|
const { getJumpLogicText, hasJumpLogic } = useJumpLogicInfo(props.moduleConfig.field)
|
||||||
|
|
||||||
const isHover = ref(false)
|
const isHover = ref(false)
|
||||||
const isMove = ref(false)
|
const isMove = ref(false)
|
||||||
@ -139,7 +142,14 @@ const onMoveDown = () => {
|
|||||||
}
|
}
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
if (unref(hasShowLogic) || getShowLogicText.value) {
|
if (unref(hasShowLogic) || getShowLogicText.value) {
|
||||||
ElMessageBox.alert('该问题被逻辑依赖,请先删除逻辑依赖', '提示', {
|
ElMessageBox.alert('该题目被显示逻辑关联,请先清除逻辑依赖', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (unref(hasJumpLogic)) {
|
||||||
|
ElMessageBox.alert('该题目被跳转逻辑关联,请先清除逻辑依赖', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
})
|
})
|
||||||
|
@ -24,10 +24,8 @@ 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'
|
|
||||||
|
|
||||||
const editStore = useEditStore()
|
const editStore = useEditStore()
|
||||||
const { schema, init, setSurveyId } = editStore
|
const { init, setSurveyId } = editStore
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@ -36,7 +34,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await init()
|
await init()
|
||||||
await initShowLogicEngine(schema.logicConf.showLogicConf || {})
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ElMessage.error(err.message)
|
ElMessage.error(err.message)
|
||||||
|
|
||||||
|
449
web/src/management/pages/edit/modules/logicModule/JumpLogic.vue
Normal file
449
web/src/management/pages/edit/modules/logicModule/JumpLogic.vue
Normal file
@ -0,0 +1,449 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch, toRaw, computed } from 'vue'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import 'element-plus/theme-chalk/src/message.scss'
|
||||||
|
import LogicFlow from '@logicflow/core'
|
||||||
|
import { MiniMap, Control } from '@logicflow/extension'
|
||||||
|
import '@logicflow/extension/lib/style/index.css'
|
||||||
|
import '@logicflow/core/es/index.css'
|
||||||
|
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateNodes,
|
||||||
|
generateLine,
|
||||||
|
getNodesStep,
|
||||||
|
getCondition
|
||||||
|
} from '@/management/hooks/useJumpLogicFlow'
|
||||||
|
|
||||||
|
import NodeExtension from './components/nodeExtension/index'
|
||||||
|
|
||||||
|
const editStore = useEditStore()
|
||||||
|
|
||||||
|
const jumpLogicEngine = computed(() => {
|
||||||
|
return editStore.jumpLogicEngine
|
||||||
|
})
|
||||||
|
const questionDataList = computed(() => {
|
||||||
|
return editStore.schema.questionDataList
|
||||||
|
})
|
||||||
|
const config: Partial<LogicFlow.Options> = {
|
||||||
|
snapline: false,
|
||||||
|
isSilentMode: false,
|
||||||
|
stopScrollGraph: true,
|
||||||
|
stopZoomGraph: true,
|
||||||
|
style: {
|
||||||
|
rect: {
|
||||||
|
rx: 5,
|
||||||
|
ry: 5,
|
||||||
|
strokeWidth: 2
|
||||||
|
},
|
||||||
|
circle: {
|
||||||
|
fill: '#f5f5f5',
|
||||||
|
stroke: '#666'
|
||||||
|
},
|
||||||
|
ellipse: {
|
||||||
|
fill: '#dae8fc',
|
||||||
|
stroke: '#6c8ebf'
|
||||||
|
},
|
||||||
|
polygon: {
|
||||||
|
fill: '#d5e8d4',
|
||||||
|
stroke: '#82b366'
|
||||||
|
},
|
||||||
|
diamond: {
|
||||||
|
fill: '#ffe6cc',
|
||||||
|
stroke: '#d79b00'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: '#b85450',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
adjustEdgeStartAndEnd: true,
|
||||||
|
adjustEdgeStart: false,
|
||||||
|
adjustEdgeEnd: true
|
||||||
|
// grid: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const customTheme: Partial<LogicFlow.Theme> = {
|
||||||
|
baseNode: {
|
||||||
|
stroke: '#FBC559'
|
||||||
|
},
|
||||||
|
nodeText: {
|
||||||
|
overflowMode: 'ellipsis',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontSize: 13
|
||||||
|
},
|
||||||
|
edgeText: {
|
||||||
|
overflowMode: 'ellipsis',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontSize: 13,
|
||||||
|
textWidth: 100
|
||||||
|
}, // 确认 textWidth 是否必传
|
||||||
|
polyline: {
|
||||||
|
stroke: 'red'
|
||||||
|
},
|
||||||
|
rect: {
|
||||||
|
width: 200,
|
||||||
|
height: 40
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
offset: 4, // 箭头长度
|
||||||
|
verticalLength: 2 // 箭头垂直于边的距离
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
nodes: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const lfRef = ref<LogicFlow | null>(null)
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const initGraph = (questionDataList: any) => {
|
||||||
|
const list = toRaw(questionDataList)
|
||||||
|
if (list.length) {
|
||||||
|
const nodes = generateNodes(list)
|
||||||
|
let models: any[] = []
|
||||||
|
nodes.forEach((item: any) => {
|
||||||
|
const nodeModel = lfRef.value?.addNode(item)
|
||||||
|
models.push(nodeModel)
|
||||||
|
})
|
||||||
|
const edges = generateLine(models)
|
||||||
|
edges.forEach((item: any) => {
|
||||||
|
const edgeModel = lfRef.value?.addEdge(item)
|
||||||
|
if (edgeModel && ('start_node' === item.sourceNodeId || 'end_node' === item.targetNodeId)) {
|
||||||
|
edgeModel.draggable = false
|
||||||
|
edgeModel.isSelected = false
|
||||||
|
edgeModel.isShowAdjustPoint = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerEvents = (lf: LogicFlow) => {
|
||||||
|
// 更新从选项拉出的逻辑
|
||||||
|
lf.on('anchor:drop', ({ edgeModel }) => {
|
||||||
|
/* 添加规则 **/
|
||||||
|
const { sourceNodeId, sourceAnchorId, targetNodeId } = edgeModel
|
||||||
|
const target = targetNodeId
|
||||||
|
const { field, operator, value } = getCondition({
|
||||||
|
anchorId: sourceAnchorId,
|
||||||
|
nodeId: sourceNodeId
|
||||||
|
})
|
||||||
|
const conditionNode = new ConditionNode(field, operator, value)
|
||||||
|
|
||||||
|
const ruleNode = new RuleNode(target)
|
||||||
|
ruleNode.addCondition(conditionNode)
|
||||||
|
edgeModel.setProperties({
|
||||||
|
ruleId: ruleNode.id,
|
||||||
|
conditionId: conditionNode.id
|
||||||
|
})
|
||||||
|
jumpLogicEngine.value.addRule(ruleNode)
|
||||||
|
})
|
||||||
|
// 调整边的起点和终点,更新题目默认的连接线
|
||||||
|
lf.on('edge:exchange-node', ({ data }: any) => {
|
||||||
|
console.log('edge:exchange-node', { data })
|
||||||
|
/* 更新规则目标 **/
|
||||||
|
|
||||||
|
const { newEdge, oldEdge } = data
|
||||||
|
|
||||||
|
const ruleId = oldEdge.properties.ruleId
|
||||||
|
// 如果新的连接线,默认的步长 == 1
|
||||||
|
if (
|
||||||
|
getNodesStep(newEdge.sourceNodeId, newEdge.targetNodeId, questionDataList.value) === 1 &&
|
||||||
|
ruleId
|
||||||
|
) {
|
||||||
|
/** 删除逻辑。step n --> 1 */
|
||||||
|
console.log('删除逻辑。step n --> 1')
|
||||||
|
jumpLogicEngine.value.removeRule(ruleId)
|
||||||
|
} else {
|
||||||
|
if (ruleId) {
|
||||||
|
/** 更新逻辑. step n --> m */
|
||||||
|
console.log('更新逻辑. step n --> m ')
|
||||||
|
// const ruleId = oldEdge.properties.ruleId
|
||||||
|
const ruleNode = jumpLogicEngine.value.findRule(ruleId)
|
||||||
|
ruleNode.setTarget(newEdge.targetNodeId)
|
||||||
|
} else {
|
||||||
|
/** 添加逻辑。step 1 --> n */
|
||||||
|
console.log('添加逻辑。step 1 --> n')
|
||||||
|
const newEdgeModel = lf.graphModel.getEdgeModelById(newEdge.id)
|
||||||
|
|
||||||
|
const { sourceNodeId, sourceAnchorId } = newEdge
|
||||||
|
const { field, operator, value } = getCondition({
|
||||||
|
anchorId: sourceAnchorId,
|
||||||
|
nodeId: sourceNodeId
|
||||||
|
})
|
||||||
|
const conditionNode = new ConditionNode(field, operator, value)
|
||||||
|
|
||||||
|
const ruleNode = new RuleNode(newEdge.targetNodeId)
|
||||||
|
ruleNode.addCondition(conditionNode)
|
||||||
|
newEdgeModel?.setProperties({
|
||||||
|
ruleId: ruleNode.id,
|
||||||
|
conditionId: conditionNode.id
|
||||||
|
})
|
||||||
|
jumpLogicEngine.value.addRule(ruleNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
if (containerRef.value) {
|
||||||
|
const lf = new LogicFlow({
|
||||||
|
...config,
|
||||||
|
container: containerRef.value,
|
||||||
|
// height: 700,
|
||||||
|
translateCenter: true,
|
||||||
|
multipleSelectKey: 'shift',
|
||||||
|
disabledTools: ['multipleSelect'],
|
||||||
|
autoExpand: true,
|
||||||
|
adjustEdgeStartAndEnd: true,
|
||||||
|
allowRotate: false,
|
||||||
|
edgeTextEdit: false,
|
||||||
|
nodeTextEdit: false,
|
||||||
|
keyboard: {
|
||||||
|
enabled: true,
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
keys: ['backspace'],
|
||||||
|
callback: () => {
|
||||||
|
ElMessageBox.confirm('确定要删除吗?', '删除提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
const elements = lf.getSelectElements(true)
|
||||||
|
lf.clearSelectElements()
|
||||||
|
elements.edges.forEach((edge) => {
|
||||||
|
console.log({ edge })
|
||||||
|
const { sourceNodeId, sourceAnchorId } = edge
|
||||||
|
if (sourceAnchorId?.split('_right')[0] === sourceNodeId) {
|
||||||
|
ElMessage({
|
||||||
|
message: '题目答完跳转的连接线不可以删除',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const { properties } = edge
|
||||||
|
jumpLogicEngine.value.removeRule(properties?.ruleId)
|
||||||
|
lf.deleteEdge(edge.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(42, elements)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
partial: false,
|
||||||
|
background: {
|
||||||
|
color: '#FFFFFF'
|
||||||
|
},
|
||||||
|
edgeTextDraggable: false,
|
||||||
|
edgeType: 'bezier',
|
||||||
|
style: {
|
||||||
|
inputText: {
|
||||||
|
background: 'black',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
idGenerator(type) {
|
||||||
|
return type + '_' + Math.random()
|
||||||
|
},
|
||||||
|
plugins: [NodeExtension, MiniMap, Control],
|
||||||
|
pluginsOptions: {
|
||||||
|
miniMap: {
|
||||||
|
width: 284,
|
||||||
|
height: 84,
|
||||||
|
isShowHeader: false,
|
||||||
|
isShowCloseIcon: false,
|
||||||
|
leftPosition: 0,
|
||||||
|
rightPosition: 0,
|
||||||
|
bottomPosition: 50,
|
||||||
|
showEdge: false,
|
||||||
|
isShow: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lf.setTheme(customTheme)
|
||||||
|
registerEvents(lf)
|
||||||
|
const control = lf.extension.control as Control
|
||||||
|
control.removeItem('zoom-out')
|
||||||
|
control.removeItem('zoom-in')
|
||||||
|
control.removeItem('reset')
|
||||||
|
control.removeItem('reset')
|
||||||
|
control.removeItem('undo')
|
||||||
|
control.removeItem('redo')
|
||||||
|
control.addItem({
|
||||||
|
key: 'zoom-out',
|
||||||
|
iconClass: 'iconfont icon-suoxiao',
|
||||||
|
title: '缩小流程图',
|
||||||
|
text: '缩小',
|
||||||
|
onClick: () => {
|
||||||
|
lf.zoom(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
control.addItem({
|
||||||
|
key: 'zoom-in',
|
||||||
|
iconClass: 'iconfont icon-fangda',
|
||||||
|
title: '放大流程图',
|
||||||
|
text: '放大',
|
||||||
|
onClick: () => {
|
||||||
|
lf.zoom(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
control.addItem({
|
||||||
|
key: 'reset',
|
||||||
|
iconClass: 'iconfont icon-shiying',
|
||||||
|
title: '恢复流程原有尺寸',
|
||||||
|
text: '适应',
|
||||||
|
onClick: () => {
|
||||||
|
lf.resetZoom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
lf.render(data)
|
||||||
|
const miniMap = lf.extension.miniMap as MiniMap
|
||||||
|
miniMap?.show()
|
||||||
|
|
||||||
|
lfRef.value = lf
|
||||||
|
if (questionDataList.value.length) {
|
||||||
|
initGraph(questionDataList.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => questionDataList.value,
|
||||||
|
(value) => {
|
||||||
|
const list = toRaw(value)
|
||||||
|
if (list.length) {
|
||||||
|
initGraph(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="containerRef" id="graph" class="viewport"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.viewport {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* q-node */
|
||||||
|
.table-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node::before {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #fbc559;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-1::before {
|
||||||
|
background: #9673a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-2::before {
|
||||||
|
background: #dae8fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-3::before {
|
||||||
|
background: #82b366;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-node.table-color-4::before {
|
||||||
|
background: #f8cecc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-name {
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: left;
|
||||||
|
color: #4a4c5b;
|
||||||
|
width: 180px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-feild {
|
||||||
|
height: 28x;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 28px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #4a4c5b;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feild-type {
|
||||||
|
color: #9f9c9f;
|
||||||
|
}
|
||||||
|
/* 自定义锚点样式 */
|
||||||
|
.custom-anchor {
|
||||||
|
cursor: crosshair;
|
||||||
|
fill: #d9d9d9;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
rx: 3;
|
||||||
|
ry: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-anchor:hover {
|
||||||
|
fill: #ff7f0e;
|
||||||
|
stroke: #ff7f0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-node-not-allow .custom-anchor:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
fill: #d9d9d9;
|
||||||
|
stroke: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incomming-anchor {
|
||||||
|
stroke: #d79b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoing-anchor {
|
||||||
|
stroke: #82b366;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-mini-map {
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
background: #f6f7f9;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 10px -2px rgba(82, 82, 102, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.lf-control {
|
||||||
|
.iconfont {
|
||||||
|
color: #6e707c;
|
||||||
|
}
|
||||||
|
color: #6e707c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -9,7 +9,7 @@ import { storeToRefs } from 'pinia'
|
|||||||
import { useEditStore } from '@/management/stores/edit'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
import RulePanel from '../../modules/logicModule/RulePanel.vue'
|
import RulePanel from './components/RulePanel.vue'
|
||||||
import { filterQuestionPreviewData } from '@/management/utils/index'
|
import { filterQuestionPreviewData } from '@/management/utils/index'
|
||||||
|
|
||||||
const editStore = useEditStore()
|
const editStore = useEditStore()
|
||||||
@ -23,11 +23,11 @@ provide('renderData', renderData)
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.logic-wrapper {
|
.logic-wrapper {
|
||||||
height: calc(100% - 120px);
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 12px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
// position: fixed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -52,7 +52,10 @@ import { ElMessageBox } from 'element-plus'
|
|||||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||||
import { RuleNode } from '@/common/logicEngine/RuleBuild'
|
import { RuleNode } from '@/common/logicEngine/RuleBuild'
|
||||||
import { cleanRichText } from '@/common/xss'
|
import { cleanRichText } from '@/common/xss'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { showLogicEngine } = storeToRefs(editStore)
|
||||||
import ConditionView from './ConditionView.vue'
|
import ConditionView from './ConditionView.vue'
|
||||||
|
|
||||||
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||||
|
@ -21,8 +21,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { shallowRef, computed } from 'vue'
|
import { shallowRef, computed } from 'vue'
|
||||||
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
||||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
import RuleNodeView from './components/RuleNodeView.vue'
|
import { storeToRefs } from 'pinia'
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { showLogicEngine } = storeToRefs(editStore)
|
||||||
|
|
||||||
|
import RuleNodeView from './RuleNodeView.vue'
|
||||||
|
|
||||||
const list = computed(() => {
|
const list = computed(() => {
|
||||||
return showLogicEngine.value?.rules || []
|
return showLogicEngine.value?.rules || []
|
@ -0,0 +1,55 @@
|
|||||||
|
import { BezierEdge, BezierEdgeModel } from '@logicflow/core'
|
||||||
|
|
||||||
|
class CustomEdge2 extends BezierEdge {}
|
||||||
|
|
||||||
|
class CustomEdgeModel2 extends BezierEdgeModel {
|
||||||
|
getEdgeStyle() {
|
||||||
|
const style = super.getEdgeStyle()
|
||||||
|
// svg属性
|
||||||
|
style.strokeWidth = 1
|
||||||
|
style.stroke = '#ababac'
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 重写此方法,使保存数据是能带上锚点数据。
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
const data = super.getData()
|
||||||
|
data.sourceAnchorId = this.sourceAnchorId
|
||||||
|
data.targetAnchorId = this.targetAnchorId
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 给边自定义方案,使其支持基于锚点的位置更新边的路径
|
||||||
|
*/
|
||||||
|
updatePathByAnchor() {
|
||||||
|
// TODO
|
||||||
|
const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId)
|
||||||
|
const sourceAnchor = sourceNodeModel
|
||||||
|
.getDefaultAnchor()
|
||||||
|
.find((anchor) => anchor.id === this.sourceAnchorId)
|
||||||
|
const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId)
|
||||||
|
const targetAnchor = targetNodeModel
|
||||||
|
.getDefaultAnchor()
|
||||||
|
.find((anchor) => anchor.id === this.targetAnchorId)
|
||||||
|
const startPoint = {
|
||||||
|
x: sourceAnchor.x,
|
||||||
|
y: sourceAnchor.y
|
||||||
|
}
|
||||||
|
this.updateStartPoint(startPoint)
|
||||||
|
const endPoint = {
|
||||||
|
x: targetAnchor.x,
|
||||||
|
y: targetAnchor.y
|
||||||
|
}
|
||||||
|
this.updateEndPoint(endPoint)
|
||||||
|
// 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
|
||||||
|
this.pointsList = []
|
||||||
|
this.initPoints()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'q-edge',
|
||||||
|
view: CustomEdge2,
|
||||||
|
model: CustomEdgeModel2
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import StartNode from './nodes/StartNode'
|
||||||
|
import EndNode from './nodes/EndNode'
|
||||||
|
import QNode from './nodes/QNode'
|
||||||
|
import QEdge from './edges/QEdge'
|
||||||
|
|
||||||
|
class NodeExtension {
|
||||||
|
static pluginName = 'NodeExtension'
|
||||||
|
constructor({ lf }) {
|
||||||
|
lf.register(StartNode)
|
||||||
|
lf.register(EndNode)
|
||||||
|
lf.register(QNode)
|
||||||
|
lf.register(QEdge)
|
||||||
|
lf.setDefaultEdgeType('q-edge')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeExtension
|
@ -0,0 +1,41 @@
|
|||||||
|
import { CircleNode, CircleNodeModel } from '@logicflow/core'
|
||||||
|
|
||||||
|
class EndNodeModel extends CircleNodeModel {
|
||||||
|
constructor(data, graphModel) {
|
||||||
|
data.text = {
|
||||||
|
value: data.text,
|
||||||
|
x: data.x,
|
||||||
|
y: data.y
|
||||||
|
}
|
||||||
|
super(data, graphModel)
|
||||||
|
|
||||||
|
this.r = 30
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 重写定义锚点
|
||||||
|
*/
|
||||||
|
getDefaultAnchor() {
|
||||||
|
const { x, y, id, width } = this
|
||||||
|
const anchors = [
|
||||||
|
{
|
||||||
|
x: x - width / 2,
|
||||||
|
y: y,
|
||||||
|
id: `${id}_left`,
|
||||||
|
type: 'left'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return anchors
|
||||||
|
}
|
||||||
|
setIsShowAnchor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
isAllowConnectedAsSource() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'end-node',
|
||||||
|
model: EndNodeModel,
|
||||||
|
view: CircleNode
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
import { HtmlNode, HtmlNodeModel, h } from '@logicflow/core'
|
||||||
|
|
||||||
|
class QNode extends HtmlNode {
|
||||||
|
/**
|
||||||
|
* 1.1.7版本后支持在view中重写锚点形状。
|
||||||
|
* 重写锚点新增
|
||||||
|
*/
|
||||||
|
getAnchorShape(anchorData) {
|
||||||
|
const { x, y, type } = anchorData
|
||||||
|
return h('rect', {
|
||||||
|
x: x - 5,
|
||||||
|
y: y - 5,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
className: `custom-anchor ${type === 'left' ? 'incomming-anchor' : 'outgoing-anchor'}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setHtml(rootEl) {
|
||||||
|
rootEl.innerHTML = ''
|
||||||
|
const {
|
||||||
|
properties: { options = [], title }
|
||||||
|
} = this.props.model
|
||||||
|
rootEl.setAttribute('class', 'table-container')
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.className = `table-node`
|
||||||
|
const tableNameElement = document.createElement('div')
|
||||||
|
tableNameElement.innerHTML = title
|
||||||
|
tableNameElement.className = 'table-name'
|
||||||
|
container.appendChild(tableNameElement)
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const item = options[i]
|
||||||
|
const itemElement = document.createElement('div')
|
||||||
|
itemElement.className = 'table-feild'
|
||||||
|
const itemKey = document.createElement('span')
|
||||||
|
itemKey.innerHTML = item.type
|
||||||
|
itemElement.appendChild(itemKey)
|
||||||
|
// itemKey.innerText = item.key;
|
||||||
|
// const itemType = document.createElement('span');
|
||||||
|
// itemType.innerHTML = item.type;
|
||||||
|
// itemType.className = 'feild-type';
|
||||||
|
// itemElement.appendChild(itemType);
|
||||||
|
fragment.appendChild(itemElement)
|
||||||
|
}
|
||||||
|
container.appendChild(fragment)
|
||||||
|
rootEl.appendChild(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QNodeModel extends HtmlNodeModel {
|
||||||
|
getOutlineStyle() {
|
||||||
|
const style = super.getOutlineStyle()
|
||||||
|
style.stroke = 'none'
|
||||||
|
style.hover.stroke = 'none'
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
// 如果不用修改锚地形状,可以重写颜色相关样式
|
||||||
|
getAnchorStyle(anchorInfo) {
|
||||||
|
const style = super.getAnchorStyle()
|
||||||
|
if (anchorInfo.type === 'left') {
|
||||||
|
style.fill = 'red'
|
||||||
|
style.hover.fill = 'transparent'
|
||||||
|
style.hover.stroke = 'transpanrent'
|
||||||
|
style.className = 'lf-hide-default'
|
||||||
|
} else {
|
||||||
|
style.fill = 'green'
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
setAttributes() {
|
||||||
|
this.width = 200
|
||||||
|
const {
|
||||||
|
properties: { options = [] }
|
||||||
|
} = this
|
||||||
|
this.height = 60 + options.length * 28
|
||||||
|
const circleOnlyAsTarget = {
|
||||||
|
message: '只允许从右边的锚点连出',
|
||||||
|
validate: (sourceNode, targetNode, sourceAnchor) => {
|
||||||
|
return sourceAnchor.type === 'right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sourceRules.push(circleOnlyAsTarget)
|
||||||
|
this.targetRules.push({
|
||||||
|
message: '只允许连接左边的锚点',
|
||||||
|
validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
|
||||||
|
return targetAnchor.type === 'left'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getDefaultAnchor() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
properties: { options }
|
||||||
|
} = this
|
||||||
|
const anchors = [
|
||||||
|
{
|
||||||
|
x: x - width / 2 + 10,
|
||||||
|
y: y - height / 2 + 60 - 28,
|
||||||
|
id: `${id}_left`,
|
||||||
|
edgeAddable: false,
|
||||||
|
type: 'left'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: x + width / 2 - 10,
|
||||||
|
y: y - height / 2 + 60 - 28,
|
||||||
|
id: `${id}_right`,
|
||||||
|
edgeAddable: false,
|
||||||
|
type: 'right'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
options.forEach((feild, index) => {
|
||||||
|
const anchorId = `${feild.key}_right`
|
||||||
|
const { edges } = this.outgoing
|
||||||
|
let edgeAddable = true
|
||||||
|
if (edges.length) {
|
||||||
|
const sourceAnchorIds = edges.map((edge) => edge.sourceAnchorId)
|
||||||
|
edgeAddable = !sourceAnchorIds.includes(anchorId)
|
||||||
|
}
|
||||||
|
anchors.push({
|
||||||
|
x: x + width / 2 - 10,
|
||||||
|
y: y - height / 2 + 60 - 28 + (index + 1) * 30,
|
||||||
|
id: anchorId,
|
||||||
|
type: 'right',
|
||||||
|
key: feild.key,
|
||||||
|
edgeAddable
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return anchors
|
||||||
|
}
|
||||||
|
setIsShowAnchor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 获取当前节点作为边的起始节点规则。
|
||||||
|
// getConnectedSourceRules() {
|
||||||
|
// const rules = super.getConnectedSourceRules();
|
||||||
|
// // 开始节点的锚点只能拉出一条连接线
|
||||||
|
// const geteWayOnlyAsTarget = {
|
||||||
|
// message: "开始节点的锚点只能拉出一条连接线",
|
||||||
|
// validate: (
|
||||||
|
// source,
|
||||||
|
// target,
|
||||||
|
// sourceAnchor,
|
||||||
|
// targetAnchor
|
||||||
|
// ) => {
|
||||||
|
// // 获取该节点下目标连接线
|
||||||
|
// const edges = this.graphModel.getNodeOutgoingEdge(source.id);
|
||||||
|
// console.log({edges});
|
||||||
|
// // 如果连接线的锚点id存在则不允许链接
|
||||||
|
// const sourceAnchorIds = edges.map(edge => edge.sourceAnchorId);
|
||||||
|
// // 判断该新拉出的边是否已经有了目标链接,有的话disable
|
||||||
|
// let isValid = true;
|
||||||
|
// if (sourceAnchorIds.includes(sourceAnchor.id)) {
|
||||||
|
// isValid = false;
|
||||||
|
// }
|
||||||
|
// console.log(edges[0].targetNodeId,target.id, sourceAnchorIds, sourceAnchor.id, isValid)
|
||||||
|
// return isValid;
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// // 如果该题存在无条件跳转则禁用选项跳转
|
||||||
|
|
||||||
|
// // 如果该题存在选项跳转则禁用无条件跳转
|
||||||
|
|
||||||
|
// rules.push(geteWayOnlyAsTarget);
|
||||||
|
// return rules;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'q-node',
|
||||||
|
model: QNodeModel,
|
||||||
|
view: QNode
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { CircleNode, CircleNodeModel } from '@logicflow/core'
|
||||||
|
|
||||||
|
class StartNodeModel extends CircleNodeModel {
|
||||||
|
constructor(data, graphModel) {
|
||||||
|
data.text = {
|
||||||
|
value: data.text,
|
||||||
|
x: data.x,
|
||||||
|
y: data.y
|
||||||
|
}
|
||||||
|
super(data, graphModel)
|
||||||
|
|
||||||
|
this.r = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写定义锚点
|
||||||
|
*/
|
||||||
|
getDefaultAnchor() {
|
||||||
|
const { x, y, id, width } = this
|
||||||
|
const anchors = [
|
||||||
|
{
|
||||||
|
x: x + width / 2,
|
||||||
|
y: y,
|
||||||
|
id: `${id}_right`,
|
||||||
|
type: 'right'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return anchors
|
||||||
|
}
|
||||||
|
setIsShowAnchor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
isAllowConnectedAsTarget() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
type: 'start-node',
|
||||||
|
model: StartNodeModel,
|
||||||
|
view: CircleNode
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
# 说明
|
||||||
|
|
||||||
|
参考node-red样式,以logicflow插件的方式实现。
|
@ -0,0 +1,23 @@
|
|||||||
|
.custom-anchor {
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: #d9d9d9;
|
||||||
|
cursor: crosshair;
|
||||||
|
rx: 3;
|
||||||
|
ry: 3;
|
||||||
|
}
|
||||||
|
.custom-anchor:hover {
|
||||||
|
fill: #ff7f0e;
|
||||||
|
stroke: #ff7f0e;
|
||||||
|
}
|
||||||
|
.node-red-palette {
|
||||||
|
width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
.node-red-start {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
/* 求字符串的字节长度 */
|
||||||
|
export const getBytesLength = (word) => {
|
||||||
|
if (!word) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
let totalLength = 0
|
||||||
|
for (let i = 0; i < word.length; i++) {
|
||||||
|
const c = word.charCodeAt(i)
|
||||||
|
if (word.match(/[A-Z]/)) {
|
||||||
|
totalLength += 1.5
|
||||||
|
} else if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) {
|
||||||
|
totalLength += 1
|
||||||
|
} else {
|
||||||
|
totalLength += 1.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalLength
|
||||||
|
}
|
85
web/src/management/pages/edit/pages/edit/LogicIndex.vue
Normal file
85
web/src/management/pages/edit/pages/edit/LogicIndex.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<el-tabs
|
||||||
|
:modelValue="activeName"
|
||||||
|
class="loigc-tabs"
|
||||||
|
:before-leave="beforeTabLeave"
|
||||||
|
@tab-change="handleChange"
|
||||||
|
>
|
||||||
|
<el-tab-pane label="显示逻辑" name="showLogic">
|
||||||
|
<ShowLogic v-if="activeName == 'showLogic'" />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="跳转逻辑" name="jumpLogic" class="logic-wrapper">
|
||||||
|
<JumpLogic v-if="activeName == 'jumpLogic'" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import ShowLogic from '../../modules/logicModule/ShowLogic.vue'
|
||||||
|
import JumpLogic from '../../modules/logicModule/JumpLogic.vue'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { showLogicEngine, jumpLogicEngine } = storeToRefs(editStore)
|
||||||
|
const props = defineProps({
|
||||||
|
active: String
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const activeName = computed(() => {
|
||||||
|
return props.active || 'showLogic'
|
||||||
|
})
|
||||||
|
const beforeTabLeave = async () => {
|
||||||
|
if (activeName.value === 'showLogic' && !showLogicEngine.value.rules.length) return true
|
||||||
|
if (activeName.value === 'jumpLogic' && !jumpLogicEngine.value.rules.length) return true
|
||||||
|
const title = `提示`
|
||||||
|
const text = `切换到${activeName.value === 'showLogic' ? '跳转' : '显示'}设置后,将丢失当前${activeName.value === 'showLogic' ? '显示' : '跳转'}逻辑设置`
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(text, title, {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
if (activeName.value === 'showLogic') {
|
||||||
|
// 清空显示逻辑逻辑
|
||||||
|
showLogicEngine.value.clear()
|
||||||
|
} else {
|
||||||
|
// 清空跳转逻辑逻辑
|
||||||
|
jumpLogicEngine.value.clear()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleChange = (name: any) => {
|
||||||
|
router.push({
|
||||||
|
name: 'LogicIndex',
|
||||||
|
query: { active: name }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loigc-tabs {
|
||||||
|
width: 98%;
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
padding: 10px;
|
||||||
|
position: relative;
|
||||||
|
top: 24px;
|
||||||
|
|
||||||
|
background-color: #fff;
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
:deep(el-tab-pane) {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.logic-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -37,7 +37,9 @@ const activeRouter = ref(route.name)
|
|||||||
watch(
|
watch(
|
||||||
activeRouter,
|
activeRouter,
|
||||||
(val: any) => {
|
(val: any) => {
|
||||||
router.push({ name: val })
|
// 避免编辑页刷新丢失query
|
||||||
|
const query = route.query
|
||||||
|
router.push({ name: val, query })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true
|
immediate: true
|
||||||
|
@ -57,7 +57,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
needLogin: true
|
needLogin: true
|
||||||
},
|
},
|
||||||
component: () => import('../pages/edit/pages/edit/LogicEditPage.vue')
|
component: () => import('../pages/edit/pages/edit/LogicIndex.vue'),
|
||||||
|
props: (route) => ({ active: route.query.active })
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -21,6 +21,7 @@ import { getBannerData } from '@/management/api/skin.js'
|
|||||||
import { getCollaboratorPermissions } from '@/management/api/space'
|
import { getCollaboratorPermissions } from '@/management/api/space'
|
||||||
import useEditGlobalBaseConf, { type TypeMethod } from './composables/useEditGlobalBaseConf'
|
import useEditGlobalBaseConf, { type TypeMethod } from './composables/useEditGlobalBaseConf'
|
||||||
import { CODE_MAP } from '../api/base'
|
import { CODE_MAP } from '../api/base'
|
||||||
|
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
|
||||||
|
|
||||||
const innerMetaConfig = {
|
const innerMetaConfig = {
|
||||||
submit: {
|
submit: {
|
||||||
@ -83,10 +84,12 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
|||||||
pageEditOne: 1,
|
pageEditOne: 1,
|
||||||
pageConf: [], // 分页逻辑
|
pageConf: [], // 分页逻辑
|
||||||
logicConf: {
|
logicConf: {
|
||||||
showLogicConf: []
|
showLogicConf: [],
|
||||||
|
jumpLogicConf: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } =
|
||||||
|
useLogicEngine(schema)
|
||||||
function initSchema({ metaData, codeData }: { metaData: any; codeData: any }) {
|
function initSchema({ metaData, codeData }: { metaData: any; codeData: any }) {
|
||||||
schema.metaData = metaData
|
schema.metaData = metaData
|
||||||
schema.bannerConf = _merge({}, schema.bannerConf, codeData.bannerConf)
|
schema.bannerConf = _merge({}, schema.bannerConf, codeData.bannerConf)
|
||||||
@ -132,6 +135,9 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
initializeSchemaCallBack()
|
initializeSchemaCallBack()
|
||||||
|
|
||||||
|
initShowLogicEngine()
|
||||||
|
initJumpLogicEngine()
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.errmsg || '问卷不存在')
|
throw new Error(res.errmsg || '问卷不存在')
|
||||||
}
|
}
|
||||||
@ -140,7 +146,9 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
|||||||
return {
|
return {
|
||||||
schema,
|
schema,
|
||||||
initSchema,
|
initSchema,
|
||||||
getSchemaFromRemote
|
getSchemaFromRemote,
|
||||||
|
showLogicEngine,
|
||||||
|
jumpLogicEngine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +307,7 @@ function useCurrentEdit({
|
|||||||
changeCurrentEditStatus
|
changeCurrentEditStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePageEdit(
|
function usePageEdit(
|
||||||
{
|
{
|
||||||
schema,
|
schema,
|
||||||
@ -431,6 +440,23 @@ function usePageEdit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useLogicEngine(schema: any) {
|
||||||
|
const logicConf = toRef(schema, 'logicConf')
|
||||||
|
const showLogicEngine = ref()
|
||||||
|
const jumpLogicEngine = ref()
|
||||||
|
function initShowLogicEngine() {
|
||||||
|
showLogicEngine.value = new RuleBuild().fromJson(logicConf.value?.showLogicConf)
|
||||||
|
}
|
||||||
|
function initJumpLogicEngine() {
|
||||||
|
jumpLogicEngine.value = new RuleBuild().fromJson(logicConf.value?.jumpLogicConf)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
showLogicEngine,
|
||||||
|
jumpLogicEngine,
|
||||||
|
initShowLogicEngine,
|
||||||
|
initJumpLogicEngine
|
||||||
|
}
|
||||||
|
}
|
||||||
type IBannerItem = {
|
type IBannerItem = {
|
||||||
name: string
|
name: string
|
||||||
key: string
|
key: string
|
||||||
@ -442,13 +468,13 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
const bannerList: Ref<IBannerList> = ref({})
|
const bannerList: Ref<IBannerList> = ref({})
|
||||||
const cooperPermissions = ref(Object.values(SurveyPermissions))
|
const cooperPermissions = ref(Object.values(SurveyPermissions))
|
||||||
const schemaUpdateTime = ref(Date.now())
|
const schemaUpdateTime = ref(Date.now())
|
||||||
const { schema, initSchema, getSchemaFromRemote } = useInitializeSchema(surveyId, () => {
|
const { schema, initSchema, getSchemaFromRemote, showLogicEngine, jumpLogicEngine } =
|
||||||
|
useInitializeSchema(surveyId, () => {
|
||||||
editGlobalBaseConf.initCounts()
|
editGlobalBaseConf.initCounts()
|
||||||
})
|
})
|
||||||
const questionDataList = toRef(schema, 'questionDataList')
|
const questionDataList = toRef(schema, 'questionDataList')
|
||||||
|
|
||||||
const editGlobalBaseConf = useEditGlobalBaseConf(questionDataList, updateTime)
|
const editGlobalBaseConf = useEditGlobalBaseConf(questionDataList, updateTime)
|
||||||
|
|
||||||
function setQuestionDataList(data: any) {
|
function setQuestionDataList(data: any) {
|
||||||
schema.questionDataList = data
|
schema.questionDataList = data
|
||||||
}
|
}
|
||||||
@ -469,6 +495,7 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
cooperPermissions.value = res.data.permissions
|
cooperPermissions.value = res.data.permissions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } = useLogicEngine(schema)
|
||||||
const {
|
const {
|
||||||
currentEditOne,
|
currentEditOne,
|
||||||
currentEditKey,
|
currentEditKey,
|
||||||
@ -483,7 +510,7 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
async function init() {
|
async function init() {
|
||||||
const { metaData } = schema
|
const { metaData } = schema
|
||||||
if (!metaData || (metaData as any)?._id !== surveyId.value) {
|
if (!metaData || (metaData as any)?._id !== surveyId.value) {
|
||||||
getSchemaFromRemote()
|
await getSchemaFromRemote()
|
||||||
}
|
}
|
||||||
currentEditOne.value = null
|
currentEditOne.value = null
|
||||||
currentEditStatus.value = 'Success'
|
currentEditStatus.value = 'Success'
|
||||||
@ -636,6 +663,8 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
createNewQuestion,
|
createNewQuestion,
|
||||||
changeSchema,
|
changeSchema,
|
||||||
changeThemePreset,
|
changeThemePreset,
|
||||||
compareQuestionSeq
|
compareQuestionSeq,
|
||||||
|
showLogicEngine,
|
||||||
|
jumpLogicEngine
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -160,3 +160,13 @@
|
|||||||
.icon-gauge:before {
|
.icon-gauge:before {
|
||||||
content: '\e6db';
|
content: '\e6db';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-suoxiao:before {
|
||||||
|
content: '\e6f4';
|
||||||
|
}
|
||||||
|
.icon-fangda:before {
|
||||||
|
content: '\e6f5';
|
||||||
|
}
|
||||||
|
.icon-shiying:before {
|
||||||
|
content: '\e6f6';
|
||||||
|
}
|
||||||
|
@ -42,11 +42,11 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
const { submitConf,isFinallyPage } = this.props
|
const { submitConf, isFinallyPage } = this.props
|
||||||
return (
|
return (
|
||||||
<div class={['submit-warp', 'preview-submit_wrapper']} onClick={this.handleClick}>
|
<div class={['submit-warp', 'preview-submit_wrapper']} onClick={this.handleClick}>
|
||||||
<button class="submit-btn" type="primary" onClick={this.submit}>
|
<button class="submit-btn" type="primary" onClick={this.submit}>
|
||||||
{isFinallyPage ? submitConf.submitTitle : '下一页'}
|
{isFinallyPage ? submitConf.submitTitle : '下一页'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,7 @@ const adapter = (() => {
|
|||||||
const list = []
|
const list = []
|
||||||
|
|
||||||
const exec = (questionData) => {
|
const exec = (questionData) => {
|
||||||
return list.reduce((pre, next) => ({ ...pre, ...next(questionData, pre) }), {})
|
return list.reduce((pre, next, index) => ({ ...pre, index, ...next(questionData, pre) }), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -7,11 +7,12 @@ import { get as _get } from 'lodash-es'
|
|||||||
export default function (questionConfig) {
|
export default function (questionConfig) {
|
||||||
let dataList = _get(questionConfig, 'dataConf.dataList')
|
let dataList = _get(questionConfig, 'dataConf.dataList')
|
||||||
// 将题目列表转成对象,并且对题目类型、题目的选项做一些字段的增加和转换
|
// 将题目列表转成对象,并且对题目类型、题目的选项做一些字段的增加和转换
|
||||||
const questionData = dataList.reduce((pre, item) => {
|
const questionData = dataList.reduce((pre, item, index) => {
|
||||||
Object.assign(pre, {
|
Object.assign(pre, {
|
||||||
[item.field]: {
|
[item.field]: {
|
||||||
indexNumber: '',
|
indexNumber: '',
|
||||||
voteTotal: 0,
|
voteTotal: 0,
|
||||||
|
index,
|
||||||
...item
|
...item
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { inject, provide, computed, onBeforeMount } from 'vue'
|
import { inject, provide, computed, onBeforeMount } from 'vue'
|
||||||
import QuestionWrapper from './QuestionWrapper.vue'
|
import QuestionWrapper from './QuestionWrapper.vue'
|
||||||
|
// import { flatten } from 'lodash-es'
|
||||||
|
|
||||||
const $bus = inject('$bus')
|
const $bus = inject('$bus')
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -68,13 +69,6 @@ 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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuestionRuleContainer
|
<QuestionRuleContainer
|
||||||
v-if="visible"
|
v-if="visibily"
|
||||||
:moduleConfig="questionConfig"
|
:moduleConfig="questionConfig"
|
||||||
:indexNumber="indexNumber"
|
:indexNumber="indexNumber"
|
||||||
:showTitle="true"
|
:showTitle="true"
|
||||||
@ -9,12 +9,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { unref, computed, watch } from 'vue'
|
import { unref, computed, watch } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import QuestionRuleContainer from '../../materials/questions/QuestionRuleContainer'
|
import QuestionRuleContainer from '../../materials/questions/QuestionRuleContainer'
|
||||||
import { useVoteMap } from '@/render/hooks/useVoteMap'
|
import { useVoteMap } from '@/render/hooks/useVoteMap'
|
||||||
import { useShowOthers } from '@/render/hooks/useShowOthers'
|
import { useShowOthers } from '@/render/hooks/useShowOthers'
|
||||||
import { useShowInput } from '@/render/hooks/useShowInput'
|
import { useShowInput } from '@/render/hooks/useShowInput'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { ruleEngine } from '@/render/hooks/useRuleEngine.js'
|
|
||||||
import { useQuestionStore } from '../stores/question'
|
import { useQuestionStore } from '../stores/question'
|
||||||
import { useSurveyStore } from '../stores/survey'
|
import { useSurveyStore } from '../stores/survey'
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ const props = defineProps({
|
|||||||
default: () => {
|
default: () => {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['change'])
|
const emit = defineEmits(['change'])
|
||||||
const questionStore = useQuestionStore()
|
const questionStore = useQuestionStore()
|
||||||
@ -39,17 +39,26 @@ const surveyStore = useSurveyStore()
|
|||||||
const formValues = computed(() => {
|
const formValues = computed(() => {
|
||||||
return surveyStore.formValues
|
return surveyStore.formValues
|
||||||
})
|
})
|
||||||
|
const { showLogicEngine } = storeToRefs(surveyStore)
|
||||||
|
const {
|
||||||
|
changeField,
|
||||||
|
changeIndex,
|
||||||
|
needHideFields,
|
||||||
|
} = storeToRefs(questionStore)
|
||||||
|
// 题型配置转换
|
||||||
const questionConfig = computed(() => {
|
const questionConfig = computed(() => {
|
||||||
let moduleConfig = props.moduleConfig
|
let moduleConfig = props.moduleConfig
|
||||||
const { type, field, options = [], ...rest } = cloneDeep(moduleConfig)
|
const { type, field, options = [], ...rest } = cloneDeep(moduleConfig)
|
||||||
// console.log(field,'这里依赖的formValue,所以change时会触发重新计算')
|
// console.log(field,'这里依赖的formValue,所以change时会触发重新计算')
|
||||||
let alloptions = options
|
let alloptions = options
|
||||||
|
|
||||||
if (type === QUESTION_TYPE.VOTE) {
|
if (type === QUESTION_TYPE.VOTE) {
|
||||||
const { options, voteTotal } = useVoteMap(field)
|
const { options, voteTotal } = useVoteMap(field)
|
||||||
const voteOptions = unref(options)
|
const voteOptions = unref(options)
|
||||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
||||||
moduleConfig.voteTotal = unref(voteTotal)
|
moduleConfig.voteTotal = unref(voteTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
NORMAL_CHOICES.includes(type) &&
|
NORMAL_CHOICES.includes(type) &&
|
||||||
options.filter((optionItem) => optionItem.others).length > 0
|
options.filter((optionItem) => optionItem.others).length > 0
|
||||||
@ -59,6 +68,7 @@ const questionConfig = computed(() => {
|
|||||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, othersOptions[index]))
|
alloptions = alloptions.map((obj, index) => Object.assign(obj, othersOptions[index]))
|
||||||
moduleConfig.othersValue = unref(othersValue)
|
moduleConfig.othersValue = unref(othersValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
RATES.includes(type) &&
|
RATES.includes(type) &&
|
||||||
rest?.rangeConfig &&
|
rest?.rangeConfig &&
|
||||||
@ -77,29 +87,41 @@ const questionConfig = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { field } = props.moduleConfig
|
const logicshow = computed(() => {
|
||||||
|
|
||||||
const visible = computed(() => {
|
|
||||||
// computed有计算缓存,当match有变化的时候触发重新计算
|
// computed有计算缓存,当match有变化的时候触发重新计算
|
||||||
return ruleEngine.match(field, 'question', formValues.value)
|
const result = showLogicEngine.value.match(props.moduleConfig.field, 'question', formValues.value)
|
||||||
|
return result === undefined ? true : result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const logicskip = computed(() => {
|
||||||
|
return needHideFields.value.includes(props.moduleConfig.field)
|
||||||
|
})
|
||||||
|
const visibily = computed(() => {
|
||||||
|
return logicshow.value && !logicskip.value
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 当题目被隐藏时,清空题目的选中项,实现a显示关联b,b显示关联c场景下,b隐藏不影响题目c的展示
|
||||||
watch(
|
watch(
|
||||||
() => visible.value,
|
() => visibily.value,
|
||||||
(newVal, oldVal) => {
|
(newVal, oldVal) => {
|
||||||
// 题目从显示到隐藏,需要清空值
|
|
||||||
const { field, type, innerType } = props.moduleConfig
|
const { field, type, innerType } = props.moduleConfig
|
||||||
if (!newVal && oldVal) {
|
if (!newVal && oldVal) {
|
||||||
let value = ''
|
// 如果被隐藏题目有选中值,则需要清空选中值
|
||||||
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
if(formValues.value[field].toString()) {
|
||||||
if (type === QUESTION_TYPE.CHECKBOX || innerType === QUESTION_TYPE.CHECKBOX) {
|
let value = ''
|
||||||
value = value ? [value] : []
|
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
||||||
|
if (type === QUESTION_TYPE.CHECKBOX || innerType === QUESTION_TYPE.CHECKBOX) {
|
||||||
|
value = value ? [value] : []
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
key: field,
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
surveyStore.changeData(data)
|
||||||
|
processJumpSkip()
|
||||||
}
|
}
|
||||||
const data = {
|
|
||||||
key: field,
|
|
||||||
value: value
|
|
||||||
}
|
|
||||||
surveyStore.changeData(data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -110,5 +132,41 @@ const handleChange = (data) => {
|
|||||||
if (props.moduleConfig.type === QUESTION_TYPE.VOTE) {
|
if (props.moduleConfig.type === QUESTION_TYPE.VOTE) {
|
||||||
questionStore.updateVoteData(data)
|
questionStore.updateVoteData(data)
|
||||||
}
|
}
|
||||||
|
processJumpSkip()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processJumpSkip = () => {
|
||||||
|
const targetResult = surveyStore.jumpLogicEngine
|
||||||
|
.getResultsByField(changeField.value, surveyStore.formValues)
|
||||||
|
.map(item => {
|
||||||
|
// 获取目标题的序号,处理跳转问卷末尾为最大题的序号
|
||||||
|
const index = item.target === 'end' ? surveyStore.dataConf.dataList.length : questionStore.getQuestionIndexByField(item.target)
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
...item
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const notMatchedFields = targetResult.filter(item => !item.result)
|
||||||
|
const matchedFields = targetResult.filter(item => item.result)
|
||||||
|
// 目标题均未匹配,需要展示出来条件题和目标题之间的题目
|
||||||
|
if (notMatchedFields.length) {
|
||||||
|
notMatchedFields.forEach(element => {
|
||||||
|
const endIndex = element.index
|
||||||
|
const fields = surveyStore.dataConf.dataList.slice(changeIndex.value + 1, endIndex).map(item => item.field)
|
||||||
|
// hideMap中remove被跳过的题
|
||||||
|
questionStore.removeNeedHideFields(fields)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedFields.length) return
|
||||||
|
// 匹配到多个目标题时,取最大序号的题目
|
||||||
|
const maxIndexQuestion =
|
||||||
|
matchedFields.filter(item => item.result).sort((a, b) => b.index - a.index)[0].index
|
||||||
|
|
||||||
|
// 条件题和目标题之间的题目隐藏
|
||||||
|
const skipKey = surveyStore.dataConf.dataList
|
||||||
|
.slice(changeIndex.value + 1, maxIndexQuestion).map(item => item.field)
|
||||||
|
questionStore.addNeedHideFields(skipKey)
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
|
|
||||||
|
|
||||||
export const ruleEngine = new RuleMatch()
|
|
||||||
export const initRuleEngine = (ruleConf) => {
|
|
||||||
ruleEngine.fromJson(ruleConf)
|
|
||||||
}
|
|
@ -10,7 +10,7 @@ import useCommandComponent from '../hooks/useCommandComponent'
|
|||||||
import { useSurveyStore } from '../stores/survey'
|
import { useSurveyStore } from '../stores/survey'
|
||||||
|
|
||||||
import AlertDialog from '../components/AlertDialog.vue'
|
import AlertDialog from '../components/AlertDialog.vue'
|
||||||
import { initRuleEngine } from '@/render/hooks/useRuleEngine.js'
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const surveyStore = useSurveyStore()
|
const surveyStore = useSurveyStore()
|
||||||
const loadData = (res: any, surveyPath: string) => {
|
const loadData = (res: any, surveyPath: string) => {
|
||||||
@ -44,7 +44,8 @@ const loadData = (res: any, surveyPath: string) => {
|
|||||||
|
|
||||||
surveyStore.setSurveyPath(surveyPath)
|
surveyStore.setSurveyPath(surveyPath)
|
||||||
surveyStore.initSurvey(questionData)
|
surveyStore.initSurvey(questionData)
|
||||||
initRuleEngine(logicConf?.showLogicConf)
|
surveyStore.initShowLogicEngine(logicConf?.showLogicConf)
|
||||||
|
surveyStore.initJumpLogicEngine(logicConf.jumpLogicConf)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.errmsg)
|
throw new Error(res.errmsg)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,11 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
const questionData = ref(null)
|
const questionData = ref(null)
|
||||||
const questionSeq = ref([]) // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
const questionSeq = ref([]) // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
||||||
const pageIndex = ref(1) // 当前分页的索引
|
const pageIndex = ref(1) // 当前分页的索引
|
||||||
|
const changeField = ref(null)
|
||||||
|
const changeIndex = computed(() => {
|
||||||
|
return questionData.value[changeField.value].index
|
||||||
|
})
|
||||||
|
const needHideFields = ref([])
|
||||||
|
|
||||||
// 题目列表
|
// 题目列表
|
||||||
const questionList = computed(() => {
|
const questionList = computed(() => {
|
||||||
@ -177,6 +182,22 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setChangeField = (field) => {
|
||||||
|
changeField.value = field
|
||||||
|
}
|
||||||
|
const getQuestionIndexByField = (field) => {
|
||||||
|
return questionData.value[field].index
|
||||||
|
}
|
||||||
|
const addNeedHideFields = (fields) => {
|
||||||
|
fields.forEach(field => {
|
||||||
|
if(!needHideFields.value.includes(field)) {
|
||||||
|
needHideFields.value.push(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const removeNeedHideFields = (fields) => {
|
||||||
|
needHideFields.value = needHideFields.value.filter(field => !fields.includes(field))
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
voteMap,
|
voteMap,
|
||||||
questionData,
|
questionData,
|
||||||
@ -191,6 +212,13 @@ export const useQuestionStore = defineStore('question', () => {
|
|||||||
setVoteMap,
|
setVoteMap,
|
||||||
updateVoteMapByKey,
|
updateVoteMapByKey,
|
||||||
initVoteData,
|
initVoteData,
|
||||||
updateVoteData
|
updateVoteData,
|
||||||
|
changeField,
|
||||||
|
changeIndex,
|
||||||
|
setChangeField,
|
||||||
|
needHideFields,
|
||||||
|
addNeedHideFields,
|
||||||
|
removeNeedHideFields,
|
||||||
|
getQuestionIndexByField
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -15,6 +15,8 @@ import 'moment/locale/zh-cn'
|
|||||||
moment.locale('zh-cn')
|
moment.locale('zh-cn')
|
||||||
|
|
||||||
import adapter from '../adapter'
|
import adapter from '../adapter'
|
||||||
|
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
|
||||||
|
// import { jumpLogicRule } from '@/common/logicEngine/jumpLogicRule'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CODE_MAP不从management引入,在dev阶段,会导致B端 router被加载,进而导致C端路由被添加 baseUrl: /management
|
* CODE_MAP不从management引入,在dev阶段,会导致B端 router被加载,进而导致C端路由被添加 baseUrl: /management
|
||||||
@ -40,6 +42,7 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
const whiteData = ref({})
|
const whiteData = ref({})
|
||||||
const pageConf = ref([])
|
const pageConf = ref([])
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const questionStore = useQuestionStore()
|
const questionStore = useQuestionStore()
|
||||||
const { setErrorInfo } = useErrorInfo()
|
const { setErrorInfo } = useErrorInfo()
|
||||||
@ -156,6 +159,16 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
if (key in formValues.value) {
|
if (key in formValues.value) {
|
||||||
formValues.value[key] = value
|
formValues.value[key] = value
|
||||||
}
|
}
|
||||||
|
questionStore.setChangeField(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showLogicEngine = ref()
|
||||||
|
const initShowLogicEngine = (showLogicConf) => {
|
||||||
|
showLogicEngine.value = new RuleMatch().fromJson(showLogicConf)
|
||||||
|
}
|
||||||
|
const jumpLogicEngine = ref()
|
||||||
|
const initJumpLogicEngine = (jumpLogicConf) => {
|
||||||
|
jumpLogicEngine.value = new RuleMatch().fromJson(jumpLogicConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -173,12 +186,15 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
formValues,
|
formValues,
|
||||||
whiteData,
|
whiteData,
|
||||||
pageConf,
|
pageConf,
|
||||||
|
|
||||||
initSurvey,
|
initSurvey,
|
||||||
changeData,
|
changeData,
|
||||||
setWhiteData,
|
setWhiteData,
|
||||||
setSurveyPath,
|
setSurveyPath,
|
||||||
setEnterTime,
|
setEnterTime,
|
||||||
getEncryptInfo
|
getEncryptInfo,
|
||||||
|
showLogicEngine,
|
||||||
|
initShowLogicEngine,
|
||||||
|
jumpLogicEngine,
|
||||||
|
initJumpLogicEngine
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user