mirror of
https://gitee.com/cai_xiao_feng/lowflow-design.git
synced 2025-12-07 00:28:23 +08:00
217 lines
5.1 KiB
Vue
217 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import type { ErrorInfo, FlowNode, NodeType } from './type'
|
|
import { ClickOutside as vClickOutside, type InputInstance } from 'element-plus'
|
|
import Add from './Add.vue'
|
|
import type { Ref } from 'vue'
|
|
|
|
const _inject = inject<{
|
|
readOnly?: Ref<boolean>
|
|
nodesError: Ref<Recordable<ErrorInfo[]>>
|
|
}>('flowDesign', { readOnly: ref(false), nodesError: ref({}) })
|
|
const $emits = defineEmits<{
|
|
(e: 'addNode', type: NodeType, node: FlowNode): void
|
|
(e: 'delNode', node: FlowNode): void
|
|
(e: 'activeNode', node: FlowNode): void
|
|
}>()
|
|
const $props = withDefaults(
|
|
defineProps<{
|
|
icon?: string
|
|
node: FlowNode
|
|
color?: string
|
|
readOnly?: boolean
|
|
close?: boolean
|
|
}>(),
|
|
{
|
|
readOnly: false,
|
|
close: true
|
|
}
|
|
)
|
|
const errorInfo = computed<ErrorInfo[] | undefined>(() => _inject.nodesError.value[$props.node.id])
|
|
const _readOnly = computed(() => _inject.readOnly?.value || $props.readOnly)
|
|
const showInput = ref(false)
|
|
const inputRef = ref<InputInstance>()
|
|
const onShowInput = () => {
|
|
if (_readOnly.value) return
|
|
showInput.value = true
|
|
nextTick(() => {
|
|
inputRef.value?.focus()
|
|
})
|
|
}
|
|
const onClickOutside = () => {
|
|
if (showInput.value) {
|
|
showInput.value = false
|
|
}
|
|
}
|
|
const activeNode = () => {
|
|
if (_readOnly.value) return
|
|
$emits('activeNode', $props.node)
|
|
}
|
|
const addNode = (type: NodeType) => {
|
|
$emits('addNode', type, $props.node)
|
|
}
|
|
const delNode = () => {
|
|
$emits('delNode', $props.node)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="node-box">
|
|
<el-card
|
|
@click="activeNode"
|
|
:class="['node', { 'error-node': errorInfo?.length && !_readOnly }]"
|
|
>
|
|
<!--头部-->
|
|
<template #header>
|
|
<div class="head">
|
|
<div @click.stop v-if="showInput">
|
|
<el-input
|
|
ref="inputRef"
|
|
v-click-outside="onClickOutside"
|
|
@blur="onClickOutside"
|
|
maxlength="30"
|
|
v-model="node.name"
|
|
/>
|
|
</div>
|
|
<el-text tag="b" truncated v-else @click.stop="onShowInput">
|
|
{{ node.name }}
|
|
<el-icon>
|
|
<EditPen />
|
|
</el-icon>
|
|
</el-text>
|
|
<slot name="icon">
|
|
<svg-icon :size="30" color="node-icon" v-if="icon" :name="icon" />
|
|
</slot>
|
|
</div>
|
|
<!--删除按钮-->
|
|
<span @click.stop>
|
|
<el-popconfirm
|
|
title="您确定要删除该节点吗?"
|
|
width="200"
|
|
:hide-after="0"
|
|
placement="right-start"
|
|
@confirm="delNode"
|
|
>
|
|
<template #reference>
|
|
<el-button
|
|
class="node-close"
|
|
v-show="close && !_readOnly"
|
|
plain
|
|
circle
|
|
icon="CircleClose"
|
|
size="small"
|
|
type="danger"
|
|
/>
|
|
</template>
|
|
</el-popconfirm>
|
|
</span>
|
|
<!--错误提示-->
|
|
<el-tooltip placement="top-start">
|
|
<template #content>
|
|
<div v-for="err in errorInfo" :key="err.id">
|
|
{{ err.message }}
|
|
</div>
|
|
</template>
|
|
<el-icon class="warn-icon" :size="20" v-show="errorInfo?.length && !_readOnly">
|
|
<WarnTriangleFilled @click.stop />
|
|
</el-icon>
|
|
</el-tooltip>
|
|
</template>
|
|
<!--插槽内容-->
|
|
<slot></slot>
|
|
</el-card>
|
|
<Add @add-node="addNode" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.node-box {
|
|
position: relative;
|
|
|
|
/* &:before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
margin: auto;
|
|
width: 1px;
|
|
height: 100%;
|
|
background-color: var(--el-border-color);
|
|
}*/
|
|
|
|
&:after {
|
|
content: '';
|
|
position: absolute;
|
|
top: -12px;
|
|
left: 50%;
|
|
transform: translate(-50%);
|
|
border-style: solid;
|
|
width: 0;
|
|
border-width: 8px 6px 4px;
|
|
border-color: var(--el-border-color) transparent transparent;
|
|
background-color: var(--flow-bg-color);
|
|
}
|
|
|
|
.warn-icon {
|
|
cursor: pointer;
|
|
position: absolute;
|
|
right: -30px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--el-color-error);
|
|
}
|
|
|
|
.error-node {
|
|
box-shadow: 0 0 5px 0 var(--el-color-error-light-3);
|
|
}
|
|
|
|
.node {
|
|
border-radius: 7px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
overflow: visible;
|
|
min-height: 90px;
|
|
width: 230px;
|
|
z-index: 10;
|
|
|
|
.node-close {
|
|
position: absolute;
|
|
top: -10px;
|
|
right: -10px;
|
|
display: none;
|
|
}
|
|
|
|
&:hover {
|
|
&:not(.error-node) {
|
|
box-shadow: 0 0 5px 0 var(--el-color-primary);
|
|
}
|
|
|
|
.node-close {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
:deep(.el-card__header) {
|
|
padding: calc(var(--el-card-padding) - 18px) calc(var(--el-card-padding) - 13px);
|
|
border-radius: 7px 7px 0 0;
|
|
background: v-bind(color);
|
|
|
|
.el-input__wrapper {
|
|
background-color: var(--el-card-bg-color);
|
|
}
|
|
}
|
|
|
|
:deep(.el-card__body) {
|
|
position: relative;
|
|
}
|
|
|
|
.head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
}
|
|
</style>
|