feat: 跳转逻辑 (#388)
This commit is contained in:
parent
b5c7ec3008
commit
47ae3ec095
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) {
|
||||||
|
return this.rules.filter((rule) => {
|
||||||
|
return rule.conditions.filter((condition) => condition.field === field).length
|
||||||
|
})
|
||||||
|
}
|
||||||
// 实现前置题删除校验
|
// 实现前置题删除校验
|
||||||
findTargetsByFields(field: string) {
|
findTargetsByFields(field: string) {
|
||||||
const nodes = this.rules.filter((rule: RuleNode) => {
|
const nodes = this.findRulesByField(field)
|
||||||
const conditions = rule.conditions.filter((item: any) => {
|
|
||||||
return item.field === field
|
|
||||||
})
|
|
||||||
return conditions.length > 0
|
|
||||||
})
|
|
||||||
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,8 +83,10 @@ 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
|
||||||
|
if (comparor === 'or') {
|
||||||
|
res = Array.from(this.conditions.entries()).some(([, value]) => {
|
||||||
const res = value.match(fact)
|
const res = value.match(fact)
|
||||||
if (res) {
|
if (res) {
|
||||||
return true
|
return true
|
||||||
@ -92,6 +94,17 @@ export class RuleNode {
|
|||||||
return false
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加条件规则到规则引擎中
|
// 添加条件规则到规则引擎中
|
||||||
@ -161,12 +175,12 @@ export class RuleMatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 匹配条件规则
|
// 匹配条件规则
|
||||||
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);
|
// this.matchCache.set(hash, result);
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
@ -196,11 +210,34 @@ export class RuleMatch {
|
|||||||
[...this.rules.entries()].filter(([, value]) => {
|
[...this.rules.entries()].filter(([, value]) => {
|
||||||
return [...value.conditions.entries()].filter(([, value]) => {
|
return [...value.conditions.entries()].filter(([, value]) => {
|
||||||
return value.field === field
|
return value.field === field
|
||||||
})
|
}).length
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return [...rules.values()].map((obj) => obj.target)
|
return [...rules.values()].map((obj) => obj.target)
|
||||||
}
|
}
|
||||||
|
findRulesByField(field: string) {
|
||||||
|
const list = [...this.rules.entries()]
|
||||||
|
const match = list.filter(([, ruleValue]) => {
|
||||||
|
const list = [...ruleValue.conditions.entries()]
|
||||||
|
const res = list.filter(([, conditionValue]) => {
|
||||||
|
const hit = conditionValue.field === field
|
||||||
|
return hit
|
||||||
|
})
|
||||||
|
return res.length
|
||||||
|
})
|
||||||
|
console.log({ match })
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
findFieldsByTarget(target: string) {
|
||||||
|
const rules = new Map(
|
||||||
|
[...this.rules.entries()].filter(([, value]) => {
|
||||||
|
return value.target === target
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return [...rules.values()].map((obj) =>
|
||||||
|
[...obj.conditions.entries()].map(([, value]) => value.field)
|
||||||
|
)
|
||||||
|
}
|
||||||
toJson() {
|
toJson() {
|
||||||
return Array.from(this.rules.entries()).map(([, value]) => {
|
return Array.from(this.rules.entries()).map(([, value]) => {
|
||||||
return value.toJson()
|
return value.toJson()
|
||||||
|
@ -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?.findTargetsByFields(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,7 +2,10 @@ 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) => {
|
||||||
@ -11,7 +14,7 @@ export const useShowLogicInfo = (field) => {
|
|||||||
// 判断该题是否作为了显示逻辑前置题
|
// 判断该题是否作为了显示逻辑前置题
|
||||||
const isField = logicEngine?.findTargetsByFields(field)?.length > 0
|
const isField = logicEngine?.findTargetsByFields(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(() => {
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
:ref="`questionWrapper-${element.field}`"
|
:ref="`questionWrapper-${element.field}`"
|
||||||
:moduleConfig="element"
|
:moduleConfig="element"
|
||||||
:qIndex="element.qIndex"
|
:qIndex="element.qIndex"
|
||||||
:isFirst="index==0"
|
:isFirst="index == 0"
|
||||||
:indexNumber="element.indexNumber"
|
:indexNumber="element.indexNumber"
|
||||||
:isSelected="currentEditOne === element.qIndex"
|
:isSelected="currentEditOne === element.qIndex"
|
||||||
:isLast="index + 1 === questionDataList.length"
|
:isLast="index + 1 === questionDataList.length"
|
||||||
@ -60,12 +60,12 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['change', 'select', 'changeSeq','change'],
|
emits: ['change', 'select', 'changeSeq', 'change'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const editStore = useEditStore()
|
const editStore = useEditStore()
|
||||||
const renderData = computed({
|
const renderData = computed({
|
||||||
get() {
|
get() {
|
||||||
return props.questionDataList; //filterQuestionPreviewData(props.questionDataList)
|
return props.questionDataList //filterQuestionPreviewData(props.questionDataList)
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
editStore.moveQuestionDataList(value)
|
editStore.moveQuestionDataList(value)
|
||||||
|
@ -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)) {
|
if (unref(hasShowLogic)) {
|
||||||
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
|
||||||
|
}
|
@ -50,7 +50,7 @@ import { ref } from 'vue'
|
|||||||
|
|
||||||
const editStore = useEditStore()
|
const editStore = useEditStore()
|
||||||
const { newQuestionIndex } = storeToRefs(editStore)
|
const { newQuestionIndex } = storeToRefs(editStore)
|
||||||
const { addQuestion, setCurrentEditOne,createNewQuestion } = editStore
|
const { addQuestion, setCurrentEditOne, createNewQuestion } = editStore
|
||||||
|
|
||||||
const activeNames = ref([0, 1])
|
const activeNames = ref([0, 1])
|
||||||
const previewImg = ref('')
|
const previewImg = ref('')
|
||||||
@ -61,7 +61,6 @@ questionLoader.init({
|
|||||||
typeList: questionTypeList.map((item) => item.type)
|
typeList: questionTypeList.map((item) => item.type)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const onQuestionType = ({ type }) => {
|
const onQuestionType = ({ type }) => {
|
||||||
const newQuestion = createNewQuestion({ type })
|
const newQuestion = createNewQuestion({ type })
|
||||||
addQuestion({ question: newQuestion, index: newQuestionIndex.value })
|
addQuestion({ question: newQuestion, index: newQuestionIndex.value })
|
||||||
|
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 })
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,7 @@ import { SurveyPermissions } from '@/management/utils/types/workSpace'
|
|||||||
import { getBannerData } from '@/management/api/skin.js'
|
import { getBannerData } from '@/management/api/skin.js'
|
||||||
import { getCollaboratorPermissions } from '@/management/api/space'
|
import { getCollaboratorPermissions } from '@/management/api/space'
|
||||||
import { CODE_MAP } from '../api/base'
|
import { CODE_MAP } from '../api/base'
|
||||||
|
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
|
||||||
|
|
||||||
const innerMetaConfig = {
|
const innerMetaConfig = {
|
||||||
submit: {
|
submit: {
|
||||||
@ -82,10 +83,12 @@ function useInitializeSchema(surveyId: Ref<string>) {
|
|||||||
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)
|
||||||
@ -130,6 +133,9 @@ function useInitializeSchema(surveyId: Ref<string>) {
|
|||||||
logicConf
|
logicConf
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
initShowLogicEngine()
|
||||||
|
initJumpLogicEngine()
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.errmsg || '问卷不存在')
|
throw new Error(res.errmsg || '问卷不存在')
|
||||||
}
|
}
|
||||||
@ -138,7 +144,9 @@ function useInitializeSchema(surveyId: Ref<string>) {
|
|||||||
return {
|
return {
|
||||||
schema,
|
schema,
|
||||||
initSchema,
|
initSchema,
|
||||||
getSchemaFromRemote
|
getSchemaFromRemote,
|
||||||
|
showLogicEngine,
|
||||||
|
jumpLogicEngine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,6 +297,7 @@ function useCurrentEdit({
|
|||||||
changeCurrentEditStatus
|
changeCurrentEditStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePageEdit(
|
function usePageEdit(
|
||||||
{
|
{
|
||||||
schema,
|
schema,
|
||||||
@ -421,6 +430,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
|
||||||
@ -432,9 +458,9 @@ 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)
|
||||||
const questionDataList = toRef(schema, 'questionDataList')
|
const questionDataList = toRef(schema, 'questionDataList')
|
||||||
|
|
||||||
function setQuestionDataList(data: any) {
|
function setQuestionDataList(data: any) {
|
||||||
schema.questionDataList = data
|
schema.questionDataList = data
|
||||||
}
|
}
|
||||||
@ -455,6 +481,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,
|
||||||
@ -469,7 +496,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'
|
||||||
@ -618,6 +645,8 @@ export const useEditStore = defineStore('edit', () => {
|
|||||||
createNewQuestion,
|
createNewQuestion,
|
||||||
changeSchema,
|
changeSchema,
|
||||||
changeThemePreset,
|
changeThemePreset,
|
||||||
compareQuestionSeq
|
compareQuestionSeq,
|
||||||
|
showLogicEngine,
|
||||||
|
jumpLogicEngine
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'iconfont';
|
font-family: 'iconfont'; /* Project id 4263849 */
|
||||||
/* Project id 4263849 */
|
|
||||||
src:
|
src:
|
||||||
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff2?t=1717580126029') format('woff2'),
|
url('//at.alicdn.com/t/c/font_4263849_wh46krnnsv.woff2?t=1722515679936') format('woff2'),
|
||||||
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff?t=1717580126029') format('woff'),
|
url('//at.alicdn.com/t/c/font_4263849_wh46krnnsv.woff?t=1722515679936') format('woff'),
|
||||||
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.ttf?t=1717580126029') format('truetype');
|
url('//at.alicdn.com/t/c/font_4263849_wh46krnnsv.ttf?t=1722515679936') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@ -162,3 +161,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,7 +42,7 @@ 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}>
|
||||||
|
@ -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({
|
||||||
@ -69,10 +70,33 @@ onBeforeMount(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
// const visible = computed(() => {
|
// const visible = computed(() => {
|
||||||
|
// return (index) => {
|
||||||
|
// const field = props.renderData[index].field
|
||||||
|
// const jumpRely = flatten(ruleEngine.findFieldsByTarget(field))
|
||||||
|
// const jumpRelyIndexs = jumpRely.map(item => props.renderData.map(i => i.field).findIndex(i=> i === item))
|
||||||
|
// const jumpRelyIndex = Math.max(...jumpRelyIndexs)
|
||||||
|
// const jumpTargetIndex = 5
|
||||||
|
// // props.renderData.map(i => i.field).findIndex(i=> i === item)
|
||||||
|
// const jumpTargetMatch = ruleEngine.getResult(field, 'question')
|
||||||
|
// console.log({index, field, jumpRely, jumpRelyIndexs, jumpRelyIndex, jumpTargetMatch})
|
||||||
|
// if(jumpTargetMatch) {
|
||||||
|
// return !(jumpRelyIndex < index && index < jumpTargetIndex)
|
||||||
|
// } else {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const jumpMatch = computed(() => {
|
||||||
// return (field) => {
|
// return (field) => {
|
||||||
// console.log(field + '重新计算visible:'+store.state.ruleEngine.getResult(field, 'question'))
|
// return ruleEngine.match(field, 'question', formValues.value)
|
||||||
// // 显示逻辑-处理视图
|
// }
|
||||||
// return store.state.ruleEngine.getResult(field, 'question')
|
// })
|
||||||
|
|
||||||
|
// watch(()=> jumpMatch,
|
||||||
|
// (newVal, oldVal) => {
|
||||||
|
// if(newVal) {
|
||||||
|
// // 去改renderData,split中间的题目
|
||||||
|
// handleJump(field, formValues.value)
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
@ -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"
|
||||||
@ -8,17 +8,18 @@
|
|||||||
></QuestionRuleContainer>
|
></QuestionRuleContainer>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { unref, computed, watch } from 'vue'
|
import { unref, ref, 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'
|
||||||
|
|
||||||
import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
|
import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
|
||||||
|
import { getQuestionIndexByField, findMinKeyInMap } from '@/render/utils/index.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
indexNumber: {
|
indexNumber: {
|
||||||
@ -39,10 +40,10 @@ const surveyStore = useSurveyStore()
|
|||||||
const formValues = computed(() => {
|
const formValues = computed(() => {
|
||||||
return surveyStore.formValues
|
return surveyStore.formValues
|
||||||
})
|
})
|
||||||
|
const { dataConf, changeField, showLogicEngine, jumpLogicEngine } = storeToRefs(surveyStore)
|
||||||
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时会触发重新计算')
|
|
||||||
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)
|
||||||
@ -77,15 +78,14 @@ const questionConfig = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { field } = props.moduleConfig
|
const showMatch = 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
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => visible.value,
|
() => showMatch.value,
|
||||||
(newVal, oldVal) => {
|
(newVal, oldVal) => {
|
||||||
// 题目从显示到隐藏,需要清空值
|
// 题目从显示到隐藏,需要清空值
|
||||||
const { field, type, innerType } = props.moduleConfig
|
const { field, type, innerType } = props.moduleConfig
|
||||||
@ -104,6 +104,80 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const jumpSkip = ref(false)
|
||||||
|
const visibily = computed(() => {
|
||||||
|
return showMatch.value && !jumpSkip.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听formValues变化,判断当前题目是否需要跳过
|
||||||
|
watch(
|
||||||
|
() => formValues,
|
||||||
|
(newVal) => {
|
||||||
|
const currentIndex = getQuestionIndexByField(dataConf.value.dataList, questionConfig.value.field)
|
||||||
|
const changeIndex = getQuestionIndexByField(dataConf.value.dataList, changeField.value)
|
||||||
|
// 前面的题目不受跳题影响
|
||||||
|
if (currentIndex < changeIndex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 找到当前题关联的目标题规则
|
||||||
|
const rules = jumpLogicEngine.value.findRulesByField(changeField.value)
|
||||||
|
// change没有跳题关联的题直接返回
|
||||||
|
if (!rules.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 计算目标题的命中情况
|
||||||
|
const targetsResult = new Map()
|
||||||
|
// 处理只有一条规则的情况
|
||||||
|
if (rules.length === 1) {
|
||||||
|
rules.forEach(([, rule]) => {
|
||||||
|
const index = getQuestionIndexByField(dataConf.value.dataList, rule.target)
|
||||||
|
targetsResult.set(
|
||||||
|
index,
|
||||||
|
jumpLogicEngine.value.match(rule.target, 'question', newVal.value, 'or')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果存在多条规则,能命中选项跳转则精确命中选项跳转,否则命中答题跳转
|
||||||
|
const optionJump = rules.filter(([, rule]) => {
|
||||||
|
// 过滤掉答题跳转,剩下的就是选项跳转
|
||||||
|
const conditionhash = `${changeField.value}neq`
|
||||||
|
return !rule.conditions.get(conditionhash)
|
||||||
|
})
|
||||||
|
if (optionJump.length) {
|
||||||
|
optionJump.forEach(([, rule]) => {
|
||||||
|
const index = getQuestionIndexByField(dataConf.value.dataList, rule.target)
|
||||||
|
targetsResult.set(
|
||||||
|
index,
|
||||||
|
jumpLogicEngine.value.match(rule.target, 'question', newVal.value, 'or')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const answerJump = rules.find(([, rule]) => {
|
||||||
|
const conditionhash = `${changeField.value}neq`
|
||||||
|
return rule.conditions.get(conditionhash)
|
||||||
|
})
|
||||||
|
const index = getQuestionIndexByField(dataConf.value.dataList, answerJump[1].target)
|
||||||
|
targetsResult.set(
|
||||||
|
index,
|
||||||
|
jumpLogicEngine.value.match(answerJump[1].target, 'question', newVal.value, 'or')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jumpFitMinIndex = findMinKeyInMap(targetsResult, true)
|
||||||
|
|
||||||
|
const jumpQuestion = currentIndex < jumpFitMinIndex
|
||||||
|
const jumpEnd = jumpFitMinIndex === -1 && rules.map(([, rule]) => rule.target).includes('end')
|
||||||
|
|
||||||
|
if (changeIndex < currentIndex && (jumpQuestion || jumpEnd)) {
|
||||||
|
jumpSkip.value = true
|
||||||
|
} else {
|
||||||
|
jumpSkip.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const handleChange = (data) => {
|
const handleChange = (data) => {
|
||||||
emit('change', data)
|
emit('change', data)
|
||||||
// 处理投票题
|
// 处理投票题
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -39,6 +41,7 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
const formValues = ref({})
|
const formValues = ref({})
|
||||||
const whiteData = ref({})
|
const whiteData = ref({})
|
||||||
const pageConf = ref([])
|
const pageConf = ref([])
|
||||||
|
const changeField = ref(null)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const questionStore = useQuestionStore()
|
const questionStore = useQuestionStore()
|
||||||
@ -153,11 +156,21 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
// 用户输入或者选择后,更新表单数据
|
// 用户输入或者选择后,更新表单数据
|
||||||
const changeData = (data) => {
|
const changeData = (data) => {
|
||||||
let { key, value } = data
|
let { key, value } = data
|
||||||
|
changeField.value = key
|
||||||
if (key in formValues.value) {
|
if (key in formValues.value) {
|
||||||
formValues.value[key] = value
|
formValues.value[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
surveyPath,
|
surveyPath,
|
||||||
isMobile,
|
isMobile,
|
||||||
@ -173,12 +186,16 @@ export const useSurveyStore = defineStore('survey', () => {
|
|||||||
formValues,
|
formValues,
|
||||||
whiteData,
|
whiteData,
|
||||||
pageConf,
|
pageConf,
|
||||||
|
changeField,
|
||||||
initSurvey,
|
initSurvey,
|
||||||
changeData,
|
changeData,
|
||||||
setWhiteData,
|
setWhiteData,
|
||||||
setSurveyPath,
|
setSurveyPath,
|
||||||
setEnterTime,
|
setEnterTime,
|
||||||
getEncryptInfo
|
getEncryptInfo,
|
||||||
|
showLogicEngine,
|
||||||
|
initShowLogicEngine,
|
||||||
|
jumpLogicEngine,
|
||||||
|
initJumpLogicEngine
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -30,3 +30,23 @@ export const formatLink = (url) => {
|
|||||||
}
|
}
|
||||||
return `http://${url}`
|
return `http://${url}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getQuestionIndexByField = (questionList, field) => {
|
||||||
|
const arr = questionList.map((item) => item.field)
|
||||||
|
const index = arr.findIndex((item) => item === field)
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMinKeyInMap(map, hit) {
|
||||||
|
let minKey = null
|
||||||
|
|
||||||
|
for (const [key, value] of map) {
|
||||||
|
if (value === hit) {
|
||||||
|
if (minKey === null || key < minKey) {
|
||||||
|
minKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minKey
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user