<template>
<div class="logicflow-page">
<div class="sidebar">
<div class="palette-title">组件面板</div>
<div class="palette-item" @mousedown="startDrag('custom-rect', '矩形')">矩形</div>
<div class="palette-item" @mousedown="startDrag('circle', '圆形')">圆形</div>
<div class="palette-item" @mousedown="startDrag('diamond', '菱形')">菱形</div>
<div class="palette-item" @mousedown="startDrag('text', '文本')">文本</div>
<div class="palette-title" style="margin-top: 20px;">关联关系</div>
<div class="edge-type-selector">
<div
class="edge-type-item"
:class="{ active: selectedEdgeType === 'solid' }"
@click="selectEdgeType('solid')"
>
<div class="edge-preview solid-line"></div>
<span>包含关系</span>
</div>
<div
class="edge-type-item"
:class="{ active: selectedEdgeType === 'dashed' }"
@click="selectEdgeType('dashed')"
>
<div class="edge-preview dashed-line"></div>
<span>关联关系</span>
</div>
<div
class="edge-type-item"
:class="{ active: selectedEdgeType === 'dotted' }"
@click="selectEdgeType('dotted')"
>
<div class="edge-preview dotted-line"></div>
<span>使用关系</span>
</div>
</div>
<div class="palette-title" style="margin-top: 20px;">操作说明</div>
<div class="help-text">
<p>• 点击节点/连接线:编辑文字</p>
<p>• 右键节点/连接线:删除</p>
<p>• Delete/Backspace:删除选中元素</p>
<p>• Ctrl+A:全选</p>
<p>• Ctrl+C:复制</p>
<p>• Ctrl+V:粘贴</p>
</div>
</div>
<div class="canvas-wrap">
<div ref="lfContainerRef" class="lf-container"></div>
</div>
<el-dialog v-model="nodeDialogVisible" title="节点配置" width="600px" append-to-body :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="节点ID">
<el-input v-model="selectedNodeId" disabled />
</el-form-item>
<el-form-item label="显示文本">
<el-input v-model="selectedNodeText" placeholder="请输入节点文本" />
</el-form-item>
<el-form-item label="节点类型">
<el-input v-model="selectedNodeType" placeholder="请输入节点类型" />
</el-form-item>
<el-form-item label="节点描述">
<el-input
v-model="selectedNodeDescription"
type="textarea"
:rows="2"
placeholder="请输入节点描述"
/>
</el-form-item>
<!-- 关联关系配置 -->
<el-divider content-position="left">关联关系配置</el-divider>
<el-form-item label="关联节点">
<div class="relation-config">
<div class="relation-list">
<div
v-for="(relation, index) in selectedNodeRelations"
:key="index"
class="relation-item"
>
<el-select
v-model="relation.targetNodeId"
placeholder="选择关联节点"
style="width: 150px; margin-right: 10px;"
>
<el-option
v-for="node in getAvailableNodes(selectedNodeId)"
:key="node.id"
:label="node.text?.value || node.id"
:value="node.id"
/>
</el-select>
<el-select
v-model="relation.relationType"
placeholder="关系类型"
style="width: 120px; margin-right: 10px;"
>
<el-option label="包含" value="contains" />
<el-option label="关联" value="associates" />
<el-option label="使用" value="uses" />
</el-select>
<el-button
type="danger"
icon="Delete"
size="small"
@click="removeRelation(index)"
/>
</div>
</div>
<el-button
type="primary"
icon="Plus"
size="small"
@click="addRelation"
style="margin-top: 10px;"
>
添加关联
</el-button>
</div>
</el-form-item>
<!-- 现有关联关系显示 -->
<el-form-item label="现有关联" v-if="getNodeRelationsSummary(selectedNodeId).length > 0">
<div class="existing-relations">
<el-tag
v-for="(summary, index) in getNodeRelationsSummary(selectedNodeId)"
:key="index"
type="info"
size="small"
style="margin-right: 8px; margin-bottom: 4px;"
>
{{ summary }}
</el-tag>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="nodeDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="applyNodeChange">保 存</el-button>
<el-button type="info" @click="viewNodeRelations">查看关系图</el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="edgeDialogVisible" title="连接线配置" width="500px" append-to-body :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="连接线ID">
<el-input v-model="selectedEdgeId" disabled />
</el-form-item>
<el-form-item label="显示文本">
<el-input v-model="selectedEdgeText" placeholder="请输入连接线文本" />
</el-form-item>
<el-form-item label="线条类型">
<el-select v-model="selectedEdgeLineType" placeholder="选择线条类型">
<el-option label="实线" value="solid" />
<el-option label="虚线" value="dashed" />
<el-option label="点线" value="dotted" />
</el-select>
</el-form-item>
<el-form-item label="文字颜色">
<div class="color-picker-container">
<el-color-picker
v-model="selectedEdgeTextColor"
:predefine="predefinedColors"
show-alpha
/>
<span class="color-preview" :style="{ color: selectedEdgeTextColor }">
预览文字效果
</span>
</div>
</el-form-item>
<el-form-item label="文字大小">
<el-slider
v-model="selectedEdgeTextSize"
:min="10"
:max="24"
:step="1"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="箭头样式">
<el-select v-model="selectedArrowType" placeholder="选择箭头样式">
<el-option label="默认箭头" value="default" />
<el-option label="实心箭头" value="filled" />
<el-option label="空心箭头" value="hollow" />
<el-option label="菱形箭头" value="diamond" />
<el-option label="圆形箭头" value="circle" />
</el-select>
</el-form-item>
<el-form-item label="起始点标记">
<el-select v-model="selectedStartMarker" placeholder="选择起始点标记">
<el-option label="无标记" value="none" />
<el-option label="实心圆" value="filled-circle" />
<el-option label="空心圆" value="hollow-circle" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="edgeDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="applyEdgeChange">保 存</el-button>
</div>
</template>
</el-dialog>
<!-- 关系图查看对话框 -->
<el-dialog v-model="relationViewDialogVisible" title="节点关系图" width="800px" append-to-body>
<div class="relation-view">
<div class="relation-header">
<h4>节点:{{ selectedNodeText }} ({{ selectedNodeId }})</h4>
</div>
<!-- 关系树状图 -->
<div class="relation-tree">
<el-tree
:data="relationTreeData"
:props="{ children: 'children', label: 'label' }"
default-expand-all
node-key="id"
class="relation-tree-view"
>
<template #default="{ node, data }">
<span class="relation-node">
<el-icon v-if="data.type === 'node'"><Box /></el-icon>
<el-icon v-else-if="data.type === 'relation'"><Connection /></el-icon>
<span>{{ data.label }}</span>
<el-tag v-if="data.relationType" size="small" type="primary">
{{ getRelationTypeLabel(data.relationType) }}
</el-tag>
</span>
</template>
</el-tree>
</div>
<!-- 关系表格 -->
<el-divider>关系详情</el-divider>
<el-table :data="relationTableData" style="width: 100%" size="small">
<el-table-column prop="sourceNode" label="源节点" width="150" />
<el-table-column prop="relationType" label="关系类型" width="120">
<template #default="scope">
<el-tag size="small" :type="getRelationTagType(scope.row.relationType)">
{{ getRelationTypeLabel(scope.row.relationType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="targetNode" label="目标节点" width="150" />
<el-table-column prop="description" label="描述" />
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="highlightRelation(scope.row)"
>
高亮
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="relationViewDialogVisible = false">关 闭</el-button>
<el-button type="primary" @click="exportRelations">导出关系</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import { ElMessage } from 'element-plus'
import LogicFlow from '@logicflow/core'
import { Menu, SelectionSelect, Control, MiniMap, Snapshot } from '@logicflow/extension'
import { RectNode, RectNodeModel, LineEdge, LineEdgeModel, h } from '@logicflow/core'
import '@logicflow/core/dist/index.css'
import '@logicflow/extension/dist/index.css'
const lfContainerRef = ref(null)
let lf = null
const nodeDialogVisible = ref(false)
const selectedNodeId = ref('')
const selectedNodeText = ref('')
const selectedNodeType = ref('')
const selectedNodeDescription = ref('')
const selectedNodeRelations = ref([])
// 关系图查看对话框
const relationViewDialogVisible = ref(false)
const relationTreeData = ref([])
const relationTableData = ref([])
// 全局关联关系存储
const nodeRelations = ref(new Map())
const edgeDialogVisible = ref(false)
const selectedEdgeId = ref('')
const selectedEdgeText = ref('')
const selectedEdgeType = ref('solid') // 当前选中的连线类型
const selectedEdgeLineType = ref('solid') // 编辑对话框中的线条类型
const selectedEdgeTextColor = ref('#374151') // 编辑对话框中的文字颜色
const selectedEdgeTextSize = ref(12) // 编辑对话框中的文字大小
const selectedArrowType = ref('default') // 编辑对话框中的箭头类型
const selectedStartMarker = ref('none') // 编辑对话框中的起始点标记
// 预定义颜色选项
const predefinedColors = [
'#374151', '#ef4444', '#f59e0b', '#10b981',
'#3b82f6', '#8b5cf6', '#ec4899', '#6b7280'
]
// 自定义矩形节点
class CustomRectNode extends RectNode {}
class CustomRectNodeModel extends RectNodeModel {
constructor(data, graphModel) {
super(data, graphModel)
// 设置默认尺寸
this.width = 180
this.height = 40
}
getNodeStyle() {
const style = super.getNodeStyle()
return {
...style,
fill: '#f0f9ff', // 自定义填充色
stroke: '#3b82f6', // 自定义边框色
strokeWidth: 2, // 自定义边框宽度
borderRadius: 8, // 自定义圆角
}
}
// 重写获取节点尺寸的方法
getWidth() {
return this.width
}
getHeight() {
return this.height
}
getTextStyle() {
const style = super.getTextStyle()
return {
...style,
fontSize: 14,
fontWeight: 'bold',
fill: '#1e40af',
}
}
}
// 备用方案:如果自定义getText不行,就用这个简单的类
class SimpleCustomEdge extends LineEdge {
// 什么都不重写,让LogicFlow完全处理
}
// 完全重写的自定义连接线 - 支持起始点标记和文字居中
class CustomEdge extends LineEdge {
getShape() {
const { model } = this.props
const { startMarker } = model.properties || {}
// 获取默认的连线形状
const shape = super.getShape()
// 如果有起始点标记,添加起始点圆形
if (startMarker && startMarker !== 'none') {
const startPoint = this.getStartPoint()
const lineType = model.properties?.lineType || 'solid'
const colors = {
solid: '#3b82f6',
dashed: '#10b981',
dotted: '#f59e0b'
}
const circle = h('circle', {
cx: startPoint.x,
cy: startPoint.y,
r: 4,
fill: startMarker === 'filled-circle' ? colors[lineType] : 'white',
stroke: colors[lineType],
strokeWidth: 2,
className: 'lf-edge-start-marker'
})
// 将起始点圆形添加到形状中
if (shape.children) {
shape.children.push(circle)
} else {
return h('g', {}, [shape, circle])
}
}
return shape
}
getStartPoint() {
const { model } = this.props
// 尝试多种方式获取起始点
if (model.startPoint) {
return model.startPoint
} else if (model.x1 !== undefined) {
return { x: model.x1, y: model.y1 }
} else {
const sourceNode = model.graphModel.getNodeModelById(model.sourceNodeId)
return sourceNode ? { x: sourceNode.x, y: sourceNode.y } : { x: 0, y: 0 }
}
}
getText() {
const { model } = this.props
const text = model.text?.value || ''
if (!text.trim()) return null
// 调试:输出模型的所有属性
console.log('模型属性:', model)
console.log('startPoint:', model.startPoint)
console.log('endPoint:', model.endPoint)
console.log('x1,y1,x2,y2:', model.x1, model.y1, model.x2, model.y2)
// 尝试多种方式获取连线坐标
let startX, startY, endX, endY
// 方法1: 直接从model获取
if (model.x1 !== undefined && model.x2 !== undefined) {
startX = model.x1
startY = model.y1
endX = model.x2
endY = model.y2
}
// 方法2: 从startPoint和endPoint获取
else if (model.startPoint && model.endPoint) {
startX = model.startPoint.x
startY = model.startPoint.y
endX = model.endPoint.x
endY = model.endPoint.y
}
// 方法3: 从源节点和目标节点获取
else {
const sourceNode = model.graphModel.getNodeModelById(model.sourceNodeId)
const targetNode = model.graphModel.getNodeModelById(model.targetNodeId)
if (sourceNode && targetNode) {
startX = sourceNode.x
startY = sourceNode.y
endX = targetNode.x
endY = targetNode.y
} else {
console.error('无法获取连线坐标')
return null
}
}
// 计算中心点
const centerX = (startX + endX) / 2
const centerY = (startY + endY) / 2
// 获取文字样式
const textColor = model.properties?.textColor || '#374151'
const textSize = model.properties?.textSize || 12
console.log(`连线坐标: (${startX},${startY}) -> (${endX},${endY})`)
console.log(`文字渲染: "${text}" 中心位置: (${centerX}, ${centerY})`)
// 直接使用绝对坐标,不使用transform
return h('text', {
x: centerX,
y: centerY,
textAnchor: 'middle',
dominantBaseline: 'central',
fontSize: textSize,
fill: textColor,
fontFamily: 'Arial, sans-serif',
fontWeight: '500'
}, text)
}
}
// 基础自定义边模型
class CustomEdgeModel extends LineEdgeModel {
getEdgeStyle() {
const style = super.getEdgeStyle()
const lineType = this.properties?.lineType || 'solid'
// 根据线条类型设置不同样式
const lineStyles = {
solid: {
stroke: '#3b82f6',
strokeWidth: 2,
strokeDasharray: '0',
},
dashed: {
stroke: '#10b981',
strokeWidth: 2,
strokeDasharray: '8,4',
},
dotted: {
stroke: '#f59e0b',
strokeWidth: 2,
strokeDasharray: '2,2',
}
}
return {
...style,
...lineStyles[lineType]
}
}
getArrowStyle() {
const style = super.getArrowStyle()
const lineType = this.properties?.lineType || 'solid'
const arrowType = this.properties?.arrowType || 'default'
const arrowColors = {
solid: '#3b82f6',
dashed: '#10b981',
dotted: '#f59e0b'
}
// 根据箭头类型设置不同的样式
const baseStyle = {
fill: arrowColors[lineType],
stroke: arrowColors[lineType],
strokeWidth: 1,
}
switch (arrowType) {
case 'filled': // 实心箭头
return {
...style,
...baseStyle,
fill: arrowColors[lineType],
stroke: arrowColors[lineType]
}
case 'hollow': // 空心箭头
return {
...style,
...baseStyle,
fill: 'white',
stroke: arrowColors[lineType],
strokeWidth: 2
}
case 'diamond': // 菱形箭头
return {
...style,
...baseStyle,
d: 'M 0 0 L 8 4 L 0 8 L -8 4 Z' // 菱形路径
}
case 'circle': // 圆形箭头
return {
...style,
...baseStyle,
r: 4 // 圆形半径
}
default: // 默认箭头
return {
...style,
...baseStyle
}
}
}
// 配置起始点标记
getStartArrowStyle() {
const startMarker = this.properties?.startMarker || 'none'
const lineType = this.properties?.lineType || 'solid'
const colors = {
solid: '#3b82f6',
dashed: '#10b981',
dotted: '#f59e0b'
}
if (startMarker === 'none') return null
return {
fill: startMarker === 'filled-circle' ? colors[lineType] : 'white',
stroke: colors[lineType],
strokeWidth: 2,
r: 4
}
}
// 配置终点标记
getEndArrowStyle() {
return this.getArrowStyle()
}
// 备用的文字样式方法,如果CustomEdge的getText有问题就用这个
getTextStyle() {
const style = super.getTextStyle()
const textColor = this.properties?.textColor || '#374151'
const textSize = this.properties?.textSize || 12
return {
...style,
fontSize: textSize,
fill: textColor,
fontWeight: '500',
fontFamily: 'Arial, sans-serif',
textAnchor: 'middle',
dominantBaseline: 'central',
background: {
fill: 'transparent',
stroke: 'transparent',
strokeWidth: 0
}
}
}
}
function startDrag(type, text) {
if (!lf) return
lf.dnd.startDrag({ type, text })
}
// 选择边类型
function selectEdgeType(edgeType) {
selectedEdgeType.value = edgeType
// 设置默认的边类型,影响后续创建的连线
if (lf) {
lf.setDefaultEdgeType('custom-edge')
}
}
// 根据线型获取对应的关联关系
function getRelationTypeByLineType(lineType) {
const mapping = {
'solid': 'contains', // 实线 -> 包含关系
'dashed': 'associates', // 虚线 -> 关联关系
'dotted': 'uses' // 点线 -> 使用关系
}
return mapping[lineType] || 'contains'
}
// 根据关联关系获取对应的线型
function getLineTypeByRelationType(relationType) {
const mapping = {
'contains': 'solid', // 包含关系 -> 实线
'associates': 'dashed', // 关联关系 -> 虚线
'uses': 'dotted' // 使用关系 -> 点线
}
return mapping[relationType] || 'solid'
}
// 将连线关系同步到源节点的关联关系配置
function syncEdgeToNodeRelation(sourceNodeId, targetNodeId, relationType) {
// 获取源节点现有的关联关系
const existingRelations = nodeRelations.value.get(sourceNodeId) || []
// 检查是否已存在相同的关系(避免重复)
const isDuplicate = existingRelations.some(relation =>
relation.targetNodeId === targetNodeId && relation.relationType === relationType
)
if (!isDuplicate) {
// 添加新的关联关系
const newRelation = {
targetNodeId: targetNodeId,
relationType: relationType,
customType: '',
description: `通过连线自动创建的${getRelationTypeLabel(relationType)}关系`
}
existingRelations.push(newRelation)
nodeRelations.value.set(sourceNodeId, existingRelations)
console.log(`已自动添加关联关系: ${sourceNodeId} -> ${targetNodeId} (${relationType})`)
}
}
// 从节点关联关系中移除指定的关系
function removeEdgeFromNodeRelation(sourceNodeId, targetNodeId, relationType) {
const existingRelations = nodeRelations.value.get(sourceNodeId) || []
const filteredRelations = existingRelations.filter(relation =>
!(relation.targetNodeId === targetNodeId && relation.relationType === relationType)
)
if (filteredRelations.length !== existingRelations.length) {
nodeRelations.value.set(sourceNodeId, filteredRelations)
console.log(`已移除关联关系: ${sourceNodeId} -> ${targetNodeId} (${relationType})`)
}
}
function applyNodeChange() {
if (!selectedNodeId.value) return
// 更新节点基本信息
lf.setProperties(selectedNodeId.value, {
text: selectedNodeText.value,
nodeType: selectedNodeType.value,
description: selectedNodeDescription.value
})
// 同步 label 文本
lf.updateText(selectedNodeId.value, selectedNodeText.value)
// 保存关联关系
saveNodeRelations(selectedNodeId.value, selectedNodeRelations.value)
nodeDialogVisible.value = false
ElMessage.success('已更新节点配置')
}
// 添加关联关系
function addRelation() {
selectedNodeRelations.value.push({
targetNodeId: '',
relationType: '',
customType: '',
description: ''
})
}
// 移除关联关系
function removeRelation(index) {
selectedNodeRelations.value.splice(index, 1)
}
// 保存节点关联关系
function saveNodeRelations(nodeId, relations) {
const validRelations = relations.filter(r => r.targetNodeId && r.relationType)
const oldRelations = nodeRelations.value.get(nodeId) || []
// 找出被删除的关系,删除对应的自动创建的连线
oldRelations.forEach((oldRelation, index) => {
const stillExists = validRelations.some(newRelation =>
newRelation.targetNodeId === oldRelation.targetNodeId &&
newRelation.relationType === oldRelation.relationType
)
if (!stillExists) {
// 删除对应的自动创建的连线
const edgeId = `relation_${nodeId}_${oldRelation.targetNodeId}_${index}`
try {
const edgeModel = lf.getEdgeModelById(edgeId)
if (edgeModel) {
lf.deleteEdge(edgeId)
console.log(`已删除关联关系对应的连线: ${edgeId}`)
}
} catch (error) {
console.warn('删除关联连线失败:', error)
}
}
})
nodeRelations.value.set(nodeId, validRelations)
// 创建自动连线(可选)
createRelationEdges(nodeId, validRelations)
}
// 创建关系连线
function createRelationEdges(sourceNodeId, relations) {
relations.forEach((relation, index) => {
if (!relation.targetNodeId || !relation.relationType) return
const edgeId = `relation_${sourceNodeId}_${relation.targetNodeId}_${index}`
const relationTypeLabel = getRelationTypeLabel(relation.relationType)
// 检查是否已存在连线
const existingEdge = lf.getEdgeModelById(edgeId)
if (existingEdge) {
// 更新现有连线
lf.setProperties(edgeId, {
relationType: relation.relationType,
relationLabel: relationTypeLabel
})
lf.updateText(edgeId, relationTypeLabel)
} else {
// 创建新连线
try {
lf.addEdge({
id: edgeId,
sourceNodeId: sourceNodeId,
targetNodeId: relation.targetNodeId,
text: relationTypeLabel,
type: 'custom-edge',
properties: {
lineType: getRelationLineType(relation.relationType),
relationType: relation.relationType,
relationLabel: relationTypeLabel,
textColor: getRelationColor(relation.relationType),
textSize: 12,
arrowType: 'filled'
}
})
} catch (error) {
console.warn('创建关系连线失败:', error)
}
}
})
}
// 获取可用节点列表
function getAvailableNodes(currentNodeId) {
if (!lf) return []
const graphData = lf.getGraphData()
return graphData.nodes.filter(node => node.id !== currentNodeId)
}
// 获取节点关系摘要
function getNodeRelationsSummary(nodeId) {
const relations = nodeRelations.value.get(nodeId) || []
const summaries = []
relations.forEach(relation => {
if (relation.targetNodeId && relation.relationType) {
const targetNode = getNodeById(relation.targetNodeId)
const targetName = targetNode?.text?.value || relation.targetNodeId
const relationLabel = getRelationTypeLabel(relation.relationType)
summaries.push(`${relationLabel} → ${targetName}`)
}
})
// 查找指向当前节点的关系
nodeRelations.value.forEach((relations, sourceNodeId) => {
relations.forEach(relation => {
if (relation.targetNodeId === nodeId) {
const sourceNode = getNodeById(sourceNodeId)
const sourceName = sourceNode?.text?.value || sourceNodeId
const relationLabel = getRelationTypeLabel(relation.relationType)
summaries.push(`${sourceName} → ${relationLabel}`)
}
})
})
return summaries
}
// 查看节点关系
function viewNodeRelations() {
buildRelationTreeData(selectedNodeId.value)
buildRelationTableData(selectedNodeId.value)
relationViewDialogVisible.value = true
}
// 构建关系树数据
function buildRelationTreeData(nodeId) {
const currentNode = getNodeById(nodeId)
const currentNodeName = currentNode?.text?.value || nodeId
const treeNode = {
id: nodeId,
label: currentNodeName,
type: 'node',
children: []
}
// 添加出去的关系
const outgoingRelations = nodeRelations.value.get(nodeId) || []
outgoingRelations.forEach((relation, index) => {
if (relation.targetNodeId && relation.relationType) {
const targetNode = getNodeById(relation.targetNodeId)
const targetName = targetNode?.text?.value || relation.targetNodeId
const relationLabel = getRelationTypeLabel(relation.relationType)
treeNode.children.push({
id: `${nodeId}_out_${index}`,
label: `${relationLabel} → ${targetName}`,
type: 'relation',
relationType: relation.relationType
})
}
})
// 添加进来的关系
nodeRelations.value.forEach((relations, sourceNodeId) => {
relations.forEach((relation, index) => {
if (relation.targetNodeId === nodeId) {
const sourceNode = getNodeById(sourceNodeId)
const sourceName = sourceNode?.text?.value || sourceNodeId
const relationLabel = getRelationTypeLabel(relation.relationType)
treeNode.children.push({
id: `${sourceNodeId}_in_${index}`,
label: `${sourceName} → ${relationLabel}`,
type: 'relation',
relationType: relation.relationType
})
}
})
})
relationTreeData.value = [treeNode]
}
// 构建关系表格数据
function buildRelationTableData(nodeId) {
const tableData = []
// 出去的关系
const outgoingRelations = nodeRelations.value.get(nodeId) || []
outgoingRelations.forEach(relation => {
if (relation.targetNodeId && relation.relationType) {
const targetNode = getNodeById(relation.targetNodeId)
const targetName = targetNode?.text?.value || relation.targetNodeId
const currentNode = getNodeById(nodeId)
const currentName = currentNode?.text?.value || nodeId
tableData.push({
sourceNode: currentName,
relationType: relation.relationType,
targetNode: targetName,
description: relation.description || `${currentName} ${getRelationTypeLabel(relation.relationType)} ${targetName}`,
direction: 'outgoing',
sourceNodeId: nodeId,
targetNodeId: relation.targetNodeId
})
}
})
// 进来的关系
nodeRelations.value.forEach((relations, sourceNodeId) => {
relations.forEach(relation => {
if (relation.targetNodeId === nodeId) {
const sourceNode = getNodeById(sourceNodeId)
const sourceName = sourceNode?.text?.value || sourceNodeId
const currentNode = getNodeById(nodeId)
const currentName = currentNode?.text?.value || nodeId
tableData.push({
sourceNode: sourceName,
relationType: relation.relationType,
targetNode: currentName,
description: relation.description || `${sourceName} ${getRelationTypeLabel(relation.relationType)} ${currentName}`,
direction: 'incoming',
sourceNodeId: sourceNodeId,
targetNodeId: nodeId
})
}
})
})
relationTableData.value = tableData
}
// 获取节点by ID
function getNodeById(nodeId) {
if (!lf) return null
const graphData = lf.getGraphData()
return graphData.nodes.find(node => node.id === nodeId)
}
// 获取关系类型标签
function getRelationTypeLabel(relationType) {
const labels = {
'contains': '包含',
'associates': '关联',
'uses': '使用'
}
return labels[relationType] || relationType
}
// 获取关系标签类型
function getRelationTagType(relationType) {
const types = {
'contains': 'success',
'associates': 'primary',
'uses': 'warning'
}
return types[relationType] || 'info'
}
// 获取关系线条类型
function getRelationLineType(relationType) {
const lineTypes = {
'contains': 'solid', // 包含关系用实线
'associates': 'dashed', // 关联关系用虚线
'uses': 'dotted' // 使用关系用点线
}
return lineTypes[relationType] || 'solid'
}
// 获取关系颜色
function getRelationColor(relationType) {
const colors = {
'contains': '#3b82f6', // 包含关系用蓝色(对应实线)
'associates': '#10b981', // 关联关系用绿色(对应虚线)
'uses': '#f59e0b' // 使用关系用橙色(对应点线)
}
return colors[relationType] || '#374151'
}
// 高亮关系
function highlightRelation(relationData) {
// 高亮相关节点
lf.selectElementById(relationData.sourceNodeId, true)
lf.selectElementById(relationData.targetNodeId, true)
ElMessage.success(`已高亮关系:${relationData.sourceNode} → ${relationData.targetNode}`)
}
// 导出关系
function exportRelations() {
const allRelations = []
nodeRelations.value.forEach((relations, sourceNodeId) => {
const sourceNode = getNodeById(sourceNodeId)
const sourceName = sourceNode?.text?.value || sourceNodeId
relations.forEach(relation => {
if (relation.targetNodeId && relation.relationType) {
const targetNode = getNodeById(relation.targetNodeId)
const targetName = targetNode?.text?.value || relation.targetNodeId
allRelations.push({
源节点: sourceName,
源节点ID: sourceNodeId,
关系类型: getRelationTypeLabel(relation.relationType),
目标节点: targetName,
目标节点ID: relation.targetNodeId,
自定义类型: relation.customType || '',
描述: relation.description || ''
})
}
})
})
// 导出为JSON
const dataStr = JSON.stringify(allRelations, null, 2)
const dataBlob = new Blob([dataStr], {type: 'application/json'})
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = 'node-relations.json'
link.click()
ElMessage.success('关系数据已导出')
}
function applyEdgeChange() {
if (!selectedEdgeId.value) return
// 获取连线数据
const edgeModel = lf.getEdgeModelById(selectedEdgeId.value)
if (!edgeModel) return
const oldRelationType = edgeModel.properties?.relationType
const newRelationType = getRelationTypeByLineType(selectedEdgeLineType.value)
// 如果关联关系类型发生变化,需要同步更新节点关联关系
if (oldRelationType && oldRelationType !== newRelationType) {
// 先移除旧的关联关系
removeEdgeFromNodeRelation(edgeModel.sourceNodeId, edgeModel.targetNodeId, oldRelationType)
// 再添加新的关联关系
syncEdgeToNodeRelation(edgeModel.sourceNodeId, edgeModel.targetNodeId, newRelationType)
}
const relationLabel = getRelationTypeLabel(newRelationType)
lf.setProperties(selectedEdgeId.value, {
text: selectedEdgeText.value || relationLabel, // 如果没有自定义文本,使用关系标签
lineType: selectedEdgeLineType.value,
relationType: newRelationType,
relationLabel: relationLabel,
textColor: selectedEdgeTextColor.value,
textSize: selectedEdgeTextSize.value,
arrowType: selectedArrowType.value,
startMarker: selectedStartMarker.value
})
// 同步连接线文本
const displayText = selectedEdgeText.value || relationLabel
lf.updateText(selectedEdgeId.value, displayText)
edgeDialogVisible.value = false
ElMessage.success('已更新连接线样式和关联关系')
}
onMounted(() => {
lf = new LogicFlow({
container: lfContainerRef.value,
grid: true,
keyboard: {
enabled: true
},
snapline: true,
edgeType: 'custom-edge', // 使用自定义连接线
plugins: [Menu, SelectionSelect, Control, MiniMap, Snapshot]
})
// 注册自定义矩形节点
lf.register({
type: 'custom-rect',
view: CustomRectNode,
model: CustomRectNodeModel,
})
// 注册自定义连接线 - 如果CustomEdge有问题,可以切换到SimpleCustomEdge
lf.register({
type: 'custom-edge',
view: CustomEdge, // 如果有问题,改为 SimpleCustomEdge
model: CustomEdgeModel,
})
console.log(lf, '__+++')
// 监听点击节点,打开编辑弹窗
lf.on('node:click', ({ data }) => {
selectedNodeId.value = data.id
selectedNodeText.value = data.text?.value || ''
selectedNodeType.value = data.properties?.nodeType || ''
selectedNodeDescription.value = data.properties?.description || ''
// 加载现有关联关系
const existingRelations = nodeRelations.value.get(data.id) || []
selectedNodeRelations.value = existingRelations.map(r => ({...r})) // 深拷贝
nodeDialogVisible.value = true
})
// 监听点击连接线,打开编辑弹窗
lf.on('edge:click', ({ data }) => {
selectedEdgeId.value = data.id
selectedEdgeText.value = data.text?.value || ''
selectedEdgeLineType.value = data.properties?.lineType || 'solid'
selectedEdgeTextColor.value = data.properties?.textColor || '#374151'
selectedEdgeTextSize.value = data.properties?.textSize || 12
selectedArrowType.value = data.properties?.arrowType || 'default'
selectedStartMarker.value = data.properties?.startMarker || 'none'
edgeDialogVisible.value = true
})
// 双击连接线添加文本
lf.on('edge:dbclick', ({ data }) => {
selectedEdgeId.value = data.id
selectedEdgeText.value = data.text?.value || ''
selectedEdgeLineType.value = data.properties?.lineType || 'solid'
selectedEdgeTextColor.value = data.properties?.textColor || '#374151'
selectedEdgeTextSize.value = data.properties?.textSize || 12
selectedArrowType.value = data.properties?.arrowType || 'default'
selectedStartMarker.value = data.properties?.startMarker || 'none'
edgeDialogVisible.value = true
})
// 监听边创建,自动设置当前选择的线条类型和关联关系
lf.on('edge:add', ({ data }) => {
const lineType = selectedEdgeType.value
const relationType = getRelationTypeByLineType(lineType)
const relationLabel = getRelationTypeLabel(relationType)
// 为新创建的边设置线条类型、关联关系和样式
lf.setProperties(data.id, {
lineType: lineType,
relationType: relationType,
relationLabel: relationLabel,
textColor: getRelationColor(relationType),
textSize: 12,
arrowType: 'filled'
})
// 设置连线文本为关联关系标签
lf.updateText(data.id, relationLabel)
// 自动同步到源节点的关联关系配置
syncEdgeToNodeRelation(data.sourceNodeId, data.targetNodeId, relationType)
})
// 监听边删除事件,同步删除节点关联关系
lf.on('edge:delete', ({ data }) => {
const properties = data.properties || {}
const relationType = properties.relationType
if (relationType && data.sourceNodeId && data.targetNodeId) {
removeEdgeFromNodeRelation(data.sourceNodeId, data.targetNodeId, relationType)
}
})
// 键盘删除事件
lf.on('keydown', ({ data }) => {
if (data.key === 'Delete' || data.key === 'Backspace') {
const selectedElements = lf.getSelectElements()
if (selectedElements.nodes.length > 0 || selectedElements.edges.length > 0) {
lf.deleteSelectElements()
ElMessage.success('已删除选中元素')
}
}
})
// 右键菜单删除
lf.on('edge:contextmenu', ({ data }) => {
lf.showContextMenu({
type: 'edge',
data: data,
callback: (type, data) => {
if (type === 'delete') {
lf.deleteEdge(data.id)
ElMessage.success('已删除连接线')
}
}
})
})
lf.on('node:contextmenu', ({ data }) => {
lf.showContextMenu({
type: 'node',
data: data,
callback: (type, data) => {
if (type === 'delete') {
lf.deleteNode(data.id)
ElMessage.success('已删除节点')
}
}
})
})
// 初始化一个示例
lf.render({
nodes: [
{ id: 'n1', type: 'custom-rect', x: 200, y: 80, text: '开始' },
{ id: 'n2', type: 'custom-rect', x: 420, y: 80, text: '处理' },
{ id: 'n3', type: 'custom-rect', x: 640, y: 80, text: '结束' },
{ id: 'n4', type: 'custom-rect', x: 420, y: 180, text: '工具' },
],
edges: [
{
id: 'e1',
sourceNodeId: 'n1',
targetNodeId: 'n2',
text: '包含',
properties: {
lineType: 'solid',
relationType: 'contains',
relationLabel: '包含',
textColor: '#3b82f6',
textSize: 14,
arrowType: 'filled',
startMarker: 'filled-circle'
}
},
{
id: 'e2',
sourceNodeId: 'n2',
targetNodeId: 'n3',
text: '关联',
properties: {
lineType: 'dashed',
relationType: 'associates',
relationLabel: '关联',
textColor: '#10b981',
textSize: 14,
arrowType: 'filled',
startMarker: 'hollow-circle'
}
},
{
id: 'e3',
sourceNodeId: 'n2',
targetNodeId: 'n4',
text: '使用',
properties: {
lineType: 'dotted',
relationType: 'uses',
relationLabel: '使用',
textColor: '#f59e0b',
textSize: 14,
arrowType: 'filled',
startMarker: 'none'
}
}
]
})
})
onBeforeUnmount(() => {
if (lf) {
lf.destroy()
lf = null
}
})
</script>
<style scoped>
.logicflow-page {
display: flex;
height: calc(100vh - 120px);
padding: 12px;
box-sizing: border-box;
}
.sidebar {
width: 200px;
border-right: 1px solid #eee;
padding-right: 12px;
}
.palette-title {
font-weight: 600;
margin-bottom: 8px;
}
.palette-item {
background: #f6f7fb;
border: 1px dashed #c0c4cc;
padding: 8px 10px;
margin-bottom: 8px;
cursor: grab;
border-radius: 4px;
user-select: none;
}
.help-text {
font-size: 12px;
color: #666;
line-height: 1.5;
}
.help-text p {
margin: 4px 0;
}
.canvas-wrap {
flex: 1;
padding-left: 12px;
}
.lf-container {
width: 100%;
height: 100%;
background: #fff;
border: 1px solid #eee;
border-radius: 4px;
}
/* 边类型选择器样式 */
.edge-type-selector {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.edge-type-item {
display: flex;
align-items: center;
padding: 8px 10px;
border: 1px solid #e5e7eb;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
background: #f9fafb;
}
.edge-type-item:hover {
border-color: #3b82f6;
background: #eff6ff;
}
.edge-type-item.active {
border-color: #3b82f6;
background: #dbeafe;
}
.edge-preview {
width: 30px;
height: 2px;
margin-right: 10px;
position: relative;
}
.solid-line {
background: #3b82f6;
}
.dashed-line {
background: linear-gradient(to right, #10b981 0%, #10b981 50%, transparent 50%, transparent 100%);
background-size: 8px 2px;
}
.dotted-line {
background: linear-gradient(to right, #f59e0b 0%, #f59e0b 25%, transparent 25%, transparent 50%, #f59e0b 50%, #f59e0b 75%, transparent 75%, transparent 100%);
background-size: 4px 2px;
}
/* 颜色选择器样式 */
.color-picker-container {
display: flex;
align-items: center;
gap: 12px;
}
.color-preview {
font-size: 14px;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
background: #f5f5f5;
}
/* LogicFlow 连线文字样式优化 */
:deep(.lf-edge-text) {
pointer-events: none;
}
/* 隐藏默认的文字背景矩形 */
:deep(.lf-edge-text rect),
:deep(.lf-edge-text-bg),
:deep(.lf-edge .lf-edge-text-bg) {
fill: transparent !important;
stroke: transparent !important;
opacity: 0 !important;
display: none !important;
}
/* 确保自定义文字样式 */
:deep(.lf-edge text) {
text-anchor: middle !important;
dominant-baseline: central !important;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* 增强文字可读性 */
:deep(.lf-edge text) {
filter: drop-shadow(1px 1px 1px rgba(255, 255, 255, 0.8))
drop-shadow(-1px -1px 1px rgba(255, 255, 255, 0.8));
}
/* 关联关系配置样式 */
.relation-config {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 12px;
background-color: #fafbfc;
}
.relation-list {
max-height: 200px;
overflow-y: auto;
}
.relation-item {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
background: white;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
.relation-item:last-child {
margin-bottom: 0;
}
.existing-relations {
max-height: 100px;
overflow-y: auto;
padding: 8px;
background-color: #f9fafb;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
/* 关系查看对话框样式 */
.relation-view {
padding: 8px;
}
.relation-header h4 {
margin: 0 0 16px 0;
color: #374151;
font-weight: 600;
}
.relation-tree {
margin-bottom: 20px;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 12px;
background-color: #fafbfc;
}
.relation-tree-view {
background: transparent;
}
.relation-node {
display: flex;
align-items: center;
gap: 8px;
}
.relation-node .el-icon {
color: #6b7280;
}
.relation-node .el-tag {
margin-left: 8px;
}
/* 关系连线的不同颜色样式 */
:deep(.lf-edge[data-relation-type="depends"]) .lf-edge-path {
stroke: #f59e0b;
}
:deep(.lf-edge[data-relation-type="contains"]) .lf-edge-path {
stroke: #10b981;
}
:deep(.lf-edge[data-relation-type="triggers"]) .lf-edge-path {
stroke: #ef4444;
}
:deep(.lf-edge[data-relation-type="inherits"]) .lf-edge-path {
stroke: #3b82f6;
}
:deep(.lf-edge[data-relation-type="references"]) .lf-edge-path {
stroke: #8b5cf6;
}
:deep(.lf-edge[data-relation-type="calls"]) .lf-edge-path {
stroke: #374151;
}
</style>