提交 9448b4ff authored 作者: 王鹏飞's avatar 王鹏飞

chore: update

上级 7afc9c57
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
"@element-plus/icons-vue": "^2.0.10", "@element-plus/icons-vue": "^2.0.10",
"@tinymce/tinymce-vue": "^5.0.0", "@tinymce/tinymce-vue": "^5.0.0",
"@vue-flow/additional-components": "^1.3.0", "@vue-flow/additional-components": "^1.3.0",
"@vue-flow/core": "^1.5.0", "@vue-flow/controls": "^1.0.3",
"@vue-flow/core": "^1.14.3",
"@vueuse/core": "^9.12.0", "@vueuse/core": "^9.12.0",
"axios": "^1.3.3", "axios": "^1.3.3",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
...@@ -19,9 +20,7 @@ ...@@ -19,9 +20,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pinia": "^2.0.30", "pinia": "^2.0.30",
"vue": "^3.2.47", "vue": "^3.2.47",
"vue-resizable": "^2.1.5", "vue-router": "^4.1.6"
"vue-router": "^4.1.6",
"vue3-smooth-dnd": "^0.0.2"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.2.0", "@rushstack/eslint-patch": "^1.2.0",
...@@ -1429,6 +1428,15 @@ ...@@ -1429,6 +1428,15 @@
"vue": "^3.2.25" "vue": "^3.2.25"
} }
}, },
"node_modules/@vue-flow/controls": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@vue-flow/controls/-/controls-1.0.3.tgz",
"integrity": "sha512-9NhjeztTGQsvg1GMuzusZebFRtov5KTHhV8xkgy7qiI0w3X16aOt7LThxKygRMeBggT9IsGA7xef4/iR8bWljA==",
"peerDependencies": {
"@vue-flow/core": "^1.12.2",
"vue": "^3.2.37"
}
},
"node_modules/@vue-flow/core": { "node_modules/@vue-flow/core": {
"version": "1.14.3", "version": "1.14.3",
"resolved": "https://registry.npmmirror.com/@vue-flow/core/-/core-1.14.3.tgz", "resolved": "https://registry.npmmirror.com/@vue-flow/core/-/core-1.14.3.tgz",
...@@ -5359,11 +5367,6 @@ ...@@ -5359,11 +5367,6 @@
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
} }
}, },
"node_modules/smooth-dnd": {
"version": "0.12.1",
"resolved": "https://registry.npmmirror.com/smooth-dnd/-/smooth-dnd-0.12.1.tgz",
"integrity": "sha512-Dndj/MOG7VP83mvzfGCLGzV2HuK1lWachMtWl/Iuk6zV7noDycIBnflwaPuDzoaapEl3Pc4+ybJArkkx9sxPZg=="
},
"node_modules/socks": { "node_modules/socks": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.7.1.tgz", "resolved": "https://registry.npmmirror.com/socks/-/socks-2.7.1.tgz",
...@@ -6162,11 +6165,6 @@ ...@@ -6162,11 +6165,6 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/vue-resizable": {
"version": "2.1.7",
"resolved": "https://registry.npmmirror.com/vue-resizable/-/vue-resizable-2.1.7.tgz",
"integrity": "sha512-zEbWhRR8iXT8+nt3u8rkfrNpkPNsPkf7HteBh+AlPIsJ7rf9fyNwMqr0Q4FRzIpNIpZD5Zrr4+3+YELU0vc1Iw=="
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz",
...@@ -6204,17 +6202,6 @@ ...@@ -6204,17 +6202,6 @@
"typescript": "*" "typescript": "*"
} }
}, },
"node_modules/vue3-smooth-dnd": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/vue3-smooth-dnd/-/vue3-smooth-dnd-0.0.2.tgz",
"integrity": "sha512-5OlpoZ1fmpA1CcjvRwJ9P3MEPZ0nnnYuIf/9zMGKF8i7jTE6fQX3oeY0jDHNtOjuCmvTSbc0AYq18M9QIF32Ew==",
"dependencies": {
"smooth-dnd": "^0.12.1"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/webpack-sources": { "node_modules/webpack-sources": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz",
......
...@@ -15,12 +15,11 @@ ...@@ -15,12 +15,11 @@
"cert": "node ./cert.js" "cert": "node ./cert.js"
}, },
"dependencies": { "dependencies": {
"@vue-flow/additional-components": "^1.3.0",
"@vue-flow/core": "^1.5.0",
"vue3-smooth-dnd": "^0.0.2",
"vue-resizable": "^2.1.5",
"@element-plus/icons-vue": "^2.0.10", "@element-plus/icons-vue": "^2.0.10",
"@tinymce/tinymce-vue": "^5.0.0", "@tinymce/tinymce-vue": "^5.0.0",
"@vue-flow/additional-components": "^1.3.0",
"@vue-flow/controls": "^1.0.3",
"@vue-flow/core": "^1.14.3",
"@vueuse/core": "^9.12.0", "@vueuse/core": "^9.12.0",
"axios": "^1.3.3", "axios": "^1.3.3",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
......
<script setup>
import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
import { computed } from 'vue'
const props = defineProps({
id: { type: String, required: true },
sourceX: { type: Number, required: true },
sourceY: { type: Number, required: true },
targetX: { type: Number, required: true },
targetY: { type: Number, required: true },
sourcePosition: { type: String, required: true },
targetPosition: { type: String, required: true },
data: { type: Object, required: false },
markerEnd: { type: String, required: false },
style: { type: Object, required: false }
})
const { removeEdges } = useVueFlow()
const path = computed(() => getBezierPath(props))
</script>
<script>
export default {
inheritAttrs: false
}
</script>
<template>
<!-- You can use the `BaseEdge` component to create your own custom edge more easily -->
<BaseEdge :id="id" :style="style" :path="path[0]" :marker-end="markerEnd" />
<!-- Use the `EdgeLabelRenderer` to escape the SVG world of edges and render your own custom label in a `<div>` ctx -->
<EdgeLabelRenderer>
<div
:style="{
pointerEvents: 'all',
position: 'absolute',
transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`
}"
class="nodrag nopan">
<button class="edgebutton" @click="removeEdges([id])">×</button>
</div>
</EdgeLabelRenderer>
</template>
<script setup lang="ts">
import TriggeringConditions1 from './components/triggeringConditions/TriggeringConditions1.vue'
</script>
<template>
<component :is="TriggeringConditions1"></component>
</template>
<script lang="ts">
export default {
inheritAttrs: false
}
</script>
<script setup lang="ts">
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Controls } from '@vue-flow/controls'
import Sidebar from './Sidebar.vue'
import CustomNode from './CustomNode.vue'
import CustomEdge from './CustomEdge.vue'
let id = 0
const getId = () => `node_${id++}`
const { findNode, onConnect, addEdges, addNodes, project, vueFlowRef } = useVueFlow()
const onDragOver = (event: DragEvent) => {
event.preventDefault()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'
}
}
onConnect(params => {
addEdges([{ ...params, type: 'custom' }])
})
const onDrop = (event: DragEvent) => {
let data: any = event.dataTransfer?.getData('application/vueflow') || ''
try {
data = JSON.parse(data)
} catch (error) {
console.log(error)
}
if (!vueFlowRef.value) return
const { left, top } = vueFlowRef.value.getBoundingClientRect()
const position = project({ x: event.clientX - left, y: event.clientY - top })
const newNode = { id: getId(), type: 'custom', position, label: data.name, data }
addNodes([newNode])
// align node position after drop, so it's centered to the mouse
nextTick(() => {
const node = findNode(newNode.id)
if (!node) return
const stop = watch(
() => node.dimensions,
dimensions => {
if (dimensions.width > 0 && dimensions.height > 0) {
node.position = {
x: node.position.x - node.dimensions.width / 2,
y: node.position.y - node.dimensions.height / 2
}
stop()
}
},
{ deep: true, flush: 'post' }
)
})
}
</script>
<template>
<div class="flow">
<Sidebar></Sidebar>
<el-card shadow="never" class="flow-main" @drop="onDrop">
<VueFlow fit-view-on-init @dragover="onDragOver" style="min-height: 50vh" v-bind="$attrs">
<template #node-custom="node">
<CustomNode :node="node" />
</template>
<template #edge-custom="props">
<CustomEdge v-bind="props" />
</template>
<Controls />
</VueFlow>
<slot name="footer"></slot>
</el-card>
</div>
</template>
<style>
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';
@import '@vue-flow/controls/dist/style.css';
.flow {
display: flex;
column-gap: 20px;
}
.flow-main {
flex: 1;
}
</style>
<template>
<section>
<div class="flow-left">
<slot name="left" />
</div>
<div class="flow-main"></div>
<div class="flow-left">
<slot name="left" />
</div>
</section>
</template>
<script setup lang="ts">
const list = ref([
{
name: '触发条件',
children: [
{ name: '实时触发', type: '触发条件', icon: '' },
{ name: '加入群组', type: '触发条件', icon: '' },
{ name: '变更属性', type: '触发条件', icon: '' },
{ name: '公众号', type: '触发条件', icon: '' },
{ name: '抖音', type: '触发条件', icon: '' },
{ name: '小红书', type: '触发条件', icon: '' },
{ name: '微博', type: '触发条件', icon: '' },
{ name: '钉钉', type: '触发条件', icon: '' }
]
},
{
name: '营销动作',
children: [
{ name: '终止旅程', type: '营销动作', icon: '' },
{ name: '加入群组', type: '营销动作', icon: '' },
{ name: '移除群组', type: '营销动作', icon: '' },
{ name: '变更属性', type: '营销动作', icon: '' },
{ name: '延时处理', type: '营销动作', icon: '' },
{ name: '内部通知', type: '营销动作', icon: '' },
{ name: '短信', type: '营销动作', icon: '' },
{ name: '邮件', type: '营销动作', icon: '' },
{ name: '公众号', type: '营销动作', icon: '' },
{ name: '抖音', type: '营销动作', icon: '' },
{ name: '小红书', type: '营销动作', icon: '' },
{ name: '微博', type: '营销动作', icon: '' },
{ name: '钉钉', type: '营销动作', icon: '' }
]
},
{
name: '条件分支',
children: [
{ name: '属性判断', type: '条件分支', icon: '' },
{ name: '标签判断', type: '条件分支', icon: '' },
{ name: '群组判断', type: '条件分支', icon: '' },
{ name: '事件判断', type: '条件分支', icon: '' },
{ name: '时间判断', type: '条件分支', icon: '' },
{ name: '公众号', type: '条件分支', icon: '' },
{ name: '钉钉', type: '条件分支', icon: '' }
]
}
])
const onDragStart = (event: DragEvent, data: any) => {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', JSON.stringify(data))
event.dataTransfer.effectAllowed = 'move'
}
}
</script>
<template>
<el-card shadow="never" class="flow-sidebar">
<dl v-for="(parent, index) in list" :key="index">
<dt>{{ parent.name }}</dt>
<dd>
<ul>
<li
v-for="(item, index) in parent.children"
:key="index"
:draggable="true"
@dragstart="event => onDragStart(event, item)">
<img src="" alt="" />
<p>{{ item.name }}</p>
</li>
</ul>
</dd>
</dl>
</el-card>
</template>
<style lang="scss">
.flow-sidebar {
width: 300px;
dt {
color: #fff;
line-height: 40px;
text-align: center;
background-color: rgb(86, 119, 34);
}
dd {
padding: 20px 0;
}
ul {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
li {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: grab;
user-select: none;
img {
width: 40px;
height: 40px;
background-color: rgb(86, 119, 34);
border-radius: 50%;
overflow: hidden;
}
p {
font-size: 12px;
text-align: center;
}
}
}
</style>
<script setup lang="ts">
import type { NodeProps } from '@vue-flow/core'
import { useVueFlow, Handle, Position } from '@vue-flow/core'
import { Setting, Delete } from '@element-plus/icons-vue'
const props = defineProps<{ node: NodeProps }>()
const emit = defineEmits<{ (e: 'setting'): void }>()
const { findNode, removeNodes } = useVueFlow()
// 删除
function onRemove() {
const node = findNode(props.node.id)
if (node) removeNodes([node])
}
</script>
<template>
<div class="custom-node">
<div class="node-toolbar">
<el-icon @click="onRemove"><Delete /></el-icon>
<el-icon @click="emit('setting')"><Setting /></el-icon>
</div>
<div class="custom-node__inner">
<slot :node="node" />
</div>
</div>
<Handle id="right" class="handle" :position="Position.Right" />
<Handle id="left" class="handle" :position="Position.Left" />
<Handle id="bottom" class="handle" :position="Position.Bottom" />
</template>
<style lang="scss" scoped>
.custom-node {
position: relative;
width: 80px;
height: 80px;
border: 1px solid #000;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
&:hover {
.node-toolbar {
display: flex;
}
}
}
.node-toolbar {
position: absolute;
left: 50%;
top: -30px;
display: none;
align-items: center;
justify-content: center;
font-size: 20px;
transform: translate(-50%);
.el-icon {
margin: 10px;
cursor: pointer;
}
}
</style>
<!-- 定时触发 -->
<script setup>
import NodeTemplate from '../NodeTemplate.vue'
const props = defineProps({ node: Object })
// 设置
const settingVisible = ref(false)
const formRef = ref()
const form = reactive({ id: '', name: '', type: '' })
watchEffect(() => {
if (props.node) Object.assign(form, props.node.data)
})
const rules = ref({})
// 保存
function handleSubmit() {}
</script>
<template>
<NodeTemplate :node="node" @setting="settingVisible = true">
{{ node.label }}
</NodeTemplate>
<el-dialog title="设置组件" append-to-body width="600px" v-model="settingVisible" v-if="settingVisible">
<el-form ref="formRef" :model="form" :rules="rules" label-suffix=":">
<el-row justify="space-between">
<el-form-item label="组件类型"> {{ form.type }} </el-form-item>
<el-form-item label="组件名称">
{{ form.name }}
</el-form-item>
<el-form-item label="分值">{{ form.score }} </el-form-item>
</el-row>
<slot name="form" :form="form"></slot>
</el-form>
<template #footer>
<el-row justify="center">
<el-button plain auto-insert-space @click="settingVisible = false">关闭</el-button>
<el-button type="primary" auto-insert-space @click="handleSubmit">保存</el-button>
</el-row>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
import { Delete, Setting } from '@element-plus/icons-vue'
const props: any = defineProps({
id: {
type: String,
required: true
},
sourceX: {
type: Number,
required: true
},
sourceY: {
type: Number,
required: true
},
targetX: {
type: Number,
required: true
},
targetY: {
type: Number,
required: true
},
sourcePosition: {
type: String,
required: true
},
targetPosition: {
type: String,
required: true
},
data: {
type: Object,
required: false
},
markerEnd: {
type: String,
required: false
},
style: {
type: Object,
required: false
}
})
const { applyEdgeChanges } = useVueFlow()
const onClick = (evt: any, id: any) => {
applyEdgeChanges([{ type: 'remove', id }])
evt.stopPropagation()
}
const getRightLefttEdge = (props: any) => {
return [
`
M ${props.sourceX - 7.5} ${props.sourceY}
L ${props.sourceX + 15} ${props.sourceY}
Q ${props.sourceX + 30} ${props.sourceY}
${props.sourceX + 30} ${props.sourceY + (props.sourceY > props.targetY ? -15 : +15)}
L ${props.sourceX + 30} ${props.targetY - (props.sourceY > props.targetY ? 45 : 75)}
Q ${props.sourceX + 30} ${props.targetY - 60}
${props.sourceX + 15} ${props.targetY - 60}
L ${props.targetX - 30} ${props.targetY - 60}
Q ${props.targetX - 45} ${props.targetY - 60}
${props.targetX - 45} ${props.targetY - 45}
L ${props.targetX - 45} ${props.targetY - 15}
Q ${props.targetX - 45} ${props.targetY}
${props.targetX - 30} ${props.targetY}
L ${props.targetX} ${props.targetY}`,
(props.sourceX + props.targetX) / 2,
props.targetY - 60
]
}
const getBottomLeftEdge = (props: any) => {
const countQ: any = props.sourceY < props.targetY - 30 ? 45 : 15
const countL: any = props.sourceY < props.targetY - 30 ? -15 : 15
return [
`
M ${props.sourceX} ${props.sourceY - 7.5}
L ${props.sourceX} ${props.sourceY + 15}
Q ${props.sourceX} ${props.sourceY + 30}
${props.sourceX - 15} ${props.sourceY + 30}
L ${props.targetX - 15} ${props.sourceY + 30}
Q ${props.targetX - 30} ${props.sourceY + 30}
${props.targetX - 30} ${props.sourceY + parseInt(countQ)}
L ${props.targetX - 30} ${props.targetY + parseInt(countL)}
Q ${props.targetX - 30} ${props.targetY}
${props.targetX - 15} ${props.targetY}
L ${props.targetX} ${props.targetY}
`,
props.targetX - 15,
props.targetY
]
}
const getLeftLeftEdge = (props: any) => {
return [
`M ${props.sourceX}, ${props.sourceY}
C ${props.targetX}, ${props.sourceY}
${props.targetX}, ${props.targetY}
${props.targetX}, ${props.targetY}`,
props.sourceX - 25,
props.sourceY
]
}
const getCurvedEdge = (props: any, margin = 8) => {
return [
`M${props.sourceX + margin}, ${props.sourceY} C ${props.sourceX} ${props.targetY} ${props.sourceX} ${
props.targetY
} ${props.targetX}, ${props.targetY}`,
(props.sourceX + props.targetX) / 2,
props.targetY
]
}
const getDirectLine = (props: any) => {
return [
`M ${props.sourceX} ${props.sourceY} L ${props.targetX} ${props.targetY}`,
(props.sourceX + props.targetX) / 2,
(props.sourceY + props.targetY) / 2
]
}
const path = computed(() => {
/* Q1, Q2, Q3, Q4 stands for Quandrants, the plane is divided by 4 zones.
Primary axis are vertical and horizontal axis passing through the point (sourceX, sourceY)*/
const [Q1, Q2, Q3, Q4] = [
props.sourceX < props.targetX && props.sourceY > props.targetY,
props.sourceX > props.targetX && props.sourceY > props.targetY,
props.sourceX > props.targetX && props.sourceY < props.targetY,
props.sourceX < props.targetX && props.sourceY < props.targetY
]
if (props.sourcePosition === 'left') {
if (props.targetPosition === 'left') {
if (Q2) {
return getLeftLeftEdge(props)
}
} else if (props.targetPosition === 'right') {
return getBezierPath(props)
}
return getCurvedEdge(props)
} else if (props.sourcePosition === 'right') {
if (props.targetPosition === 'left') {
if (/right-redirector/.test(props.id) && (Q2 || Q3)) {
// Redirector Edge
return getRightLefttEdge(props)
}
}
return getBezierPath(props)
} else if (props.sourcePosition === 'bottom') {
if (props.targetPosition === 'left') {
if (Q3 || Q2) {
return getBottomLeftEdge(props)
}
} else if (props.targetPosition === 'right') {
} else if (props.targetPosition === 'bottom') {
} else {
}
return getCurvedEdge(props, 0)
}
return getDirectLine(props)
})
let strokeColor: any = ref('')
let condition = $ref('1')
const handleCondition = function () {
const color = {
'1': 'red',
'2': 'green',
'3': 'yellow'
}
strokeColor.value = color[condition as '1']
}
</script>
<template>
<BaseEdge
:id="id"
:style="{
'stroke-width': 1,
stroke: strokeColor
}"
:path="path[0]"
marker-end="url(#triangle)"
markerWidth="1"
/>
<EdgeLabelRenderer>
<div
:style="{
pointerEvents: 'all',
position: 'absolute',
transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
'z-index': 9999
}"
class="nodrag nopan"
>
<div class="edge__button_delete">
<el-tooltip placement="top">
<template #content>
<div class="pop-box">
<div style="display: flex; align-items: center">
<el-radio-group @change="handleCondition" v-model="condition" class="ml-4">
<el-radio label="1" size="large"></el-radio>
<el-radio label="2" size="large"></el-radio>
<el-radio label="3" size="large">无条件</el-radio>
</el-radio-group>
<el-button style="margin-left: 20px" @click="event => onClick(event, id)">删除</el-button>
</div>
</div>
</template>
<el-icon><Setting /></el-icon>
</el-tooltip>
</div>
</div>
</EdgeLabelRenderer>
</template>
<style scoped>
svg {
transform: translate(-4%, -4%);
}
.edge__button_delete {
display: flex;
justify-content: center;
align-items: center;
border: 2px black solid;
border-radius: 1rem;
padding: 0.1rem;
background-color: #f2f5f7;
}
.edge__button_delete:hover {
transform: scale(1.2);
transition: transform 0.5s 0.1s;
}
</style>
<script setup lang="ts">
import SidebarVue from './Sidebar.vue'
</script>
<template>
<div class="border rounded w-25">
<SidebarVue></SidebarVue>
</div>
</template>
<style scoped>
.file-input label:hover {
transform: scale(1.02);
}
.file-input label {
display: block;
position: relative;
width: auto;
height: 3rem;
border-radius: 2rem;
padding: 1rem;
background: linear-gradient(40deg, #297cbc, #16d462);
box-shadow: 0 4px 7px rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: bold;
transition: transform 0.2s ease-out;
overflow: hidden;
}
.file {
opacity: 0%;
width: 100%;
height: 100%;
position: absolute;
cursor: pointer;
}
.btn {
font-size: small;
margin: 2px;
}
.btn:hover {
background-color: #eee;
}
.options {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 1rem;
margin: 0.2rem;
padding: 0.5rem;
}
</style>
<script setup lang="ts">
// Icons
import ConnectionIcon from '@/components/ConnectionIcon.vue'
const onDragStart = (event: any, nodeType: any) => {
if (event.dataTransfer) {
console.log(nodeType, 'onDragStart')
event.dataTransfer.setData('application/vueflow', nodeType)
event.dataTransfer.effectAllowed = 'move'
}
}
</script>
<template>
<aside>
<div class="sidebar-box">
<!-- Simple Text Custom Template -->
<div class="sidebar-item">
<h3>触发条件</h3>
<div class="icons">
<div class="btn" :draggable="true" @dragstart="onDragStart($event, 'simple-text')">
<ConnectionIcon name="1" />
</div>
</div>
</div>
</div>
</aside>
</template>
<style scoped lang="scss">
.sidebar-box {
width: 300px;
h3 {
text-align: left;
}
.icons {
display: flex;
flex-wrap: wrap;
}
.sidebar-item {
margin-bottom: 30px;
}
.btn {
width: 50px;
height: 50px;
border: 1px solid #000;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin: 10px 10px 0 0;
}
}
</style>
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core'
import ConnectionIcon from '@/components/ConnectionIcon.vue'
// custom Top Menu import
import TopMenu from './ToolMenu.vue'
// Usage of Store Pinia
import { useStore } from '@/stores/main.js'
const store = useStore()
// Computed Values from Store.
let localStates: any = computed(() => {
return store.getMessageById(props.id)
})
////////////////////////////////////////////.
// Renderless resizable textarea
const textarea = ref(null) // Access the textarea by his ref.
const resizeTextarea = (event: any) => {
event.target.style.height = 'auto'
event.target.style.height = event.target.scrollHeight + 4 + 'px'
}
onMounted(() => {
// textarea.value.style.height = textarea.value.scrollHeight + 'px'
})
////////////////////////////////////////////.
// Watching Selected Manual event.
watch(
() => props.selected,
isSelected => (selectedColor.value = isSelected)
)
////////////////////////////////////////////.
// Local Variables and props related things.
const transparent = ref(true)
let selectedColor = ref(false)
const props = defineProps({
id: String,
selected: Boolean
})
</script>
<template>
<Handle id="right" class="handle" :position="Position.Right" />
<Handle id="left" class="handle" :position="Position.Left" />
<Handle id="bottom" class="handle" :position="Position.Bottom" style="top: 101%" />
<div @mouseenter="transparent = false" @mouseleave="transparent = true">
<div class="top-menu">
<TopMenu :eid="props.id" :transparent="transparent"></TopMenu>
</div>
<div :id="id + 'subtitle'" class="icon-item">
<ConnectionIcon name="1"></ConnectionIcon>
</div>
</div>
</template>
<style scoped lang="scss">
.icon-item {
width: 80px;
height: 80px;
border: 1px solid #000;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.top-menu {
display: flex;
align-items: center;
justify-content: center;
}
</style>
<script setup lang="ts">
import { useVueFlow } from '@vue-flow/core'
import { Setting, Delete } from '@element-plus/icons-vue'
// Icons
// import TrashIcon from '../assets/svg/TrashIcon.svg'
// import GearIcon from '../assets/svg/GearIcon.svg'
// import Check2All from '../assets/svg/check2-all.svg'
// import copyIcon from '../assets/svg/copyIcon.svg'
import { copyVueNode } from '@/utils/createVueNode'
// Usage of Store Pini
import { useStore } from '@/stores/main.js'
const store = useStore()
const { addNodes, applyEdgeChanges, applyNodeChanges, getNode, toObject }: any = useVueFlow()
// Computed Values from Store.
const localStates: any = computed(() => {
return store.getMessageById(props.eid)
})
const messages: any = computed(() => {
return store.getMessages()
})
////////////////////////////////////////////.
// Elements related methods.
const deleteElement = (event: any, id: any) => {
event.stopPropagation()
let connectedEdges = toObject().edges.filter((edge: any) => [edge.target, edge.source].some(item => item === id))
const changeEdgesObjectArray = connectedEdges.map((item: any) => ({
type: 'remove',
id: item.id
}))
applyNodeChanges([{ type: 'remove', id }])
applyEdgeChanges(changeEdgesObjectArray)
store.layers.messages = store.layers.messages.filter(element => {
return element.id !== id
})
}
////////////////////////////////////////////.
// Ref Targeting Hidden Color Input
const colorInput: any = ref(null)
const onClickColorInput = () => {
colorInput.value.click()
}
////////////////////////////////////////////.
// handling container in and out
const hanldeContainer = (e: any) => {
menu.value = !menu.value
function containerParser(containerNodes: any) {
return containerNodes.map((item: any) => {
const label = messages.value.find((element: any) => element.id === item.id).label
return { id: item.id, label: label }
})
}
let containersNodes = toObject().nodes.filter((item: any) => item.type === 'container')
containers.value = containerParser(containersNodes)
let currentObject = toObject().nodes.filter((item: any) => item.id === props.eid)
if (currentObject[0].parentNode) {
parent.value = currentObject[0].parentNode
} else {
parent.value = null
}
}
////////////////////////////////////////////.
// Local Variables and props related things.
const menu = ref(true)
const containers: any = ref(null)
const parent = ref(null)
const props = defineProps({
eid: String,
transparent: Boolean
})
////////////////////////////////////////////.
// Watch over transparent value
watch(
() => props.transparent,
transparent => {
if (transparent === true) {
menu.value = true
}
}
)
////////////////////////////////////////////.
// Set the parent of the current node.
const setParent = (parentId: any) => {
menu.value = true
const node = getNode.value(props.eid)
if (node.parentNode === parentId) {
delete node.parentNode
} else {
if (parentId !== props.eid) {
node.parentNode = parentId
} else {
return
}
}
}
</script>
<template>
<div class="button-menu-container" :class="{ transparent: transparent }">
<div @click="event => deleteElement(event, eid)">
<el-icon><Delete /></el-icon>
</div>
<div style="position: relative" @click.self="hanldeContainer">
<el-icon @click.stop="hanldeContainer"><Setting /></el-icon>
<div :class="{ transparent: menu }" class="menu-container">
<div style="width: 80%; border-bottom: 1px black solid">Parent Container</div>
<ul>
<li
v-for="item in containers"
@click="
() => {
setParent(item.id)
}
"
>
<div
:style="{
width: item.id === parent ? '90%' : '100%'
}"
>
{{ item.label }}
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<style scoped>
li div {
transition: opacity 0.5s 0s;
}
/* Button Menu Top Container */
.button-menu-container {
display: flex;
margin-bottom: 0.1rem;
background-color: white;
width: fit-content;
border-radius: 1rem;
overflow: visible;
transition: opacity 1s 0.5s;
}
.button-menu-container > div {
display: flex;
padding: 0.35rem;
}
.button-menu-container > div:hover {
background-color: #eee;
}
/* Button Menu Top Container */
/* Colored Color Input with input Hidden */
.color-input {
width: 0px;
height: 0px;
padding: 0.5rem;
border-radius: 50%;
overflow: hidden;
position: relative;
}
.container-color {
position: absolute;
width: 0;
height: 0;
transform: translate(-50%, -50%);
transform: scale(0);
}
/* Colored Color Input with input Hidden */
.menu-container {
display: flex;
flex-direction: column;
align-items: center;
position: absolute;
background-color: white;
border-radius: 1rem;
width: 15rem;
left: 125%;
font-size: x-small;
border: 1px black solid;
padding: 0.2rem 0.4rem;
}
ul {
width: 100%;
padding: 0;
margin: 0.3rem;
}
li {
display: flex;
border: 1px rgb(162, 159, 159) dashed;
width: 100%;
border-radius: 0.3rem;
padding: 0.3rem;
margin: 0.1rem 0rem;
}
li::marker {
content: none;
}
li:hover {
background-color: #eee;
}
li:active {
transform: scale(1.1);
}
span:hover,
svg:hover {
cursor: pointer;
}
.transparent {
opacity: 0%;
transition: opacity 0.5s 0s;
}
</style>
...@@ -10,13 +10,6 @@ const routes: RouteRecordRaw[] = [ ...@@ -10,13 +10,6 @@ const routes: RouteRecordRaw[] = [
path: '/trip/my', path: '/trip/my',
component: Layout, component: Layout,
children: [{ path: '', component: () => import('./views/Index.vue') }] children: [{ path: '', component: () => import('./views/Index.vue') }]
},
{
path: '/template',
component: Layout,
children: [
{ path: '', component: () => import('./views/Template.vue') }
]
} }
] ]
......
<script setup lang="ts">
import TripFlow from '@/components/flow/Index.vue'
const elements = ref([
{
id: '1',
type: 'custom',
label: '实时触发',
position: { x: 0, y: 0 },
data: { name: '实时触发', type: '触发条件', score: 10 }
},
{
id: '2',
type: 'custom',
label: '实时触发',
position: { x: 100, y: 100 },
data: { name: '实时触发', type: '触发条件', score: 12 }
}
])
watchEffect(() => {
console.log(elements)
})
// 保存
function handleSubmit() {}
</script>
<template> <template>
<AppCard></AppCard> <AppCard title="自由旅程">
<el-card shadow="never" style="margin-bottom: 20px"></el-card>
<TripFlow v-model="elements" style="height: 80vh">
<template #footer>
<el-row justify="center">
<el-button type="primary" auto-insert-space @click="handleSubmit">保存</el-button>
</el-row>
</template>
</TripFlow>
</AppCard>
</template> </template>
<script setup lang="ts">
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Background, Controls, MiniMap } from '@vue-flow/additional-components'
import SimpleTextVue from '../components/SimpleText.vue'
import GlobalMenu from '../components/GlobalMenu.vue'
// Externalise node creation process on Drop here
import { createVueNode } from '@/utils/createVueNode'
////////////////////////////////////////////.
// Usage of Store Pinia
import { useStore } from '@/stores/main.js'
// Custom Connection line and Custom Edge
import CustomEdgeVue from '../components/CustomEdge.vue'
const store: any = useStore()
const { setInteractive, onConnect, addEdges, addNodes, project, onPaneReady } = useVueFlow()
// Methods that helps, centering the vue.
onPaneReady(({ fitView }) => {
fitView()
})
////////////////////////////////////////////.
// The dragAndDrop function that helps creating new nodes
// Just by dragging elements into the canvas.
// DragOver from the Sidebars.
const onDragOver = (event: any) => {
event.preventDefault()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'
}
}
////////////////////////////////////////////.
// The onDrop event handler that is responsible for the creation
const onDrop = (event: any) => {
// console.log(event.target.parentNode);
createVueNode(event, addNodes, project, store)
}
////////////////////////////////////////////.
// OnConnect node event, there is more work to do here.
onConnect((params: any) => {
;(params.type = 'custom'), (params.animated = false)
addEdges([params])
})
////////////////////////////////////////////.
// Handling Clicked message to the message editor
// OnClick : connect message clicked to the message editor.
const onClick = (event: any) => {
if (event.node.type == 'facebook-message') {
if (messageToEdit.value == event.node.id) {
messageToEdit.value = ''
} else {
messageToEdit.value = event.node.id
}
}
store.messageToEdit = messageToEdit.value
}
////////////////////////////////////////////.
// Implementation of a global key listener
let onKeyUp = (event: any) => {
switch (event.key) {
case 'AltGraph':
setInteractive(true)
break
// Close the editor if Escape key is pressed
case ' ':
messageToEdit.value = ''
break
default:
break
}
}
let onKeyDown = (event: any) => {
switch (event.key) {
case 'AltGraph':
setInteractive(false)
break
default:
break
}
}
onMounted(() => {
window.addEventListener('keydown', onKeyDown)
window.addEventListener('keyup', onKeyUp)
})
////////////////////////////////////////////.
// Local Variables and props related things.
let messageToEdit = ref('')
const elements = ref([
{
type: 'simple-text',
dimensions: { width: 100, height: 100 },
handleBounds: {
source: [
{ id: 'right', position: 'right', x: 281.99987873053556, y: 73.5937466308119, width: 16, height: 16 },
{ id: 'left', position: 'left', x: -3.999995328414287, y: 73.5937466308119, width: 16, height: 16 },
{
id: 'bottom',
position: 'bottom',
x: 138.99998331652628,
y: 104.20308685759466,
width: 16,
height: 16
}
],
input: [
{ id: 'right', position: 'right', x: 282, y: 73.59375, width: 16, height: 16 },
{ id: 'left', position: 'left', x: -4, y: 73.59375, width: 16, height: 16 },
{ id: 'bottom', position: 'bottom', x: 139, y: 100.15625, width: 16, height: 16 }
]
},
computedPosition: { x: 180, y: 210, z: 0 },
selected: false,
dragging: false,
resizing: false,
initialized: true,
data: {},
events: {},
id: 'simple-textc9xkhtp5yypo7qsnp9e1gh',
position: { x: 180, y: 210 },
label: 'simple-text node'
},
{
type: 'simple-text',
dimensions: { width: 294, height: 103 },
handleBounds: {
source: [
{
id: 'right',
position: 'right',
x: 281.99987873053556,
y: 73.59372582307907,
width: 16,
height: 16
},
{ id: 'left', position: 'left', x: -3.999995328414287, y: 73.59372582307907, width: 16, height: 16 },
{
id: 'bottom',
position: 'bottom',
x: 138.99998331652628,
y: 104.20308685759466,
width: 16,
height: 16
}
],
input: [
{
id: 'right',
position: 'right',
x: 281.99987873053556,
y: 73.59372582307907,
width: 16,
height: 16
},
{ id: 'left', position: 'left', x: -3.999995328414287, y: 73.59372582307907, width: 16, height: 16 },
{
id: 'bottom',
position: 'bottom',
x: 138.99990008559496,
y: 100.15621170711313,
width: 16,
height: 16
}
]
},
computedPosition: { x: -45, y: 60, z: 0 },
selected: false,
dragging: false,
resizing: false,
initialized: true,
data: {},
events: {},
id: 'simple-textfuxp9gjvnc78ru0n1tkhul',
position: { x: -45, y: 60 },
label: 'simple-text node'
}
])
////////////////////////////////////////////.
// Removing data from the message store if delete button used
const onChange = (event: any) => {
event.forEach((element: any) => {
if (element.type == 'remove') {
store.layers.messages = store.layers.messages.filter((item: any) => {
return item.id != element.id
})
}
})
}
////////////////////////////////////////////.
</script>
<template>
<!-- {{ store }} -->
<div class="d-flex border" style="height: 100vh">
<GlobalMenu></GlobalMenu>
<div
class="m-1 border"
id="vue_flow"
oncontextmenu="return false;"
style="position: relative; width: 100%; height: 98%"
>
<VueFlow
v-model="elements"
class="customnodeflow"
:snap-to-grid="true"
:select-nodes-on-drag="true"
:only-render-visible-elements="true"
:max-zoom="50"
:min-zoom="0.05"
@dragover="onDragOver"
@drop="onDrop"
@nodeDoubleClick="onClick"
@nodesChange="onChange"
>
<Background pattern-color="#999" :gap="16" :size="1.2" />
<!-- Custom Edge from example -->
<template #edge-custom="props">
<CustomEdgeVue v-bind="props" />
</template>
<template #node-simple-text="props">
<SimpleTextVue :id="props.id" :selected="props.selected" />
</template>
<!-- End of importing Custom templates -->
<Controls />
<MiniMap v-show="messageToEdit === ''" />
</VueFlow>
</div>
</div>
</template>
<style>
.d-flex {
display: flex;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s 0.1s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
html,
body,
#app {
margin: 0;
height: 100%;
}
#app {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
/* Vueflow additional components style */
.vue-flow__minimap {
background-color: #2c3e50a6;
transform: scale(75%);
transform-origin: bottom right;
}
.customnodeflow button {
padding: 5px;
width: 25px;
height: 25px;
border-radius: 25px;
box-shadow: 0 5px 10px #0000004d;
cursor: pointer;
}
.customnodeflow button:hover {
opacity: 0.9;
transform: scale(105%);
transition: 0.25s all ease;
}
/* VueFlow Specifics */
.vue-flow {
background-color: #f2f5f7;
}
.vue-flow__edges {
z-index: 9999 !important;
}
/* Customize Handle */
.handle {
cursor: pointer !important;
}
/* Class used to select Control and Control Button */
.vue-flow__controls {
background-color: white;
padding: 0.15rem;
border-radius: 1rem;
}
.vue-flow__controls-button {
margin: 0.15rem;
border: 1px grey solid;
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论