feat: 跳转逻辑 (#388)

This commit is contained in:
dayou 2024-08-08 20:05:16 +08:00 committed by GitHub
parent b5c7ec3008
commit 47ae3ec095
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1453 additions and 185 deletions

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ pnpm-debug.log*
*.sw?
.history
components.d.ts
# 默认的上传文件夹
userUpload

78
web/components.d.ts vendored
View File

@ -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']
}
}

View File

@ -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",

View File

@ -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)
}
findRulesByField(field: string) {
return this.rules.filter((rule) => {
return rule.conditions.filter((condition) => condition.field === field).length
})
}
// 实现前置题删除校验
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
})
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)
}

View File

@ -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
}
// 添加条件规则到规则引擎中
@ -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 rule = this.rules.get(hash)
if (rule) {
const result = rule.match(fact)
const result = rule.match(fact, comparor)
// this.matchCache.set(hash, result);
return result
} else {
@ -196,11 +210,34 @@ export class RuleMatch {
[...this.rules.entries()].filter(([, value]) => {
return [...value.conditions.entries()].filter(([, value]) => {
return value.field === field
})
}).length
})
)
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() {
return Array.from(this.rules.entries()).map(([, value]) => {
return value.toJson()

View File

@ -53,7 +53,7 @@ export const defaultQuestionConfig = {
star: 5,
optionOrigin: '',
originType: 'selected',
innerType:'',
innerType: '',
matrixOptionsRely: '',
numberRange: {
min: {

View 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
}
}
}

View 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> &nbsp;则跳转到 【${getQuestionTitle.value()}】</span> </br>`
)
})
return ruleText.join('')
})
return { hasJumpLogic, getJumpLogicText }
}

View File

@ -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(() => {

View File

@ -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)
}

View File

@ -2,7 +2,10 @@ 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) => {
@ -11,7 +14,7 @@ export const useShowLogicInfo = (field) => {
// 判断该题是否作为了显示逻辑前置题
const isField = logicEngine?.findTargetsByFields(field)?.length > 0
// 判断该题是否作为了显示逻辑目标题
const isTarget = logicEngine?.findTargetsByScope(field)?.length > 0
const isTarget = logicEngine?.findConditionByTarget(field)?.length > 0
return isField || isTarget
})
const getShowLogicText = computed(() => {

View File

@ -14,7 +14,7 @@
:ref="`questionWrapper-${element.field}`"
:moduleConfig="element"
:qIndex="element.qIndex"
:isFirst="index==0"
:isFirst="index == 0"
:indexNumber="element.indexNumber"
:isSelected="currentEditOne === element.qIndex"
: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 }) {
const editStore = useEditStore()
const renderData = computed({
get() {
return props.questionDataList; //filterQuestionPreviewData(props.questionDataList)
return props.questionDataList //filterQuestionPreviewData(props.questionDataList)
},
set(value) {
editStore.moveQuestionDataList(value)

View File

@ -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
}

View File

@ -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)) {
ElMessageBox.alert('该问题被逻辑依赖,请先删除逻辑依赖', '提示', {
ElMessageBox.alert('该题目被显示逻辑关联,请先清除逻辑依赖', '提示', {
confirmButtonText: '确定',
type: 'warning'
})
return
}
if (unref(hasJumpLogic)) {
ElMessageBox.alert('该题目被跳转逻辑关联,请先清除逻辑依赖', '提示', {
confirmButtonText: '确定',
type: 'warning'
})

View File

@ -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)

View 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>

View File

@ -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>

View File

@ -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([])

View File

@ -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 || []

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,3 @@
# 说明
参考node-red样式以logicflow插件的方式实现。

View File

@ -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;
}

View File

@ -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
}

View File

@ -46,11 +46,11 @@ import { DND_GROUP } from '@/management/config/dnd'
import questionMenuConfig, { questionTypeList } from '@/management/config/questionMenuConfig'
import { storeToRefs } from 'pinia'
import { useEditStore } from '@/management/stores/edit'
import { ref } from 'vue'
import { ref } from 'vue'
const editStore = useEditStore()
const { newQuestionIndex } = storeToRefs(editStore)
const { addQuestion, setCurrentEditOne,createNewQuestion } = editStore
const { addQuestion, setCurrentEditOne, createNewQuestion } = editStore
const activeNames = ref([0, 1])
const previewImg = ref('')
@ -61,7 +61,6 @@ questionLoader.init({
typeList: questionTypeList.map((item) => item.type)
})
const onQuestionType = ({ type }) => {
const newQuestion = createNewQuestion({ type })
addQuestion({ question: newQuestion, index: newQuestionIndex.value })

View 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>

View File

@ -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

View File

@ -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 })
}
]
},

View File

@ -20,6 +20,7 @@ import { SurveyPermissions } from '@/management/utils/types/workSpace'
import { getBannerData } from '@/management/api/skin.js'
import { getCollaboratorPermissions } from '@/management/api/space'
import { CODE_MAP } from '../api/base'
import { RuleBuild } from '@/common/logicEngine/RuleBuild'
const innerMetaConfig = {
submit: {
@ -82,10 +83,12 @@ function useInitializeSchema(surveyId: Ref<string>) {
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)
@ -130,6 +133,9 @@ function useInitializeSchema(surveyId: Ref<string>) {
logicConf
}
})
initShowLogicEngine()
initJumpLogicEngine()
} else {
throw new Error(res.errmsg || '问卷不存在')
}
@ -138,7 +144,9 @@ function useInitializeSchema(surveyId: Ref<string>) {
return {
schema,
initSchema,
getSchemaFromRemote
getSchemaFromRemote,
showLogicEngine,
jumpLogicEngine
}
}
@ -289,6 +297,7 @@ function useCurrentEdit({
changeCurrentEditStatus
}
}
function usePageEdit(
{
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 = {
name: string
key: string
@ -432,9 +458,9 @@ 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)
const questionDataList = toRef(schema, 'questionDataList')
function setQuestionDataList(data: any) {
schema.questionDataList = data
}
@ -455,6 +481,7 @@ export const useEditStore = defineStore('edit', () => {
cooperPermissions.value = res.data.permissions
}
}
// const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } = useLogicEngine(schema)
const {
currentEditOne,
currentEditKey,
@ -469,7 +496,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'
@ -618,6 +645,8 @@ export const useEditStore = defineStore('edit', () => {
createNewQuestion,
changeSchema,
changeThemePreset,
compareQuestionSeq
compareQuestionSeq,
showLogicEngine,
jumpLogicEngine
}
})

View File

@ -1,10 +1,9 @@
@font-face {
font-family: 'iconfont';
/* Project id 4263849 */
font-family: 'iconfont'; /* Project id 4263849 */
src:
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff2?t=1717580126029') format('woff2'),
url('//at.alicdn.com/t/c/font_4263849_tj4thybmhq.woff?t=1717580126029') 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.woff2?t=1722515679936') format('woff2'),
url('//at.alicdn.com/t/c/font_4263849_wh46krnnsv.woff?t=1722515679936') format('woff'),
url('//at.alicdn.com/t/c/font_4263849_wh46krnnsv.ttf?t=1722515679936') format('truetype');
}
.iconfont {
@ -162,3 +161,13 @@
.icon-gauge:before {
content: '\e6db';
}
.icon-suoxiao:before {
content: '\e6f4';
}
.icon-fangda:before {
content: '\e6f5';
}
.icon-shiying:before {
content: '\e6f6';
}

View File

@ -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>
)

View File

@ -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({
@ -69,10 +70,33 @@ onBeforeMount(() => {
})
})
// 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) => {
// console.log(field + 'visible'+store.state.ruleEngine.getResult(field, 'question'))
// // -
// return store.state.ruleEngine.getResult(field, 'question')
// return ruleEngine.match(field, 'question', formValues.value)
// }
// })
// watch(()=> jumpMatch,
// (newVal, oldVal) => {
// if(newVal) {
// // renderDatasplit
// handleJump(field, formValues.value)
// }
// })

View File

@ -1,6 +1,6 @@
<template>
<QuestionRuleContainer
v-if="visible"
v-if="visibily"
:moduleConfig="questionConfig"
:indexNumber="indexNumber"
:showTitle="true"
@ -8,17 +8,18 @@
></QuestionRuleContainer>
</template>
<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 { 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'
import { NORMAL_CHOICES, RATES, QUESTION_TYPE } from '@/common/typeEnum.ts'
import { getQuestionIndexByField, findMinKeyInMap } from '@/render/utils/index.js'
const props = defineProps({
indexNumber: {
@ -39,10 +40,10 @@ const surveyStore = useSurveyStore()
const formValues = computed(() => {
return surveyStore.formValues
})
const { dataConf, changeField, showLogicEngine, jumpLogicEngine } = storeToRefs(surveyStore)
const questionConfig = computed(() => {
let moduleConfig = props.moduleConfig
const { type, field, options = [], ...rest } = cloneDeep(moduleConfig)
// console.log(field,'formValuechange')
let alloptions = options
if (type === QUESTION_TYPE.VOTE) {
const { options, voteTotal } = useVoteMap(field)
@ -77,15 +78,14 @@ const questionConfig = computed(() => {
}
})
const { field } = props.moduleConfig
const visible = computed(() => {
const showMatch = computed(() => {
// computedmatch
return ruleEngine.match(field, 'question', formValues.value)
const result = showLogicEngine.value.match(props.moduleConfig.field, 'question', formValues.value)
return result === undefined ? true : result
})
watch(
() => visible.value,
() => showMatch.value,
(newVal, oldVal) => {
//
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) => {
emit('change', data)
//

View File

@ -1,6 +0,0 @@
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
export const ruleEngine = new RuleMatch()
export const initRuleEngine = (ruleConf) => {
ruleEngine.fromJson(ruleConf)
}

View File

@ -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)
}

View File

@ -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
@ -39,6 +41,7 @@ export const useSurveyStore = defineStore('survey', () => {
const formValues = ref({})
const whiteData = ref({})
const pageConf = ref([])
const changeField = ref(null)
const router = useRouter()
const questionStore = useQuestionStore()
@ -153,11 +156,21 @@ export const useSurveyStore = defineStore('survey', () => {
// 用户输入或者选择后,更新表单数据
const changeData = (data) => {
let { key, value } = data
changeField.value = key
if (key in formValues.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 {
surveyPath,
isMobile,
@ -173,12 +186,16 @@ export const useSurveyStore = defineStore('survey', () => {
formValues,
whiteData,
pageConf,
changeField,
initSurvey,
changeData,
setWhiteData,
setSurveyPath,
setEnterTime,
getEncryptInfo
getEncryptInfo,
showLogicEngine,
initShowLogicEngine,
jumpLogicEngine,
initJumpLogicEngine
}
})

View File

@ -30,3 +30,23 @@ export const formatLink = (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
}