代码拉取完成,页面将自动刷新
document.addEventListener("DOMContentLoaded", () => {
// 初始化变量
const canvas = document.getElementById("workflow-canvas")
const nodeItems = document.querySelectorAll(".node-item")
const propertyPanel = document.querySelector(".property-panel")
const propertyForm = document.getElementById("property-form")
const noSelection = document.getElementById("no-selection")
const nodeNameInput = document.getElementById("node-name")
const codeEditor = document.getElementById("code-editor")
const codeEditorContainer = document.querySelector(".code-editor-container")
const tabButtons = document.querySelectorAll(".tab-btn")
const tabContents = document.querySelectorAll(".tab-content")
const exportBtn = document.getElementById("export-btn")
const runBtn = document.getElementById("run-btn")
const zoomInBtn = document.getElementById("zoom-in-btn")
const zoomOutBtn = document.getElementById("zoom-out-btn")
const resetZoomBtn = document.getElementById("reset-zoom-btn")
const minimapContainer = document.getElementById("minimap-container")
const minimapCanvas = document.getElementById("minimap-canvas")
const minimapViewport = document.getElementById("minimap-viewport")
const backgroundTypeSelect = document.getElementById("background-type")
const backgroundColorPicker = document.getElementById("background-color")
const gridSizeInput = document.getElementById("grid-size")
const gridColorPicker = document.getElementById("grid-color")
// 预览和保存相关元素
const previewBtn = document.getElementById("preview-btn")
// const saveBtn = document.getElementById("save-btn")
const previewModal = document.getElementById("preview-modal")
const saveModal = document.getElementById("save-modal")
const closePreviewBtn = document.getElementById("close-preview")
const closePreviewBtnFooter = document.getElementById("close-preview-btn")
const closeSaveBtn = document.getElementById("close-save")
const cancelSaveBtn = document.getElementById("cancel-save")
const confirmSaveBtn = document.getElementById("confirm-save")
const previewTabs = document.querySelectorAll(".preview-tab")
const previewTabContents = document.querySelectorAll(".preview-tab-content")
const previewCanvas = document.getElementById("preview-canvas")
const jsonContent = document.getElementById("json-content")
const nodeCountElement = document.getElementById("node-count")
const connectionCountElement = document.getElementById("connection-count")
const nodeListElement = document.getElementById("node-list")
const workflowNameInput = document.getElementById("workflow-name")
const workflowDescriptionInput = document.getElementById("workflow-description")
const workflowVersionInput = document.getElementById("workflow-version")
const workflowPermissionSelect = document.getElementById("workflow-permission")
// 节点数据
const nodes = []
let selectedNode = null
let selectedConnection = null
let nodeCounter = 0
let scale = 1 // 当前缩放比例
// 背景设置
const backgroundSettings = {
type: "grid",
color: "#ffffff",
gridSize: 20,
gridColor: "#e0e0e0",
}
// 初始化jsPlumb
const jsPlumbInstance = jsPlumb.getInstance({
Connector: ["Bezier", { curviness: 50 }],
Endpoint: ["Dot", { radius: 5 }],
HoverPaintStyle: { stroke: "#1e88e5", strokeWidth: 2 },
ConnectionOverlays: [
[
"Arrow",
{
location: 1,
width: 10,
length: 10,
id: "arrow",
},
],
],
Container: canvas,
})
// 设置连接线可选中
jsPlumbInstance.bind("click", (conn, originalEvent) => {
originalEvent.stopPropagation()
selectConnection(conn)
})
// 节点类型图标映射
const nodeTypeIcons = {
start: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>`,
input: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"></path></svg>`,
transform: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>`,
code: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>`,
condition: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>`,
loop: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 1l4 4-4 4"></path><path d="M3 11V9a4 4 0 014-4h14"></path><path d="M7 23l-4-4 4-4"></path><path d="M21 13v2a4 4 0 01-4 4H3"></path></svg>`,
database: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>`,
api: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>`,
output: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>`,
timer: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
notification: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 01-3.46 0"></path></svg>`,
function: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
}
// 节点类型背景色映射
const nodeTypeColors = {
start: "#e8f0fe",
input: "#e6f7ff",
transform: "#e6ffed",
code: "#fffbe6",
condition: "#fff2e8",
loop: "#f9f0ff",
database: "#f0f5ff",
api: "#e6fffb",
output: "#f0f2f5",
timer: "#fff0f6",
notification: "#fcf4dc",
function: "#f9f0ff",
}
// 节点类型文本颜色映射
const nodeTypeTextColors = {
start: "#4285f4",
input: "#1890ff",
transform: "#52c41a",
code: "#faad14",
condition: "#fa541c",
loop: "#722ed1",
database: "#2f54eb",
api: "#13c2c2",
output: "#595959",
timer: "#eb2f96",
notification: "#d48806",
function: "#722ed1",
}
// 获取节点名称
function getNodeName(type) {
const nameMap = {
start: "开始",
input: "输入",
transform: "文本处理",
code: "代码",
condition: "条件",
loop: "循环",
database: "数据库",
api: "API",
output: "输出",
timer: "定时器",
notification: "通知",
function: "函数",
}
return nameMap[type] || "节点"
}
// 修改缩略图相关函数
function updateMinimap() {
const minimapCtx = minimapCanvas.getContext("2d")
// 清除缩略图
minimapCtx.clearRect(0, 0, minimapCanvas.width, minimapCanvas.height)
// 设置缩略图背景
minimapCtx.fillStyle = backgroundSettings.color
minimapCtx.fillRect(0, 0, minimapCanvas.width, minimapCanvas.height)
// 如果是网格背景,绘制网格
if (backgroundSettings.type === "grid") {
minimapCtx.strokeStyle = "rgba(200, 200, 200, 0.3)"
minimapCtx.lineWidth = 0.5
const gridSize = 5 // 缩略图中的网格大小
for (let x = 0; x <= minimapCanvas.width; x += gridSize) {
minimapCtx.beginPath()
minimapCtx.moveTo(x, 0)
minimapCtx.lineTo(x, minimapCanvas.height)
minimapCtx.stroke()
}
for (let y = 0; y <= minimapCanvas.height; y += gridSize) {
minimapCtx.beginPath()
minimapCtx.moveTo(0, y)
minimapCtx.lineTo(minimapCanvas.width, y)
minimapCtx.stroke()
}
}
// 找到所有节点的位置以计算缩略图的适当比例
let minX = Number.POSITIVE_INFINITY,
minY = Number.POSITIVE_INFINITY,
maxX = 0,
maxY = 0
nodes.forEach((node) => {
minX = Math.min(minX, node.position.x)
minY = Math.min(minY, node.position.y)
const nodeElement = document.getElementById(node.id)
if (nodeElement) {
maxX = Math.max(maxX, node.position.x + nodeElement.offsetWidth)
maxY = Math.max(maxY, node.position.y + nodeElement.offsetHeight)
} else {
maxX = Math.max(maxX, node.position.x + 120) // 默认节点宽度
maxY = Math.max(maxY, node.position.y + 40) // 默认节点高度
}
})
// 添加边距
minX = Math.max(0, minX - 50)
minY = Math.max(0, minY - 50)
maxX += 50
maxY += 50
// 如果没有节点,使用默认视图区域
if (nodes.length === 0 || minX === Number.POSITIVE_INFINITY) {
minX = 0
minY = 0
maxX = 1000
maxY = 1000
}
const contentWidth = maxX - minX
const contentHeight = maxY - minY
// 计算缩放比例以适应所有节点
const scaleX = minimapCanvas.width / contentWidth
const scaleY = minimapCanvas.height / contentHeight
const minimapScale = Math.min(scaleX, scaleY, 1) // 不超过1:1比例
// 计算绘制偏移,使内容居中
const offsetX = (minimapCanvas.width - contentWidth * minimapScale) / 2
const offsetY = (minimapCanvas.height - contentHeight * minimapScale) / 2
// 绘制节点
nodes.forEach((node) => {
const nodeElement = document.getElementById(node.id)
if (nodeElement) {
const x = (node.position.x - minX) * minimapScale + offsetX
const y = (node.position.y - minY) * minimapScale + offsetY
const width = nodeElement.offsetWidth * minimapScale
const height = nodeElement.offsetHeight * minimapScale
// 根据节点类型设置颜色
minimapCtx.fillStyle = nodeTypeColors[node.type] || "#e6f7ff"
minimapCtx.fillRect(x, y, width, height)
minimapCtx.strokeStyle = "#666"
minimapCtx.strokeRect(x, y, width, height)
}
})
// 绘制连接线
minimapCtx.strokeStyle = "#5c6bc0"
minimapCtx.lineWidth = 1
jsPlumbInstance.getConnections().forEach((conn) => {
const sourceNode = nodes.find((node) => node.id === conn.source.id)
const targetNode = nodes.find((node) => node.id === conn.target.id)
if (sourceNode && targetNode) {
const sourceElement = document.getElementById(sourceNode.id)
const targetElement = document.getElementById(targetNode.id)
if (sourceElement && targetElement) {
const sourceX = (sourceNode.position.x + sourceElement.offsetWidth / 2 - minX) * minimapScale + offsetX
const sourceY = (sourceNode.position.y + sourceElement.offsetHeight / 2 - minY) * minimapScale + offsetY
const targetX = (targetNode.position.x + targetElement.offsetWidth / 2 - minX) * minimapScale + offsetX
const targetY = (targetNode.position.y + targetElement.offsetHeight / 2 - minY) * minimapScale + offsetY
minimapCtx.beginPath()
minimapCtx.moveTo(sourceX, sourceY)
minimapCtx.lineTo(targetX, targetY)
minimapCtx.stroke()
}
}
})
}
// 添加节点样式数据
const nodeStyles = {
default: {
name: "默认",
className: "node-style-default",
},
modern: {
name: "现代",
className: "node-style-modern",
},
flat: {
name: "扁平",
className: "node-style-flat",
},
rounded: {
name: "圆角",
className: "node-style-rounded",
},
}
// 创建节点函数增加样式支持
function createNode(type, x, y) {
const nodeId = `node-${++nodeCounter}`
const nodeName = getNodeName(type)
// 创建节点数据
const nodeData = {
id: nodeId,
type: type,
position: { x, y },
data: {
name: nodeName,
code: type === "code" ? 'console.log("Hello World");' : "",
description: "",
timeout: 5000,
retryCount: 0,
retryDelay: 1000,
enabled: true,
priority: "medium",
tags: [],
style: "default", // 默认样式
},
}
// 添加到节点数组
nodes.push(nodeData)
// 创建DOM元素
const nodeElement = document.createElement("div")
nodeElement.id = nodeId
nodeElement.className = `workflow-node ${nodeStyles.default.className}`
nodeElement.style.left = `${x}px`
nodeElement.style.top = `${y}px`
// 设置节点内容
nodeElement.innerHTML = `
<div class="workflow-node-header">
<div class="workflow-node-icon" style="background-color: ${nodeTypeColors[type]}; color: ${nodeTypeTextColors[type]}">
${nodeTypeIcons[type] || ""}
</div>
<div class="workflow-node-title">${nodeName}</div>
</div>
`
// 添加到画布
canvas.appendChild(nodeElement)
// 使节点可拖动
jsPlumbInstance.draggable(nodeElement, {
grid: [10, 10],
stop: (event) => {
// 更新节点位置
const nodeIndex = nodes.findIndex((n) => n.id === nodeId)
if (nodeIndex !== -1) {
nodes[nodeIndex].position.x = Number.parseInt(nodeElement.style.left)
nodes[nodeIndex].position.y = Number.parseInt(nodeElement.style.top)
}
// 更新缩略图
updateMinimap()
},
})
// 添加端点
// 源端点(输出)
jsPlumbInstance.addEndpoint(nodeElement, {
anchor: "Right",
isSource: true,
maxConnections: -1,
connectorStyle: { stroke: "#5c6bc0", strokeWidth: 2 },
})
// 目标端点(输入)
jsPlumbInstance.addEndpoint(nodeElement, {
anchor: "Left",
isTarget: true,
maxConnections: -1,
connectorStyle: { stroke: "#5c6bc0", strokeWidth: 2 },
})
// 添加点击事件
nodeElement.addEventListener("click", (e) => {
e.stopPropagation()
selectNode(nodeData)
})
// 更新缩略图
updateMinimap()
return nodeData
}
// 更新节点样式函数
function updateNodeStyle(nodeId, styleName) {
const nodeElement = document.getElementById(nodeId)
if (nodeElement) {
// 移除所有样式类
Object.values(nodeStyles).forEach((style) => {
nodeElement.classList.remove(style.className)
})
// 添加新样式类
if (nodeStyles[styleName]) {
nodeElement.classList.add(nodeStyles[styleName].className)
}
}
}
// 选择节点
function selectNode(node) {
// 清除之前的连接线选择
if (selectedConnection) {
selectedConnection.setPaintStyle({ stroke: "#5c6bc0", strokeWidth: 2 })
selectedConnection = null
}
// 清除之前的节点选择
if (selectedNode) {
const prevElement = document.getElementById(selectedNode.id)
if (prevElement) {
prevElement.classList.remove("selected")
}
}
// 设置新的选择
selectedNode = node
if (node) {
// 高亮选中的节点
const nodeElement = document.getElementById(node.id)
if (nodeElement) {
nodeElement.classList.add("selected")
}
// 更新属性面板
updatePropertyPanel(node)
} else {
// 隐藏属性面板
propertyForm.style.display = "none"
noSelection.style.display = "flex"
}
}
// 选择连接线
function selectConnection(connection) {
// 清除之前的节点选择
if (selectedNode) {
const prevElement = document.getElementById(selectedNode.id)
if (prevElement) {
prevElement.classList.remove("selected")
}
}
selectedNode = null
// 清除之前的连接线选择
if (selectedConnection) {
selectedConnection.setPaintStyle({ stroke: "#5c6bc0", strokeWidth: 2 })
}
// 设置新的选择
selectedConnection = connection
if (connection) {
// 高亮选中的连接线
connection.setPaintStyle({ stroke: "#1890ff", strokeWidth: 3 })
// 更新属性面板
updateConnectionPropertyPanel(connection)
} else {
// 隐藏属性面板
propertyForm.style.display = "none"
noSelection.style.display = "flex"
}
}
// 更新属性面板
function updatePropertyPanel(node) {
// 显示属性表单,隐藏无选择提示
propertyForm.style.display = "block"
noSelection.style.display = "none"
// 更新节点信息
document.querySelector(".node-info-title").textContent = getNodeName(node.type)
document.querySelector(".node-info-id").textContent = `ID: ${node.id}`
document.querySelector(".node-info-icon").textContent = node.type.charAt(0).toUpperCase()
document.querySelector(".node-info-icon").style.backgroundColor = nodeTypeColors[node.type]
document.querySelector(".node-info-icon").style.color = nodeTypeTextColors[node.type]
// 显示节点属性表单,隐藏连接线属性表单
document.getElementById("node-properties").style.display = "block"
document.getElementById("connection-properties").style.display = "none"
// 更新表单字段
nodeNameInput.value = node.data.name
// 显示/隐藏代码编辑器
if (node.type === "code" || node.type === "function") {
codeEditorContainer.style.display = "block"
codeEditor.value = node.data.code || 'console.log("Hello World");'
} else {
codeEditorContainer.style.display = "none"
}
// 更新样式选择器
const styleSelect = document.getElementById("node-style")
if (styleSelect) {
styleSelect.value = node.data.style || "default"
} else {
// 如果样式选择器不存在,创建一个
const styleFormGroup = document.createElement("div")
styleFormGroup.className = "form-group"
styleFormGroup.innerHTML = `
<label>节点样式</label>
<select id="node-style" class="form-control">
${Object.entries(nodeStyles)
.map(
([value, style]) => `
<option value="${value}" ${node.data.style === value ? "selected" : ""}>${style.name}</option>
`,
)
.join("")}
</select>
<div id="style-grid" class="style-grid">
${Object.entries(nodeStyles)
.map(
([value, style]) => `
<div class="style-option ${node.data.style === value ? "selected" : ""}"
data-style="${value}"
style="background-color: white; border-radius: ${style.className.includes("rounded") ? "8px" : style.className.includes("modern") ? "4px" : "0"};
box-shadow: ${style.className.includes("modern") ? "0 4px 8px rgba(0,0,0,0.1)" : style.className.includes("flat") ? "none" : "0 1px 3px rgba(0,0,0,0.1)"};
border: ${style.className.includes("flat") ? "1px solid #e0e0e0" : style.className.includes("modern") ? "none" : "1px solid #e0e0e0"};">
</div>
`,
)
.join("")}
</div>
`
// 在描述表单组之前插入样式选择器
const descriptionFormGroup = document.querySelector("#node-properties .form-group:nth-child(2)")
if (descriptionFormGroup) {
descriptionFormGroup.parentNode.insertBefore(styleFormGroup, descriptionFormGroup)
} else {
document.getElementById("node-properties").appendChild(styleFormGroup)
}
// 添加事件监听
document.querySelectorAll("#style-grid .style-option").forEach((option) => {
option.addEventListener("click", function () {
const styleName = this.getAttribute("data-style")
document.getElementById("node-style").value = styleName
// 更新选中状态
document.querySelectorAll("#style-grid .style-option").forEach((opt) => opt.classList.remove("selected"))
this.classList.add("selected")
// 更新节点数据和样式
updateNodeData(node.id, { style: styleName })
updateNodeStyle(node.id, styleName)
})
})
document.getElementById("node-style").addEventListener("change", function () {
// 更新选中状态
const styleName = this.value
document.querySelectorAll("#style-grid .style-option").forEach((opt) => {
if (opt.getAttribute("data-style") === styleName) {
opt.classList.add("selected")
} else {
opt.classList.remove("selected")
}
})
// 更新节点数据和样式
updateNodeData(node.id, { style: styleName })
updateNodeStyle(node.id, styleName)
})
}
// 更新高级选项
document.getElementById("node-description").value = node.data.description || ""
document.getElementById("node-timeout").value = node.data.timeout || 5000
document.getElementById("node-retry-count").value = node.data.retryCount || 0
document.getElementById("node-retry-delay").value = node.data.retryDelay || 1000
document.getElementById("node-enabled").checked = node.data.enabled !== false
// 更新优先级选择
const prioritySelect = document.getElementById("node-priority")
if (prioritySelect) {
prioritySelect.value = node.data.priority || "medium"
}
// 更新标签
const tagsInput = document.getElementById("node-tags")
if (tagsInput) {
tagsInput.value = (node.data.tags || []).join(", ")
}
}
// 更新连接线属性面板
function updateConnectionPropertyPanel(connection) {
// 显示属性表单,隐藏无选择提示
propertyForm.style.display = "block"
noSelection.style.display = "none"
// 更新连接线信息
document.querySelector(".node-info-title").textContent = "连接线"
document.querySelector(".node-info-id").textContent = `从 ${connection.source.id} 到 ${connection.target.id}`
document.querySelector(".node-info-icon").textContent = "C"
document.querySelector(".node-info-icon").style.backgroundColor = "#e6f7ff"
document.querySelector(".node-info-icon").style.color = "#1890ff"
// 显示连接线属性表单,隐藏节点属性表单
document.getElementById("connection-properties").style.display = "block"
document.getElementById("node-properties").style.display = "none"
// 更新表单字段
document.getElementById("connection-label").value = connection.getLabel() || ""
document.getElementById("connection-color").value = connection.getPaintStyle().stroke || "#5c6bc0"
document.getElementById("connection-width").value = connection.getPaintStyle().strokeWidth || 2
document.getElementById("connection-style").value = connection.getConnector()[0] || "Bezier"
}
// 更新节点数据
function updateNodeData(id, data) {
const nodeIndex = nodes.findIndex((n) => n.id === id)
if (nodeIndex !== -1) {
nodes[nodeIndex].data = { ...nodes[nodeIndex].data, ...data }
// 如果更新了名称,同时更新DOM
if (data.name) {
const nodeElement = document.getElementById(id)
if (nodeElement) {
nodeElement.querySelector(".workflow-node-title").textContent = data.name
}
}
// 如果更新了样式,应用样式
if (data.style) {
updateNodeStyle(id, data.style)
}
}
}
// 更新连接线数据
function updateConnectionData(connection, data) {
if (connection) {
if (data.label !== undefined) {
connection.setLabel(data.label)
}
if (data.color !== undefined) {
const style = connection.getPaintStyle()
style.stroke = data.color
connection.setPaintStyle(style)
}
if (data.width !== undefined) {
const style = connection.getPaintStyle()
style.strokeWidth = data.width
connection.setPaintStyle(style)
}
if (data.style !== undefined) {
connection.setConnector([data.style, { curviness: 50 }])
}
}
// 更新缩略图
updateMinimap()
}
// 导出工作流
function exportWorkflow() {
const connections = jsPlumbInstance.getConnections().map((conn) => ({
source: conn.source.id,
target: conn.target.id,
label: conn.getLabel() || "",
style: {
stroke: conn.getPaintStyle().stroke,
strokeWidth: conn.getPaintStyle().strokeWidth,
connector: conn.getConnector()[0],
},
}))
const workflow = {
nodes,
connections,
backgroundSettings,
}
const dataStr = JSON.stringify(workflow, null, 2)
const dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(dataStr)
const exportFileDefaultName = "workflow.json"
const linkElement = document.createElement("a")
linkElement.setAttribute("href", dataUri)
linkElement.setAttribute("download", exportFileDefaultName)
linkElement.click()
}
// 运行工作流
function runWorkflow() {
alert("工作流运行功能将在实际应用中实现")
}
// 设置画布缩放
function setZoom(newScale) {
// 限制缩放范围
newScale = Math.max(0.1, Math.min(2, newScale))
// 更新缩放比例
scale = newScale
// 应用缩放
canvas.style.transform = `scale(${scale})`
canvas.style.transformOrigin = "0 0"
// 更新jsPlumb
jsPlumbInstance.setZoom(scale)
// 更新缩放显示
document.getElementById("zoom-level").textContent = `${Math.round(scale * 100)}%`
// 更新缩略图
updateMinimap()
}
// 放大
function zoomIn() {
setZoom(scale + 0.1)
}
// 缩小
function zoomOut() {
setZoom(scale - 0.1)
}
// 重置缩放
function resetZoom() {
setZoom(1)
}
// 更新缩略图
// function updateMinimap() {
// const minimapCtx = minimapCanvas.getContext("2d")
// const canvasRect = canvas.getBoundingClientRect()
// const containerRect = canvas.parentElement.getBoundingClientRect()
// // 清除缩略图
// minimapCtx.clearRect(0, 0, minimapCanvas.width, minimapCanvas.height)
// // 设置缩略图背景
// minimapCtx.fillStyle = backgroundSettings.color
// minimapCtx.fillRect(0, 0, minimapCanvas.width, minimapCanvas.height)
// // 如果是网格背景,绘制网格
// if (backgroundSettings.type === "grid") {
// minimapCtx.strokeStyle = "rgba(200, 200, 200, 0.3)"
// minimapCtx.lineWidth = 0.5
// const gridSize = 5 // 缩略图中的网格大小
// for (let x = 0; x <= minimapCanvas.width; x += gridSize) {
// minimapCtx.beginPath()
// minimapCtx.moveTo(x, 0)
// minimapCtx.lineTo(x, minimapCanvas.height)
// minimapCtx.stroke()
// }
// for (let y = 0; y <= minimapCanvas.height; y += gridSize) {
// minimapCtx.beginPath()
// minimapCtx.moveTo(0, y)
// minimapCtx.lineTo(minimapCanvas.width, y)
// minimapCtx.stroke()
// }
// }
// // 绘制节点
// const minimapScale = minimapCanvas.width / canvas.scrollWidth
// nodes.forEach((node) => {
// const nodeElement = document.getElementById(node.id)
// if (nodeElement) {
// const x = node.position.x * minimapScale
// const y = node.position.y * minimapScale
// const width = nodeElement.offsetWidth * minimapScale
// const height = nodeElement.offsetHeight * minimapScale
// minimapCtx.fillStyle = nodeTypeColors[node.type] || "#e6f7ff"
// minimapCtx.fillRect(x, y, width, height)
// minimapCtx.strokeStyle = "#666"
// minimapCtx.strokeRect(x, y, width, height)
// }
// })
// // 绘制连接线
// minimapCtx.strokeStyle = "#5c6bc0"
// minimapCtx.lineWidth = 1
// jsPlumbInstance.getConnections().forEach((conn) => {
// const sourceElement = document.getElementById(conn.source.id)
// const targetElement = document.getElementById(conn.target.id)
// if (sourceElement && targetElement) {
// const sourceX = (Number.parseInt(sourceElement.style.left) + sourceElement.offsetWidth) * minimapScale
// const sourceY = (Number.parseInt(sourceElement.style.top) + sourceElement.offsetHeight / 2) * minimapScale
// const targetX = Number.parseInt(targetElement.style.left) * minimapScale
// const targetY = (Number.parseInt(targetElement.style.top) + targetElement.offsetHeight / 2) * minimapScale
// minimapCtx.beginPath()
// minimapCtx.moveTo(sourceX, sourceY)
// minimapCtx.lineTo(targetX, targetY)
// minimapCtx.stroke()
// }
// })
// // 更新视口矩形
// const viewportWidth = containerRect.width * minimapScale
// const viewportHeight = containerRect.height * minimapScale
// const viewportX = (canvas.parentElement.scrollLeft / scale) * minimapScale
// const viewportY = (canvas.parentElement.scrollTop / scale) * minimapScale
// minimapViewport.style.width = `${viewportWidth}px`
// minimapViewport.style.height = `${viewportHeight}px`
// minimapViewport.style.left = `${viewportX}px`
// minimapViewport.style.top = `${viewportY}px`
// }
// 应用背景设置
function applyBackgroundSettings() {
// 应用背景类型
if (backgroundSettings.type === "grid") {
canvas.querySelector(".canvas-grid").style.display = "block"
canvas.style.backgroundColor = backgroundSettings.color
// 设置网格样式
canvas.querySelector(".canvas-grid").style.backgroundSize =
`${backgroundSettings.gridSize}px ${backgroundSettings.gridSize}px`
canvas.querySelector(".canvas-grid").style.backgroundImage = `
linear-gradient(to right, ${backgroundSettings.gridColor} 1px, transparent 1px),
linear-gradient(to bottom, ${backgroundSettings.gridColor} 1px, transparent 1px)
`
} else {
canvas.querySelector(".canvas-grid").style.display = "none"
canvas.style.backgroundColor = backgroundSettings.color
}
// 更新UI控件
backgroundTypeSelect.value = backgroundSettings.type
backgroundColorPicker.value = backgroundSettings.color
gridSizeInput.value = backgroundSettings.gridSize
gridColorPicker.value = backgroundSettings.gridColor
// 更新网格设置区域的显示/隐藏
document.getElementById("grid-settings").style.display = backgroundSettings.type === "grid" ? "block" : "none"
// 更新缩略图
updateMinimap()
}
// 事件监听:从左侧面板拖拽节点到画布
nodeItems.forEach((item) => {
item.addEventListener("mousedown", function (e) {
const nodeType = this.getAttribute("data-type")
// 创建拖拽预览元素
const dragPreview = document.createElement("div")
dragPreview.className = "workflow-node drag-preview"
dragPreview.innerHTML = `
<div class="workflow-node-header">
<div class="workflow-node-icon" style="background-color: ${nodeTypeColors[nodeType]}; color: ${nodeTypeTextColors[nodeType]}">
${nodeTypeIcons[nodeType] || ""}
</div>
<div class="workflow-node-title">${getNodeName(nodeType)}</div>
</div>
`
dragPreview.style.position = "absolute"
dragPreview.style.opacity = "0.7"
dragPreview.style.pointerEvents = "none"
document.body.appendChild(dragPreview)
// 记录鼠标在预览元素内的位置
const offsetX = e.clientX - this.getBoundingClientRect().left
const offsetY = e.clientY - this.getBoundingClientRect().top
// 鼠标移动事件
function onMouseMove(e) {
dragPreview.style.left = `${e.clientX - offsetX}px`
dragPreview.style.top = `${e.clientY - offsetY}px`
}
// 鼠标松开事件
function onMouseUp(e) {
document.removeEventListener("mousemove", onMouseMove)
document.removeEventListener("mouseup", onMouseUp)
// 移除预览元素
document.body.removeChild(dragPreview)
// 获取画布相对于视口的位置
const canvasRect = canvas.getBoundingClientRect()
// 检查鼠标是否在画布上
if (
e.clientX >= canvasRect.left &&
e.clientX <= canvasRect.right &&
e.clientY >= canvasRect.top &&
e.clientY <= canvasRect.bottom
) {
// 计算在画布中的位置,考虑缩放因素
const canvasX = (e.clientX - canvasRect.left) / scale + canvas.scrollLeft
const canvasY = (e.clientY - canvasRect.top) / scale + canvas.scrollTop
// 创建新节点
const newNode = createNode(nodeType, canvasX, canvasY)
// 选中新节点
selectNode(newNode)
}
}
// 添加事件监听
document.addEventListener("mousemove", onMouseMove)
document.addEventListener("mouseup", onMouseUp)
})
})
// 事件监听:属性面板表单变化
nodeNameInput.addEventListener("input", function () {
if (selectedNode) {
updateNodeData(selectedNode.id, { name: this.value })
}
})
codeEditor.addEventListener("input", function () {
if (selectedNode && (selectedNode.type === "code" || selectedNode.type === "function")) {
updateNodeData(selectedNode.id, { code: this.value })
}
})
// 监听高级选项变化
document.getElementById("node-description").addEventListener("input", function () {
if (selectedNode) {
updateNodeData(selectedNode.id, { description: this.value })
}
})
document.getElementById("node-timeout").addEventListener("input", function () {
if (selectedNode) {
updateNodeData(selectedNode.id, { timeout: Number.parseInt(this.value) || 5000 })
}
})
document.getElementById("node-retry-count").addEventListener("input", function () {
if (selectedNode) {
updateNodeData(selectedNode.id, { retryCount: Number.parseInt(this.value) || 0 })
}
})
document.getElementById("node-retry-delay").addEventListener("input", function () {
if (selectedNode) {
updateNodeData(selectedNode.id, { retryDelay: Number.parseInt(this.value) || 1000 })
}
})
document.getElementById("node-enabled").addEventListener("change", function () {
if (selectedNode) {
updateNodeData(selectedNode.id, { enabled: this.checked })
}
})
document.getElementById("node-priority").addEventListener("change", function () {
if (selectedNode) {
updateNodeData(selectedNode.id, { priority: this.value })
}
})
document.getElementById("node-tags").addEventListener("input", function () {
if (selectedNode) {
const tags = this.value
.split(",")
.map((tag) => tag.trim())
.filter((tag) => tag)
updateNodeData(selectedNode.id, { tags })
}
})
// 连接线属性事件监听
document.getElementById("connection-label").addEventListener("input", function () {
if (selectedConnection) {
updateConnectionData(selectedConnection, { label: this.value })
}
})
document.getElementById("connection-color").addEventListener("input", function () {
if (selectedConnection) {
updateConnectionData(selectedConnection, { color: this.value })
}
})
document.getElementById("connection-width").addEventListener("input", function () {
if (selectedConnection) {
updateConnectionData(selectedConnection, { width: Number.parseInt(this.value) || 2 })
}
})
document.getElementById("connection-style").addEventListener("change", function () {
if (selectedConnection) {
updateConnectionData(selectedConnection, { style: this.value })
}
})
// 背景设置事件监听
backgroundTypeSelect.addEventListener("change", function () {
backgroundSettings.type = this.value
applyBackgroundSettings()
})
backgroundColorPicker.addEventListener("input", function () {
backgroundSettings.color = this.value
applyBackgroundSettings()
})
gridSizeInput.addEventListener("input", function () {
backgroundSettings.gridSize = Number.parseInt(this.value) || 20
applyBackgroundSettings()
})
gridColorPicker.addEventListener("input", function () {
backgroundSettings.gridColor = this.value
applyBackgroundSettings()
})
// 事件监听:切换标签页
tabButtons.forEach((button) => {
button.addEventListener("click", function () {
const tabId = this.getAttribute("data-tab")
// 更新按钮状态
tabButtons.forEach((btn) => btn.classList.remove("active"))
this.classList.add("active")
// 更新内容显示
tabContents.forEach((content) => {
content.style.display = content.id === `${tabId}-tab` ? "block" : "none"
})
})
})
// 事件监听:导出按钮
exportBtn.addEventListener("click", exportWorkflow)
// 事件监听:运行按钮
runBtn.addEventListener("click", runWorkflow)
// 事件监听:缩放按钮
zoomInBtn.addEventListener("click", zoomIn)
zoomOutBtn.addEventListener("click", zoomOut)
resetZoomBtn.addEventListener("click", resetZoom)
// 事件监听:画布点击,取消选择
canvas.addEventListener("click", (e) => {
if (e.target === canvas || e.target === canvas.querySelector(".canvas-grid")) {
selectNode(null)
}
})
// 事件监听:鼠标滚轮缩放
canvas.addEventListener("wheel", (e) => {
if (e.ctrlKey) {
e.preventDefault()
const delta = e.deltaY > 0 ? -0.1 : 0.1
setZoom(scale + delta)
}
})
// 缩略图点击事件
minimapCanvas.addEventListener("click", (e) => {
const minimapRect = minimapCanvas.getBoundingClientRect()
const minimapScale = minimapCanvas.width / canvas.scrollWidth
const x = ((e.clientX - minimapRect.left) / minimapScale) * scale
const y = ((e.clientY - minimapRect.top) / minimapScale) * scale
// 将画布滚动到点击位置
canvas.parentElement.scrollLeft = x - canvas.parentElement.clientWidth / 2
canvas.parentElement.scrollTop = y - canvas.parentElement.clientHeight / 2
// 更新缩略图
updateMinimap()
})
// 缩略图拖拽事件
minimapViewport.addEventListener("mousedown", (e) => {
e.stopPropagation()
const startX = e.clientX
const startY = e.clientY
const startScrollLeft = canvas.parentElement.scrollLeft
const startScrollTop = canvas.parentElement.scrollTop
const minimapScale = minimapCanvas.width / canvas.scrollWidth
function onMouseMove(e) {
const dx = ((e.clientX - startX) / minimapScale) * scale
const dy = ((e.clientY - startY) / minimapScale) * scale
canvas.parentElement.scrollLeft = startScrollLeft + dx
canvas.parentElement.scrollTop = startScrollTop + dy
updateMinimap()
}
function onMouseUp() {
document.removeEventListener("mousemove", onMouseMove)
document.removeEventListener("mouseup", onMouseUp)
}
document.addEventListener("mousemove", onMouseMove)
document.addEventListener("mouseup", onMouseUp)
})
// 画布滚动事件
canvas.parentElement.addEventListener("scroll", () => {
updateMinimap()
})
// 监听窗口大小变化,更新缩略图
window.addEventListener("resize", () => {
updateMinimap()
})
// 在jsPlumb创建连接后更新缩略图
jsPlumbInstance.bind("connection", () => {
updateMinimap()
})
// 初始化jsPlumb
jsPlumbInstance.setContainer(canvas)
// 初始化缩略图
updateMinimap()
// 应用初始背景设置
applyBackgroundSettings()
// 预览功能
function showPreview() {
// Update node and connection counts
nodeCountElement.textContent = nodes.length
connectionCountElement.textContent = jsPlumbInstance.getConnections().length
// Update node list
nodeListElement.innerHTML = ""
nodes.forEach((node) => {
const nodeItem = document.createElement("div")
nodeItem.className = "preview-list-item"
nodeItem.innerHTML = `
<div class="preview-node-icon" style="background-color: ${nodeTypeColors[node.type]}; color: ${
nodeTypeTextColors[node.type]
}">
${nodeTypeIcons[node.type] || ""}
</div>
<div>
<div>${node.data.name}</div>
<div class="text-xs text-gray-500">类型: ${getNodeName(node.type)}</div>
</div>
`
nodeListElement.appendChild(nodeItem)
})
// Clear the previous preview canvas container
const previewCanvasContainer = document.querySelector(".preview-canvas-container")
previewCanvasContainer.innerHTML = ""
// Create interactive preview canvas
const previewCanvasElement = document.createElement("div")
previewCanvasElement.id = "interactive-preview-canvas"
previewCanvasElement.className = "interactive-preview-canvas"
previewCanvasContainer.appendChild(previewCanvasElement)
// Initialize the preview with the same settings as the main workflow
initializePreviewCanvas(previewCanvasElement)
// Update JSON preview
const workflowData = {
nodes,
connections: jsPlumbInstance.getConnections().map((conn) => ({
source: conn.source.id,
target: conn.target.id,
label: conn.getLabel() || "",
style: {
stroke: conn.getPaintStyle().stroke,
strokeWidth: conn.getPaintStyle().strokeWidth,
connector: conn.getConnector()[0],
},
})),
backgroundSettings,
}
jsonContent.textContent = JSON.stringify(workflowData, null, 2)
// Show preview modal
previewModal.style.display = "flex"
}
// Add this new function to initialize the preview canvas
function initializePreviewCanvas(previewContainer) {
// Apply background settings to preview canvas
previewContainer.style.position = "relative"
previewContainer.style.width = "100%"
previewContainer.style.height = "100%"
previewContainer.style.backgroundColor = backgroundSettings.color
// Add grid if needed
if (backgroundSettings.type === "grid") {
const gridElement = document.createElement("div")
gridElement.className = "preview-grid"
gridElement.style.position = "absolute"
gridElement.style.top = "0"
gridElement.style.left = "0"
gridElement.style.right = "0"
gridElement.style.bottom = "0"
gridElement.style.backgroundSize = `${backgroundSettings.gridSize}px ${backgroundSettings.gridSize}px`
gridElement.style.backgroundImage = `
linear-gradient(to right, ${backgroundSettings.gridColor} 1px, transparent 1px),
linear-gradient(to bottom, ${backgroundSettings.gridColor} 1px, transparent 1px)
`
gridElement.style.zIndex = "0"
previewContainer.appendChild(gridElement)
}
// Create a new jsPlumb instance for the preview
const previewJsPlumb = jsPlumb.getInstance({
Connector: ["Bezier", { curviness: 50 }],
Endpoint: ["Dot", { radius: 5 }],
HoverPaintStyle: { stroke: "#1e88e5", strokeWidth: 2 },
ConnectionOverlays: [
[
"Arrow",
{
location: 1,
width: 10,
length: 10,
id: "arrow",
},
],
],
Container: previewContainer,
})
// Clone all nodes from the main workflow
nodes.forEach((node) => {
const originalNode = document.getElementById(node.id)
if (originalNode) {
// Create preview node element
const previewNode = document.createElement("div")
previewNode.id = `preview-${node.id}`
previewNode.className = originalNode.className
previewNode.innerHTML = originalNode.innerHTML
previewNode.style.position = "absolute"
previewNode.style.left = `${node.position.x}px`
previewNode.style.top = `${node.position.y}px`
// Add preview node to container
previewContainer.appendChild(previewNode)
// Make the node draggable in preview (but no other interactions)
previewJsPlumb.draggable(previewNode, {
grid: [10, 10],
// No callbacks for position updates since we're just previewing
})
// Add endpoints similar to the original node
previewJsPlumb.addEndpoint(previewNode, {
anchor: "Right",
isSource: false, // Disable creating new connections
isTarget: false, // Disable creating new connections
maxConnections: -1,
connectorStyle: { stroke: "#5c6bc0", strokeWidth: 2 },
})
previewJsPlumb.addEndpoint(previewNode, {
anchor: "Left",
isSource: false, // Disable creating new connections
isTarget: false, // Disable creating new connections
maxConnections: -1,
connectorStyle: { stroke: "#5c6bc0", strokeWidth: 2 },
})
}
})
// Recreate all connections
jsPlumbInstance.getConnections().forEach((conn) => {
const sourceId = `preview-${conn.source.id}`
const targetId = `preview-${conn.target.id}`
// Get connection style
const style = conn.getPaintStyle()
const connectorType = conn.getConnector()[0]
// Create connection in preview
const connection = previewJsPlumb.connect({
source: sourceId,
target: targetId,
paintStyle: {
stroke: style.stroke,
strokeWidth: style.strokeWidth,
},
connector: [connectorType, { curviness: 50 }],
})
// Set label if exists
const label = conn.getLabel()
if (label) {
connection.setLabel(label)
}
})
}
// 渲染预览画布
function renderPreviewCanvas() {
const ctx = previewCanvas.getContext("2d")
const width = previewCanvas.width
const height = previewCanvas.height
// 清除画布
ctx.clearRect(0, 0, width, height)
// 设置背景
ctx.fillStyle = backgroundSettings.color
ctx.fillRect(0, 0, width, height)
// 如果是网格背景,绘制网格
if (backgroundSettings.type === "grid") {
ctx.strokeStyle = backgroundSettings.gridColor
ctx.lineWidth = 0.5
const gridSize = 20
for (let x = 0; x <= width; x += gridSize) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, height)
ctx.stroke()
}
for (let y = 0; y <= height; y += gridSize) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(width, y)
ctx.stroke()
}
}
// 找到所有节点的位置以计算适当的缩放比例
if (nodes.length === 0) return
let minX = Number.POSITIVE_INFINITY,
minY = Number.POSITIVE_INFINITY,
maxX = 0,
maxY = 0
nodes.forEach((node) => {
minX = Math.min(minX, node.position.x)
minY = Math.min(minY, node.position.y)
const nodeElement = document.getElementById(node.id)
if (nodeElement) {
maxX = Math.max(maxX, node.position.x + nodeElement.offsetWidth)
maxY = Math.max(maxY, node.position.y + nodeElement.offsetHeight)
} else {
maxX = Math.max(maxX, node.position.x + 120) // 默认节点宽度
maxY = Math.max(maxY, node.position.y + 40) // 默认节点高度
}
})
// 添加边距
minX = Math.max(0, minX - 50)
minY = Math.max(0, minY - 50)
maxX += 50
maxY += 50
const contentWidth = maxX - minX
const contentHeight = maxY - minY
// 计算缩放比例以适应所有节点
const scaleX = width / contentWidth
const scaleY = height / contentHeight
const previewScale = Math.min(scaleX, scaleY, 1) // 不超过1:1比例
// 计算绘制偏移,使内容居中
const offsetX = (width - contentWidth * previewScale) / 2
const offsetY = (height - contentHeight * previewScale) / 2
// 绘制连接线
ctx.strokeStyle = "#5c6bc0"
ctx.lineWidth = 2
jsPlumbInstance.getConnections().forEach((conn) => {
const sourceNode = nodes.find((node) => node.id === conn.source.id)
const targetNode = nodes.find((node) => node.id === conn.target.id)
if (sourceNode && targetNode) {
const sourceElement = document.getElementById(sourceNode.id)
const targetElement = document.getElementById(targetNode.id)
if (sourceElement && targetElement) {
const sourceX = (sourceNode.position.x + sourceElement.offsetWidth / 2 - minX) * previewScale + offsetX
const sourceY = (sourceNode.position.y + sourceElement.offsetHeight / 2 - minY) * previewScale + offsetY
const targetX = (targetNode.position.x + targetElement.offsetWidth / 2 - minX) * previewScale + offsetX
const targetY = (targetNode.position.y + targetElement.offsetHeight / 2 - minY) * previewScale + offsetY
// 使用连接线的自定义样式
const connStyle = conn.getPaintStyle()
ctx.strokeStyle = connStyle.stroke || "#5c6bc0"
ctx.lineWidth = connStyle.strokeWidth || 2
// 绘制连接线
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
ctx.lineTo(targetX, targetY)
ctx.stroke()
// 绘制箭头
const angle = Math.atan2(targetY - sourceY, targetX - sourceX)
const arrowLength = 10
const arrowWidth = 6
ctx.beginPath()
ctx.moveTo(targetX, targetY)
ctx.lineTo(
targetX - arrowLength * Math.cos(angle) + arrowWidth * Math.sin(angle),
targetY - arrowLength * Math.sin(angle) - arrowWidth * Math.cos(angle),
)
ctx.lineTo(
targetX - arrowLength * Math.cos(angle) - arrowWidth * Math.sin(angle),
targetY - arrowLength * Math.sin(angle) + arrowWidth * Math.cos(angle),
)
ctx.closePath()
ctx.fillStyle = connStyle.stroke || "#5c6bc0"
ctx.fill()
// 绘制标签
const label = conn.getLabel()
if (label) {
const labelX = (sourceX + targetX) / 2
const labelY = (sourceY + targetY) / 2 - 10
ctx.font = "12px Arial"
ctx.fillStyle = "#333"
ctx.textAlign = "center"
ctx.fillText(label, labelX, labelY)
}
}
}
})
// 绘制节点
nodes.forEach((node) => {
const nodeElement = document.getElementById(node.id)
if (nodeElement) {
const x = (node.position.x - minX) * previewScale + offsetX
const y = (node.position.y - minY) * previewScale + offsetY
const width = nodeElement.offsetWidth * previewScale
const height = nodeElement.offsetHeight * previewScale
// 绘制节点背景
ctx.fillStyle = "#fff"
ctx.strokeStyle = "#e0e0e0"
ctx.lineWidth = 1
// 根据节点样式绘制不同的形状
if (node.data.style === "rounded") {
ctx.beginPath()
ctx.roundRect(x, y, width, height, 16)
ctx.fill()
ctx.stroke()
} else if (node.data.style === "modern") {
ctx.shadowColor = "rgba(0, 0, 0, 0.1)"
ctx.shadowBlur = 10
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 4
ctx.beginPath()
ctx.roundRect(x, y, width, height, 8)
ctx.fill()
ctx.stroke()
ctx.shadowColor = "transparent"
} else if (node.data.style === "flat") {
ctx.fillStyle = "#f5f5f5"
ctx.beginPath()
ctx.rect(x, y, width, height)
ctx.fill()
ctx.stroke()
} else {
// 默认样式
ctx.beginPath()
ctx.rect(x, y, width, height)
ctx.fill()
ctx.stroke()
}
// 绘制节点图标
const iconSize = 24 * previewScale
const iconX = x + 10 * previewScale
const iconY = y + (height - iconSize) / 2
const iconBgColor = nodeTypeColors[node.type] || "#e6f7ff"
ctx.fillStyle = iconBgColor
ctx.beginPath()
ctx.roundRect(iconX, iconY, iconSize, iconSize, 4 * previewScale)
ctx.fill()
// 绘制节点标题
ctx.font = `${12 * previewScale}px Arial`
ctx.fillStyle = "#333"
ctx.textBaseline = "middle"
ctx.fillText(node.data.name, x + (iconSize + 20) * previewScale, y + height / 2)
}
})
}
// 保存功能
// function showSaveDialog() {
// // 显示保存对话框
// saveModal.style.display = "flex"
// }
function saveWorkflow() {
// 获取表单数据
const name = workflowNameInput.value || "我的工作流"
const description = workflowDescriptionInput.value || ""
const version = workflowVersionInput.value || "1.0.0"
const permission = workflowPermissionSelect.value || "private"
// 创建工作流数据
const workflowData = {
metadata: {
name,
description,
version,
permission,
createdAt: new Date().toISOString(),
},
nodes,
connections: jsPlumbInstance.getConnections().map((conn) => ({
source: conn.source.id,
target: conn.target.id,
label: conn.getLabel() || "",
style: {
stroke: conn.getPaintStyle().stroke,
strokeWidth: conn.getPaintStyle().strokeWidth,
connector: conn.getConnector()[0],
},
})),
backgroundSettings,
}
// 转换为JSON并下载
const dataStr = JSON.stringify(workflowData, null, 2)
const dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(dataStr)
const fileName = name.replace(/\s+/g, "-").toLowerCase() + ".json"
const linkElement = document.createElement("a")
linkElement.setAttribute("href", dataUri)
linkElement.setAttribute("download", fileName)
linkElement.click()
// 关闭对话框
saveModal.style.display = "none"
}
// 事件监听:预览按钮
previewBtn.addEventListener("click", () => {
console.log("Preview button clicked")
showPreview()
})
// 事件监听:保存按钮
// saveBtn.addEventListener("click", showSaveDialog)
// 事件监听:关闭预览
closePreviewBtn.addEventListener("click", () => {
previewModal.style.display = "none"
})
closePreviewBtnFooter.addEventListener("click", () => {
previewModal.style.display = "none"
})
// 事件监听:关闭保存对话框
closeSaveBtn.addEventListener("click", () => {
saveModal.style.display = "none"
})
cancelSaveBtn.addEventListener("click", () => {
saveModal.style.display = "none"
})
confirmSaveBtn.addEventListener("click", saveWorkflow)
// 事件监听:预览标签页切换
previewTabs.forEach((tab) => {
tab.addEventListener("click", function () {
const tabId = this.getAttribute("data-tab")
// 更新标签页状态
previewTabs.forEach((t) => t.classList.remove("active"))
this.classList.add("active")
// 更新内容显示
previewTabContents.forEach((content) => {
content.style.display = content.id === `${tabId}-preview` ? "flex" : "none"
})
})
})
})
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。