feat: 跳转逻辑稳定版 (#399)
* feat: 跳转逻辑 (#388) * fix: 跳转逻辑优化 (#397) * fix: 跳转逻辑优化 * fix: processJumpSkip逻辑放在题目组件中
This commit is contained in:
parent
3e7f0cac90
commit
bc3ce31c9e
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ pnpm-debug.log*
|
||||
*.sw?
|
||||
|
||||
.history
|
||||
components.d.ts
|
||||
|
||||
# 默认的上传文件夹
|
||||
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/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logicflow/core": "2.0.0",
|
||||
"@logicflow/extension": "2.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"async-validator": "^4.2.5",
|
||||
|
@ -33,8 +33,8 @@ export class RuleNode {
|
||||
conditions: ConditionNode[] = []
|
||||
scope: string = Scope.Question
|
||||
target: string = ''
|
||||
constructor(scope: string = Scope.Question, target: string = '') {
|
||||
this.id = generateID(PrefixID.Rule)
|
||||
constructor(target: string = '', scope: string = Scope.Question, id?: string) {
|
||||
this.id = id || generateID(PrefixID.Rule)
|
||||
this.scope = scope
|
||||
this.target = target
|
||||
}
|
||||
@ -54,14 +54,8 @@ export class RuleNode {
|
||||
|
||||
export class RuleBuild {
|
||||
rules: RuleNode[] = []
|
||||
static instance: RuleBuild
|
||||
constructor() {
|
||||
this.rules = []
|
||||
if (!RuleBuild.instance) {
|
||||
RuleBuild.instance = this
|
||||
}
|
||||
|
||||
return RuleBuild.instance
|
||||
}
|
||||
|
||||
// 添加条件规则到规则引擎中
|
||||
@ -71,6 +65,9 @@ export class RuleBuild {
|
||||
removeRule(ruleId: string) {
|
||||
this.rules = this.rules.filter((rule) => rule.id !== ruleId)
|
||||
}
|
||||
clear() {
|
||||
this.rules = []
|
||||
}
|
||||
findRule(ruleId: string) {
|
||||
return this.rules.find((rule) => rule.id === ruleId)
|
||||
}
|
||||
@ -94,7 +91,7 @@ export class RuleBuild {
|
||||
if (ruleConf instanceof Array) {
|
||||
ruleConf.forEach((rule: any) => {
|
||||
const { scope, target } = rule
|
||||
const ruleNode = new RuleNode(scope, target)
|
||||
const ruleNode = new RuleNode(target, scope)
|
||||
rule.conditions.forEach((condition: any) => {
|
||||
const { field, operator, value } = condition
|
||||
const conditionNode = new ConditionNode(field, operator, value)
|
||||
@ -112,19 +109,19 @@ export class RuleBuild {
|
||||
findTargetsByScope(scope: string) {
|
||||
return this.rules.filter((rule) => rule.scope === scope).map((rule) => rule.target)
|
||||
}
|
||||
// 实现前置题删除校验
|
||||
findTargetsByFields(field: string) {
|
||||
const nodes = this.rules.filter((rule: RuleNode) => {
|
||||
const conditions = rule.conditions.filter((item: any) => {
|
||||
return item.field === field
|
||||
})
|
||||
return conditions.length > 0
|
||||
findRulesByField(field: string) {
|
||||
return this.rules.filter((rule) => {
|
||||
return rule.conditions.filter((condition) => condition.field === field).length
|
||||
})
|
||||
}
|
||||
// 实现前置题删除校验
|
||||
findTargetsByField(field: string) {
|
||||
const nodes = this.findRulesByField(field)
|
||||
return nodes.map((item: any) => {
|
||||
return item.target
|
||||
})
|
||||
}
|
||||
// 根据目标题获取显示逻辑
|
||||
// 根据目标题获取关联的逻辑条件
|
||||
findConditionByTarget(target: string) {
|
||||
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> {
|
||||
// 默认显示
|
||||
public result: boolean = false
|
||||
public result: boolean | undefined = undefined
|
||||
constructor(
|
||||
public field: F,
|
||||
public operator: O,
|
||||
@ -16,7 +16,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
||||
return this.field + this.operator + this.value
|
||||
}
|
||||
|
||||
match(facts: Fact): boolean {
|
||||
match(facts: Fact): boolean | undefined {
|
||||
// console.log(this.calculateHash())
|
||||
// 如果该特征在事实对象中不存在,则直接返回false
|
||||
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))
|
||||
return this.result
|
||||
} else {
|
||||
this.result = facts[this.field].includes(this.value)
|
||||
this.result = !facts[this.field].includes(this.value)
|
||||
return this.result
|
||||
}
|
||||
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))
|
||||
return this.result
|
||||
} else {
|
||||
this.result = facts[this.field].includes(this.value)
|
||||
this.result = facts[this.field].toString() !== this.value
|
||||
return this.result
|
||||
}
|
||||
// 其他比较操作符的判断逻辑
|
||||
@ -69,7 +69,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
||||
|
||||
export class RuleNode {
|
||||
conditions: Map<string, ConditionNode<string, Operator>> // 使用哈希表存储条件规则对象
|
||||
public result: boolean = false
|
||||
public result: boolean | undefined
|
||||
constructor(
|
||||
public target: string,
|
||||
public scope: string
|
||||
@ -83,15 +83,28 @@ export class RuleNode {
|
||||
}
|
||||
|
||||
// 匹配条件规则
|
||||
match(fact: Fact) {
|
||||
const res = Array.from(this.conditions.entries()).every(([, value]) => {
|
||||
const res = value.match(fact)
|
||||
if (res) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
match(fact: Fact, comparor?: any) {
|
||||
let res: boolean | undefined = undefined
|
||||
if (comparor === 'or') {
|
||||
res = Array.from(this.conditions.entries()).some(([, value]) => {
|
||||
const res = value.match(fact)
|
||||
if (res) {
|
||||
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
|
||||
return res
|
||||
}
|
||||
@ -121,14 +134,14 @@ export class RuleNode {
|
||||
|
||||
export class RuleMatch {
|
||||
rules: Map<string, RuleNode>
|
||||
static instance: any
|
||||
// static instance: any
|
||||
constructor() {
|
||||
this.rules = new Map()
|
||||
if (!RuleMatch.instance) {
|
||||
RuleMatch.instance = this
|
||||
}
|
||||
// if (!RuleMatch.instance) {
|
||||
// RuleMatch.instance = this
|
||||
// }
|
||||
|
||||
return RuleMatch.instance
|
||||
// return RuleMatch.instance
|
||||
}
|
||||
fromJson(ruleConf: any) {
|
||||
if (ruleConf instanceof Array) {
|
||||
@ -145,6 +158,7 @@ export class RuleMatch {
|
||||
this.addRule(ruleNode)
|
||||
})
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// 添加条件规则到规则引擎中
|
||||
@ -160,22 +174,31 @@ export class RuleMatch {
|
||||
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 rule = this.rules.get(hash)
|
||||
if (rule) {
|
||||
const result = rule.match(fact)
|
||||
// this.matchCache.set(hash, result);
|
||||
const result = rule.match(fact, comparor)
|
||||
return result
|
||||
} else {
|
||||
// 默认显示
|
||||
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 rule = this.rules.get(hash)
|
||||
if (rule) {
|
||||
@ -191,15 +214,18 @@ export class RuleMatch {
|
||||
// 假设哈希值计算方法为简单的字符串拼接或其他哈希算法
|
||||
return target + scope
|
||||
}
|
||||
findTargetsByField(field: string) {
|
||||
const rules = new Map(
|
||||
[...this.rules.entries()].filter(([, value]) => {
|
||||
return [...value.conditions.entries()].filter(([, value]) => {
|
||||
return value.field === field
|
||||
})
|
||||
// 查找条件题的规则
|
||||
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 [...rules.values()].map((obj) => obj.target)
|
||||
return res.length
|
||||
})
|
||||
return match
|
||||
}
|
||||
toJson() {
|
||||
return Array.from(this.rules.entries()).map(([, value]) => {
|
||||
|
@ -53,7 +53,7 @@ export const defaultQuestionConfig = {
|
||||
star: 5,
|
||||
optionOrigin: '',
|
||||
originType: 'selected',
|
||||
innerType:'',
|
||||
innerType: '',
|
||||
matrixOptionsRely: '',
|
||||
numberRange: {
|
||||
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(() => {
|
||||
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(() => {
|
||||
|
@ -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 { flatten } from 'lodash-es'
|
||||
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) => {
|
||||
const hasShowLogic = computed(() => {
|
||||
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
|
||||
})
|
||||
const getShowLogicText = computed(() => {
|
||||
|
@ -29,8 +29,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
|
||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import BackPanel from '../modules/generalModule/BackPanel.vue'
|
||||
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
|
||||
@ -44,6 +43,8 @@ import CooperationPanel from '../modules/contentModule/CooperationPanel.vue'
|
||||
const editStore = useEditStore()
|
||||
const { schema, changeSchema } = editStore
|
||||
const title = computed(() => (editStore.schema?.metaData as any)?.title || '')
|
||||
|
||||
const { showLogicEngine, jumpLogicEngine } = storeToRefs(editStore)
|
||||
// 校验 - 逻辑
|
||||
const updateLogicConf = () => {
|
||||
let res = {
|
||||
@ -67,12 +68,16 @@ const updateLogicConf = () => {
|
||||
}
|
||||
|
||||
const showLogicConf = showLogicEngine.value.toJson()
|
||||
|
||||
// 更新逻辑配置
|
||||
changeSchema({ key: 'logicConf', value: { showLogicConf } })
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const jumpLogicConf = jumpLogicEngine.value.toJson()
|
||||
changeSchema({ key: 'logicConf', value: { jumpLogicConf } })
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,8 @@
|
||||
<i-ep-close />
|
||||
</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>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@ -37,6 +38,7 @@ import { ref, computed, unref } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||
import { useShowLogicInfo } from '@/management/hooks/useShowLogicInfo'
|
||||
import { useJumpLogicInfo } from '@/management/hooks/useJumpLogicInfo'
|
||||
|
||||
const props = defineProps({
|
||||
qIndex: {
|
||||
@ -69,6 +71,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['changeSeq', 'select'])
|
||||
|
||||
const { getShowLogicText, hasShowLogic } = useShowLogicInfo(props.moduleConfig.field)
|
||||
const { getJumpLogicText, hasJumpLogic } = useJumpLogicInfo(props.moduleConfig.field)
|
||||
|
||||
const isHover = ref(false)
|
||||
const isMove = ref(false)
|
||||
@ -139,7 +142,14 @@ const onMoveDown = () => {
|
||||
}
|
||||
const onDelete = async () => {
|
||||
if (unref(hasShowLogic) || getShowLogicText.value) {
|
||||
ElMessageBox.alert('该问题被逻辑依赖,请先删除逻辑依赖', '提示', {
|
||||
ElMessageBox.alert('该题目被显示逻辑关联,请先清除逻辑依赖', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (unref(hasJumpLogic)) {
|
||||
ElMessageBox.alert('该题目被跳转逻辑关联,请先清除逻辑依赖', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'warning'
|
||||
})
|
||||
|
@ -24,10 +24,8 @@ import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||
import CommonTemplate from './components/CommonTemplate.vue'
|
||||
import Navbar from './components/ModuleNavbar.vue'
|
||||
|
||||
import { initShowLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
|
||||
const editStore = useEditStore()
|
||||
const { schema, init, setSurveyId } = editStore
|
||||
const { init, setSurveyId } = editStore
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@ -36,7 +34,6 @@ onMounted(async () => {
|
||||
|
||||
try {
|
||||
await init()
|
||||
await initShowLogicEngine(schema.logicConf.showLogicConf || {})
|
||||
} catch (err: any) {
|
||||
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 { cloneDeep } from 'lodash-es'
|
||||
|
||||
import RulePanel from '../../modules/logicModule/RulePanel.vue'
|
||||
import RulePanel from './components/RulePanel.vue'
|
||||
import { filterQuestionPreviewData } from '@/management/utils/index'
|
||||
|
||||
const editStore = useEditStore()
|
||||
@ -23,11 +23,11 @@ provide('renderData', renderData)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.logic-wrapper {
|
||||
height: calc(100% - 120px);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 12px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
// position: fixed;
|
||||
}
|
||||
</style>
|
@ -52,7 +52,10 @@ import { ElMessageBox } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message-box.scss'
|
||||
import { RuleNode } from '@/common/logicEngine/RuleBuild'
|
||||
import { cleanRichText } from '@/common/xss'
|
||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const editStore = useEditStore()
|
||||
const { showLogicEngine } = storeToRefs(editStore)
|
||||
import ConditionView from './ConditionView.vue'
|
||||
|
||||
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
|
||||
|
@ -21,8 +21,12 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, computed } from 'vue'
|
||||
import { RuleNode, ConditionNode } from '@/common/logicEngine/RuleBuild'
|
||||
import { showLogicEngine } from '@/management/hooks/useShowLogicEngine'
|
||||
import RuleNodeView from './components/RuleNodeView.vue'
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const editStore = useEditStore()
|
||||
const { showLogicEngine } = storeToRefs(editStore)
|
||||
|
||||
import RuleNodeView from './RuleNodeView.vue'
|
||||
|
||||
const list = computed(() => {
|
||||
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(
|
||||
activeRouter,
|
||||
(val: any) => {
|
||||
router.push({ name: val })
|
||||
// 避免编辑页刷新丢失query
|
||||
const query = route.query
|
||||
router.push({ name: val, query })
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
|
@ -57,7 +57,8 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
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 useEditGlobalBaseConf, { type TypeMethod } from './composables/useEditGlobalBaseConf'
|
||||
import { CODE_MAP } from '../api/base'
|
||||
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
|
||||
|
||||
const innerMetaConfig = {
|
||||
submit: {
|
||||
@ -83,10 +84,12 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
||||
pageEditOne: 1,
|
||||
pageConf: [], // 分页逻辑
|
||||
logicConf: {
|
||||
showLogicConf: []
|
||||
showLogicConf: [],
|
||||
jumpLogicConf: []
|
||||
}
|
||||
})
|
||||
|
||||
const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } =
|
||||
useLogicEngine(schema)
|
||||
function initSchema({ metaData, codeData }: { metaData: any; codeData: any }) {
|
||||
schema.metaData = metaData
|
||||
schema.bannerConf = _merge({}, schema.bannerConf, codeData.bannerConf)
|
||||
@ -132,6 +135,9 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
||||
}
|
||||
})
|
||||
initializeSchemaCallBack()
|
||||
|
||||
initShowLogicEngine()
|
||||
initJumpLogicEngine()
|
||||
} else {
|
||||
throw new Error(res.errmsg || '问卷不存在')
|
||||
}
|
||||
@ -140,7 +146,9 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
|
||||
return {
|
||||
schema,
|
||||
initSchema,
|
||||
getSchemaFromRemote
|
||||
getSchemaFromRemote,
|
||||
showLogicEngine,
|
||||
jumpLogicEngine
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,6 +307,7 @@ function useCurrentEdit({
|
||||
changeCurrentEditStatus
|
||||
}
|
||||
}
|
||||
|
||||
function usePageEdit(
|
||||
{
|
||||
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 = {
|
||||
name: string
|
||||
key: string
|
||||
@ -442,13 +468,13 @@ export const useEditStore = defineStore('edit', () => {
|
||||
const bannerList: Ref<IBannerList> = ref({})
|
||||
const cooperPermissions = ref(Object.values(SurveyPermissions))
|
||||
const schemaUpdateTime = ref(Date.now())
|
||||
const { schema, initSchema, getSchemaFromRemote } = useInitializeSchema(surveyId, () => {
|
||||
const { schema, initSchema, getSchemaFromRemote, showLogicEngine, jumpLogicEngine } =
|
||||
useInitializeSchema(surveyId, () => {
|
||||
editGlobalBaseConf.initCounts()
|
||||
})
|
||||
const questionDataList = toRef(schema, 'questionDataList')
|
||||
|
||||
const editGlobalBaseConf = useEditGlobalBaseConf(questionDataList, updateTime)
|
||||
|
||||
function setQuestionDataList(data: any) {
|
||||
schema.questionDataList = data
|
||||
}
|
||||
@ -469,6 +495,7 @@ export const useEditStore = defineStore('edit', () => {
|
||||
cooperPermissions.value = res.data.permissions
|
||||
}
|
||||
}
|
||||
// const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } = useLogicEngine(schema)
|
||||
const {
|
||||
currentEditOne,
|
||||
currentEditKey,
|
||||
@ -483,7 +510,7 @@ export const useEditStore = defineStore('edit', () => {
|
||||
async function init() {
|
||||
const { metaData } = schema
|
||||
if (!metaData || (metaData as any)?._id !== surveyId.value) {
|
||||
getSchemaFromRemote()
|
||||
await getSchemaFromRemote()
|
||||
}
|
||||
currentEditOne.value = null
|
||||
currentEditStatus.value = 'Success'
|
||||
@ -636,6 +663,8 @@ export const useEditStore = defineStore('edit', () => {
|
||||
createNewQuestion,
|
||||
changeSchema,
|
||||
changeThemePreset,
|
||||
compareQuestionSeq
|
||||
compareQuestionSeq,
|
||||
showLogicEngine,
|
||||
jumpLogicEngine
|
||||
}
|
||||
})
|
||||
|
@ -160,3 +160,13 @@
|
||||
.icon-gauge:before {
|
||||
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() {
|
||||
const { submitConf,isFinallyPage } = this.props
|
||||
const { submitConf, isFinallyPage } = this.props
|
||||
return (
|
||||
<div class={['submit-warp', 'preview-submit_wrapper']} onClick={this.handleClick}>
|
||||
<button class="submit-btn" type="primary" onClick={this.submit}>
|
||||
{isFinallyPage ? submitConf.submitTitle : '下一页'}
|
||||
{isFinallyPage ? submitConf.submitTitle : '下一页'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ const adapter = (() => {
|
||||
const list = []
|
||||
|
||||
const exec = (questionData) => {
|
||||
return list.reduce((pre, next) => ({ ...pre, ...next(questionData, pre) }), {})
|
||||
return list.reduce((pre, next, index) => ({ ...pre, index, ...next(questionData, pre) }), {})
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -7,11 +7,12 @@ import { get as _get } from 'lodash-es'
|
||||
export default function (questionConfig) {
|
||||
let dataList = _get(questionConfig, 'dataConf.dataList')
|
||||
// 将题目列表转成对象,并且对题目类型、题目的选项做一些字段的增加和转换
|
||||
const questionData = dataList.reduce((pre, item) => {
|
||||
const questionData = dataList.reduce((pre, item, index) => {
|
||||
Object.assign(pre, {
|
||||
[item.field]: {
|
||||
indexNumber: '',
|
||||
voteTotal: 0,
|
||||
index,
|
||||
...item
|
||||
}
|
||||
})
|
||||
|
@ -13,6 +13,7 @@
|
||||
<script setup>
|
||||
import { inject, provide, computed, onBeforeMount } from 'vue'
|
||||
import QuestionWrapper from './QuestionWrapper.vue'
|
||||
// import { flatten } from 'lodash-es'
|
||||
|
||||
const $bus = inject('$bus')
|
||||
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 length = fields.length
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<QuestionRuleContainer
|
||||
v-if="visible"
|
||||
v-if="visibily"
|
||||
:moduleConfig="questionConfig"
|
||||
:indexNumber="indexNumber"
|
||||
:showTitle="true"
|
||||
@ -9,12 +9,12 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { unref, computed, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import QuestionRuleContainer from '../../materials/questions/QuestionRuleContainer'
|
||||
import { useVoteMap } from '@/render/hooks/useVoteMap'
|
||||
import { useShowOthers } from '@/render/hooks/useShowOthers'
|
||||
import { useShowInput } from '@/render/hooks/useShowInput'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { ruleEngine } from '@/render/hooks/useRuleEngine.js'
|
||||
import { useQuestionStore } from '../stores/question'
|
||||
import { useSurveyStore } from '../stores/survey'
|
||||
|
||||
@ -30,7 +30,7 @@ const props = defineProps({
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['change'])
|
||||
const questionStore = useQuestionStore()
|
||||
@ -39,17 +39,26 @@ const surveyStore = useSurveyStore()
|
||||
const formValues = computed(() => {
|
||||
return surveyStore.formValues
|
||||
})
|
||||
const { showLogicEngine } = storeToRefs(surveyStore)
|
||||
const {
|
||||
changeField,
|
||||
changeIndex,
|
||||
needHideFields,
|
||||
} = storeToRefs(questionStore)
|
||||
// 题型配置转换
|
||||
const questionConfig = computed(() => {
|
||||
let moduleConfig = props.moduleConfig
|
||||
const { type, field, options = [], ...rest } = cloneDeep(moduleConfig)
|
||||
// console.log(field,'这里依赖的formValue,所以change时会触发重新计算')
|
||||
let alloptions = options
|
||||
|
||||
if (type === QUESTION_TYPE.VOTE) {
|
||||
const { options, voteTotal } = useVoteMap(field)
|
||||
const voteOptions = unref(options)
|
||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, voteOptions[index]))
|
||||
moduleConfig.voteTotal = unref(voteTotal)
|
||||
}
|
||||
|
||||
if (
|
||||
NORMAL_CHOICES.includes(type) &&
|
||||
options.filter((optionItem) => optionItem.others).length > 0
|
||||
@ -59,6 +68,7 @@ const questionConfig = computed(() => {
|
||||
alloptions = alloptions.map((obj, index) => Object.assign(obj, othersOptions[index]))
|
||||
moduleConfig.othersValue = unref(othersValue)
|
||||
}
|
||||
|
||||
if (
|
||||
RATES.includes(type) &&
|
||||
rest?.rangeConfig &&
|
||||
@ -77,29 +87,41 @@ const questionConfig = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const { field } = props.moduleConfig
|
||||
|
||||
const visible = computed(() => {
|
||||
const logicshow = computed(() => {
|
||||
// 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(
|
||||
() => visible.value,
|
||||
() => visibily.value,
|
||||
(newVal, oldVal) => {
|
||||
// 题目从显示到隐藏,需要清空值
|
||||
const { field, type, innerType } = props.moduleConfig
|
||||
if (!newVal && oldVal) {
|
||||
let value = ''
|
||||
// 题型是多选,或者子题型是多选(innerType是用于投票)
|
||||
if (type === QUESTION_TYPE.CHECKBOX || innerType === QUESTION_TYPE.CHECKBOX) {
|
||||
value = value ? [value] : []
|
||||
// 如果被隐藏题目有选中值,则需要清空选中值
|
||||
if(formValues.value[field].toString()) {
|
||||
let 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) {
|
||||
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>
|
||||
|
@ -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 AlertDialog from '../components/AlertDialog.vue'
|
||||
import { initRuleEngine } from '@/render/hooks/useRuleEngine.js'
|
||||
|
||||
const route = useRoute()
|
||||
const surveyStore = useSurveyStore()
|
||||
const loadData = (res: any, surveyPath: string) => {
|
||||
@ -44,7 +44,8 @@ const loadData = (res: any, surveyPath: string) => {
|
||||
|
||||
surveyStore.setSurveyPath(surveyPath)
|
||||
surveyStore.initSurvey(questionData)
|
||||
initRuleEngine(logicConf?.showLogicConf)
|
||||
surveyStore.initShowLogicEngine(logicConf?.showLogicConf)
|
||||
surveyStore.initJumpLogicEngine(logicConf.jumpLogicConf)
|
||||
} else {
|
||||
throw new Error(res.errmsg)
|
||||
}
|
||||
|
@ -11,6 +11,11 @@ export const useQuestionStore = defineStore('question', () => {
|
||||
const questionData = ref(null)
|
||||
const questionSeq = ref([]) // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
|
||||
const pageIndex = ref(1) // 当前分页的索引
|
||||
const changeField = ref(null)
|
||||
const changeIndex = computed(() => {
|
||||
return questionData.value[changeField.value].index
|
||||
})
|
||||
const needHideFields = ref([])
|
||||
|
||||
// 题目列表
|
||||
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 {
|
||||
voteMap,
|
||||
questionData,
|
||||
@ -191,6 +212,13 @@ export const useQuestionStore = defineStore('question', () => {
|
||||
setVoteMap,
|
||||
updateVoteMapByKey,
|
||||
initVoteData,
|
||||
updateVoteData
|
||||
updateVoteData,
|
||||
changeField,
|
||||
changeIndex,
|
||||
setChangeField,
|
||||
needHideFields,
|
||||
addNeedHideFields,
|
||||
removeNeedHideFields,
|
||||
getQuestionIndexByField
|
||||
}
|
||||
})
|
||||
|
@ -15,6 +15,8 @@ import 'moment/locale/zh-cn'
|
||||
moment.locale('zh-cn')
|
||||
|
||||
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
|
||||
@ -40,6 +42,7 @@ export const useSurveyStore = defineStore('survey', () => {
|
||||
const whiteData = ref({})
|
||||
const pageConf = ref([])
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const questionStore = useQuestionStore()
|
||||
const { setErrorInfo } = useErrorInfo()
|
||||
@ -156,6 +159,16 @@ export const useSurveyStore = defineStore('survey', () => {
|
||||
if (key in formValues.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 {
|
||||
@ -173,12 +186,15 @@ export const useSurveyStore = defineStore('survey', () => {
|
||||
formValues,
|
||||
whiteData,
|
||||
pageConf,
|
||||
|
||||
initSurvey,
|
||||
changeData,
|
||||
setWhiteData,
|
||||
setSurveyPath,
|
||||
setEnterTime,
|
||||
getEncryptInfo
|
||||
getEncryptInfo,
|
||||
showLogicEngine,
|
||||
initShowLogicEngine,
|
||||
jumpLogicEngine,
|
||||
initJumpLogicEngine
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user