mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-23 22:24:32 +08:00
【调整】增加部署雨云
This commit is contained in:
106
frontend/apps/vue-flow/components/WorkflowEditor.module.css
Normal file
106
frontend/apps/vue-flow/components/WorkflowEditor.module.css
Normal file
@@ -0,0 +1,106 @@
|
||||
.workflowEditor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editorHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.titleInput {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.titleInput:focus {
|
||||
outline: none;
|
||||
border-color: #4096ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
padding: 6px 16px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.saveButton:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.editorContent {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nodesPanel {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.nodesPanelTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.nodesList {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nodeItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nodeItem:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.nodeItemIcon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nodeItemTitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
397
frontend/apps/vue-flow/components/WorkflowEditor.tsx
Normal file
397
frontend/apps/vue-flow/components/WorkflowEditor.tsx
Normal file
@@ -0,0 +1,397 @@
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import { VueFlow, Position, MarkerType } from '@vue-flow/core'
|
||||
import { Background } from '@vue-flow/background'
|
||||
import { MiniMap } from '@vue-flow/minimap'
|
||||
import { Controls } from '@vue-flow/controls'
|
||||
import { NodeToolbar } from '@vue-flow/node-toolbar'
|
||||
import { useWorkflowStore } from '../store/workflow'
|
||||
import { processWorkflowData } from '../store/transformFlowData'
|
||||
import '@vue-flow/core/dist/style.css'
|
||||
import '@vue-flow/core/dist/theme-default.css'
|
||||
import '../flow.css'
|
||||
import styles from './WorkflowEditor.module.css'
|
||||
|
||||
// 导入新的节点组件
|
||||
import NewStartNode from './nodes/NewStartNode'
|
||||
import NewApplyNode from './nodes/NewApplyNode'
|
||||
import NewBranchNode from './nodes/NewBranchNode'
|
||||
import NewDeployNode from './nodes/NewDeployNode'
|
||||
import NewUploadNode from './nodes/NewUploadNode'
|
||||
import NewNotifyNode from './nodes/NewNotifyNode'
|
||||
|
||||
// 示例数据
|
||||
const exampleFlowData = {
|
||||
id: '10001',
|
||||
name: '请假审批',
|
||||
childNode: {
|
||||
id: 'dfag-123',
|
||||
name: '开始',
|
||||
type: 'start',
|
||||
config: {
|
||||
time: '2025-04-11 10:00:00',
|
||||
type: 'manual',
|
||||
},
|
||||
childNode: {
|
||||
id: 'dfag-124',
|
||||
name: '申请证书',
|
||||
type: 'apply',
|
||||
config: {
|
||||
name: null,
|
||||
},
|
||||
childNode: {
|
||||
id: 'dfag-125',
|
||||
name: '分支节点',
|
||||
type: 'branch',
|
||||
conditionNodes: [
|
||||
{
|
||||
id: 'dfag-126',
|
||||
name: '部署网站',
|
||||
type: 'execute_result_branch',
|
||||
childNode: {
|
||||
id: 'dfag-128',
|
||||
name: '执行结果',
|
||||
type: 'execute_result_branch',
|
||||
conditionNodes: [
|
||||
{
|
||||
id: 'dfag-129',
|
||||
name: '执行成功',
|
||||
type: 'execute_result_condition',
|
||||
config: {
|
||||
type: 'SUCCESS',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dfag-130',
|
||||
name: '执行失败',
|
||||
type: 'execute_result_condition',
|
||||
config: {
|
||||
type: 'FAILURE',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dfag-131',
|
||||
name: '申请xxx证书',
|
||||
type: 'condition',
|
||||
config: {
|
||||
days: 3,
|
||||
},
|
||||
childNode: {
|
||||
id: 'dfag-132',
|
||||
name: '申请证书',
|
||||
type: 'apply',
|
||||
childNode: {
|
||||
id: 'dfag-133',
|
||||
name: '执行结果',
|
||||
type: 'execute_result_branch',
|
||||
conditionNodes: [
|
||||
{
|
||||
id: 'dfag-134',
|
||||
name: '执行成功',
|
||||
type: 'condition',
|
||||
config: {
|
||||
type: 'SUCCESS',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dfag-135',
|
||||
name: '执行失败',
|
||||
type: 'condition',
|
||||
config: {
|
||||
type: 'FAILURE',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dfag-131a',
|
||||
name: '申请xxx证书2',
|
||||
type: 'condition',
|
||||
config: {
|
||||
days: 3,
|
||||
},
|
||||
childNode: {
|
||||
id: 'dfag-132b',
|
||||
name: '申请证书',
|
||||
type: 'apply',
|
||||
childNode: {
|
||||
id: 'dfag-133v',
|
||||
name: '执行结果',
|
||||
type: 'execute_result_branch',
|
||||
conditionNodes: [
|
||||
{
|
||||
id: 'dfag-134f',
|
||||
name: '执行成功',
|
||||
type: 'condition',
|
||||
config: {
|
||||
type: 'SUCCESS',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dfag-135s',
|
||||
name: '执行失败',
|
||||
type: 'condition',
|
||||
config: {
|
||||
type: 'FAILURE',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dfag-131a2222',
|
||||
name: '申请xxx证书2',
|
||||
type: 'condition',
|
||||
config: {
|
||||
days: 3,
|
||||
},
|
||||
childNode: {
|
||||
id: 'dfag-132b2222',
|
||||
name: '申请证书',
|
||||
type: 'apply',
|
||||
childNode: {
|
||||
id: 'dfag-133v1111',
|
||||
name: '执行结果',
|
||||
type: 'execute_result_branch',
|
||||
conditionNodes: [
|
||||
{
|
||||
id: 'dfag-134faaa',
|
||||
name: '执行成功',
|
||||
type: 'condition',
|
||||
config: {
|
||||
type: 'SUCCESS',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dfag-135sccc',
|
||||
name: '执行失败',
|
||||
type: 'condition',
|
||||
config: {
|
||||
type: 'FAILURE',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
childNode: {
|
||||
id: 'dfag-1aa36',
|
||||
name: '通知任务',
|
||||
type: 'notify',
|
||||
config: {
|
||||
name: '李四',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 工作流编辑器组件
|
||||
export default defineComponent({
|
||||
name: 'WorkflowEditor',
|
||||
setup() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// 选中的节点ID
|
||||
const selectedNodeId = ref<string | null>(null)
|
||||
|
||||
// 初始化工作流
|
||||
onMounted(() => {
|
||||
// 将示例数据转换为VueFlow所需的节点和边
|
||||
const { nodes, edges } = processWorkflowData(exampleFlowData.childNode)
|
||||
|
||||
// 禁用所有节点的拖拽
|
||||
const nodesWithoutDrag = nodes.map((node) => ({
|
||||
...node,
|
||||
draggable: false, // 禁止拖拽
|
||||
data: {
|
||||
...node.data,
|
||||
canMove: false, // 禁止移动
|
||||
},
|
||||
}))
|
||||
|
||||
// 加载到工作流中
|
||||
workflowStore.loadWorkflow({ nodes: nodesWithoutDrag, edges })
|
||||
|
||||
// 设置工作流标题
|
||||
if (exampleFlowData.name) {
|
||||
workflowStore.setWorkflowTitle(exampleFlowData.name)
|
||||
}
|
||||
|
||||
// 监听打开节点配置面板事件
|
||||
window.addEventListener('open-node-config', (e: any) => {
|
||||
const { nodeId, nodeType, nodeData } = e.detail
|
||||
console.log('打开节点配置', nodeId, nodeType, nodeData)
|
||||
// 调用打开配置面板的方法
|
||||
workflowStore.selectNode(nodeData)
|
||||
})
|
||||
})
|
||||
|
||||
// 是否显示确认保存对话框
|
||||
const showSaveDialog = ref(false)
|
||||
|
||||
// 保存工作流
|
||||
const saveWorkflow = () => {
|
||||
workflowStore.saveWorkflow()
|
||||
showSaveDialog.value = false
|
||||
}
|
||||
|
||||
// 删除节点
|
||||
const deleteNode = (nodeId: string) => {
|
||||
if (nodeId) {
|
||||
// 获取当前节点列表和边列表的副本
|
||||
const newNodes = workflowStore.nodes.filter((node) => node.id !== nodeId)
|
||||
const newEdges = workflowStore.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)
|
||||
|
||||
// 更新store中的节点和边
|
||||
workflowStore.nodes = newNodes
|
||||
workflowStore.edges = newEdges
|
||||
|
||||
// 清除选中状态
|
||||
selectedNodeId.value = null
|
||||
workflowStore.selectNode(null)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制节点
|
||||
const duplicateNode = (nodeId: string) => {
|
||||
const node = workflowStore.nodes.find((node) => node.id === nodeId)
|
||||
if (node) {
|
||||
// 创建新节点(副本)
|
||||
const newNode = {
|
||||
...node,
|
||||
id: `${node.id}-copy`,
|
||||
position: {
|
||||
x: node.position.x + 50,
|
||||
y: node.position.y + 50,
|
||||
},
|
||||
}
|
||||
|
||||
// 更新store中的节点
|
||||
workflowStore.nodes = [...workflowStore.nodes, newNode]
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={styles.workflowEditor}>
|
||||
<div class={styles.editorHeader}>
|
||||
<div class={styles.title}>
|
||||
<input
|
||||
type="text"
|
||||
value={workflowStore.workflowTitle}
|
||||
onInput={(e) => workflowStore.setWorkflowTitle((e.target as HTMLInputElement).value)}
|
||||
placeholder="输入工作流名称"
|
||||
class={styles.titleInput}
|
||||
/>
|
||||
</div>
|
||||
<div class={styles.actions}>
|
||||
{workflowStore.isDataChanged && (
|
||||
<button class={styles.saveButton} onClick={() => saveWorkflow()}>
|
||||
保存
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.editorContent}>
|
||||
<VueFlow
|
||||
nodes={workflowStore.nodes}
|
||||
edges={workflowStore.edges}
|
||||
onNodeClick={(event, node) => {
|
||||
selectedNodeId.value = node.id
|
||||
workflowStore.selectNode(node)
|
||||
}}
|
||||
onPaneClick={() => {
|
||||
selectedNodeId.value = null
|
||||
workflowStore.selectNode(null)
|
||||
}}
|
||||
onNodesChange={workflowStore.onNodesChange}
|
||||
onEdgesChange={workflowStore.onEdgesChange}
|
||||
onConnect={workflowStore.onConnect}
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
defaultViewport={{ x: 0, y: 0, zoom: 0.6 }}
|
||||
fitViewOnInit
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
padding: 0.4,
|
||||
includeHiddenNodes: false,
|
||||
maxZoom: 0.7,
|
||||
}}
|
||||
connectionLineType="smoothstep"
|
||||
connectionLineStyle={{
|
||||
stroke: '#b1b1b7',
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
defaultEdgeOptions={{
|
||||
type: 'smoothstep',
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
animated: true,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 15,
|
||||
height: 15,
|
||||
color: '#b1b1b7',
|
||||
},
|
||||
}}
|
||||
nodeTypes={{
|
||||
// 注册新的节点类型
|
||||
start: NewStartNode,
|
||||
apply: NewApplyNode,
|
||||
branch: NewBranchNode,
|
||||
deploy: NewDeployNode,
|
||||
upload: NewUploadNode,
|
||||
notify: NewNotifyNode,
|
||||
}}
|
||||
snapToGrid={true}
|
||||
snapGrid={[10, 10]}
|
||||
elevateEdgesOnSelect
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
elementsSelectable={true}
|
||||
panOnScroll
|
||||
zoomOnScroll={false}
|
||||
>
|
||||
<Background gap={20} size={1} color="#e5e5e5" variant="dots" />
|
||||
<MiniMap zoomable pannable nodeStrokeWidth={3} maskColor="rgba(240, 240, 240, 0.6)" nodeBorderRadius={2} />
|
||||
<Controls showInteractive={true} />
|
||||
|
||||
{/* 节点工具栏 */}
|
||||
<NodeToolbar
|
||||
nodeId={selectedNodeId.value || ''}
|
||||
position={Position.Top}
|
||||
offset={10}
|
||||
isVisible={!!selectedNodeId.value}
|
||||
>
|
||||
<div class="node-toolbar">
|
||||
<button
|
||||
class="toolbar-btn delete-btn"
|
||||
onClick={() => deleteNode(selectedNodeId.value || '')}
|
||||
title="删除节点"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
<button
|
||||
class="toolbar-btn duplicate-btn"
|
||||
onClick={() => duplicateNode(selectedNodeId.value || '')}
|
||||
title="复制节点"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</NodeToolbar>
|
||||
</VueFlow>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
140
frontend/apps/vue-flow/components/WorkflowNodeConfig.module.css
Normal file
140
frontend/apps/vue-flow/components/WorkflowNodeConfig.module.css
Normal file
@@ -0,0 +1,140 @@
|
||||
.nodeConfig {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fcfcfc;
|
||||
border-left: 1px solid #e8e8e8;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nodeConfigHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.nodeConfigTitle {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.saveConfigButton {
|
||||
padding: 4px 12px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.saveConfigButton:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
.nodeConfigContent {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.emptyConfig {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.noConfig {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.configField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.configLabel {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.configValue {
|
||||
font-size: 14px;
|
||||
padding: 6px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.configInput {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.configInput:focus {
|
||||
border-color: #4096ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.configTextarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.configTextarea:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
|
||||
}
|
||||
|
||||
.configSelect {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.configButton {
|
||||
padding: 8px 16px;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.configButton:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
62
frontend/apps/vue-flow/components/WorkflowNodeConfig.tsx
Normal file
62
frontend/apps/vue-flow/components/WorkflowNodeConfig.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { useWorkflowStore } from '../store/workflow'
|
||||
import styles from './WorkflowNodeConfig.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WorkflowNodeConfig',
|
||||
setup() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
return () => {
|
||||
const selectedNode = workflowStore.selectedNode
|
||||
|
||||
// 如果没有选中节点,显示默认内容
|
||||
if (!selectedNode) {
|
||||
return (
|
||||
<div class={styles.nodeConfig}>
|
||||
<div class={styles.emptyConfig}>
|
||||
<div class={styles.emptyIcon}>🔍</div>
|
||||
<div class={styles.emptyText}>请选择一个节点进行配置</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 渲染节点配置内容
|
||||
return (
|
||||
<div class={styles.nodeConfig}>
|
||||
<div class={styles.nodeConfigHeader}>
|
||||
<div class={styles.nodeConfigTitle}>{selectedNode.data?.label || '未命名节点'}</div>
|
||||
</div>
|
||||
<div class={styles.nodeConfigContent}>
|
||||
<div class={styles.configField}>
|
||||
<div class={styles.configLabel}>节点ID</div>
|
||||
<div class={styles.configValue}>{selectedNode.id}</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.configField}>
|
||||
<div class={styles.configLabel}>节点类型</div>
|
||||
<div class={styles.configValue}>{selectedNode.type}</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.configField}>
|
||||
<div class={styles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedNode.data?.label || ''}
|
||||
onInput={(e) => {
|
||||
if (selectedNode.data) {
|
||||
workflowStore.updateNodeData(selectedNode.id, {
|
||||
label: (e.target as HTMLInputElement).value,
|
||||
})
|
||||
}
|
||||
}}
|
||||
class={styles.configInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useWorkflowStore } from '../../store/workflow'
|
||||
import { ApplyNodeData } from '../../types'
|
||||
import configStyles from './Config.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ApplyNodeConfig',
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodeData: {
|
||||
type: Object as () => ApplyNodeData,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const applicationContent = ref(props.nodeData.applicationContent || '')
|
||||
const applyStatus = ref<'idle' | 'applying' | 'success' | 'error'>('idle')
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 更新节点标签
|
||||
const updateNodeLabel = (value: string) => {
|
||||
workflowStore.updateNodeData(props.nodeId, { label: value })
|
||||
}
|
||||
|
||||
// 更新申请内容
|
||||
const updateApplicationContent = (value: string) => {
|
||||
applicationContent.value = value
|
||||
}
|
||||
|
||||
// 提交申请
|
||||
const submitApplication = () => {
|
||||
if (!applicationContent.value.trim()) {
|
||||
errorMessage.value = '请输入申请内容'
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟申请过程
|
||||
applyStatus.value = 'applying'
|
||||
errorMessage.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
applyStatus.value = 'success'
|
||||
workflowStore.updateNodeData(props.nodeId, {
|
||||
applicationContent: applicationContent.value,
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={configStyles.configContainer}>
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.nodeData.label}
|
||||
onInput={(e) => updateNodeLabel((e.target as HTMLInputElement).value)}
|
||||
class={configStyles.configInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>申请内容</div>
|
||||
<textarea
|
||||
value={applicationContent.value}
|
||||
onInput={(e) => updateApplicationContent((e.target as HTMLTextAreaElement).value)}
|
||||
class={configStyles.configTextarea}
|
||||
placeholder="请输入申请内容"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{errorMessage.value && <div class={configStyles.configError}>{errorMessage.value}</div>}
|
||||
|
||||
<div class={configStyles.configActions}>
|
||||
<button
|
||||
class={configStyles.configButton}
|
||||
onClick={submitApplication}
|
||||
disabled={applyStatus.value === 'applying'}
|
||||
>
|
||||
{applyStatus.value === 'applying' ? '申请中...' : '提交申请'}
|
||||
</button>
|
||||
|
||||
{applyStatus.value === 'success' && <div class={configStyles.configSuccess}>申请提交成功</div>}
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configInfo}>
|
||||
<div class={configStyles.configInfoTitle}>节点说明</div>
|
||||
<div class={configStyles.configInfoContent}>申请节点用于提交申请。申请成功后,申请内容将保存到节点中。</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
126
frontend/apps/vue-flow/components/configs/Config.module.css
Normal file
126
frontend/apps/vue-flow/components/configs/Config.module.css
Normal file
@@ -0,0 +1,126 @@
|
||||
.configContainer {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.configField {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.configLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.configInput {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.configInput:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
|
||||
}
|
||||
|
||||
.configFileInput {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.configTextarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.configTextarea:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
|
||||
}
|
||||
|
||||
.configSelect {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.configSelect:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
|
||||
}
|
||||
|
||||
.configInfo {
|
||||
margin-top: 20px;
|
||||
background-color: #f3f4f6;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.configInfoTitle {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.configInfoContent {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.configError {
|
||||
margin-bottom: 16px;
|
||||
padding: 8px;
|
||||
background-color: #fee2e2;
|
||||
border-radius: 4px;
|
||||
color: #b91c1c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.configSuccess {
|
||||
margin-left: 12px;
|
||||
color: #047857;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.configActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.configButton {
|
||||
padding: 8px 16px;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.configButton:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.configButton:disabled {
|
||||
background-color: #93c5fd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useWorkflowStore } from '../../store/workflow'
|
||||
import { DeployNodeData } from '../../types'
|
||||
import configStyles from './Config.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DeployNodeConfig',
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodeData: {
|
||||
type: Object as () => DeployNodeData,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const certificateContent = ref(props.nodeData.certificateContent || '')
|
||||
const deployStatus = ref<'idle' | 'deploying' | 'success' | 'error'>('idle')
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 更新节点标签
|
||||
const updateNodeLabel = (value: string) => {
|
||||
workflowStore.updateNodeData(props.nodeId, { label: value })
|
||||
}
|
||||
|
||||
// 更新证书内容
|
||||
const updateCertificateContent = (value: string) => {
|
||||
certificateContent.value = value
|
||||
}
|
||||
|
||||
// 部署证书
|
||||
const deployCertificate = () => {
|
||||
if (!certificateContent.value.trim()) {
|
||||
errorMessage.value = '请输入证书内容'
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟部署过程
|
||||
deployStatus.value = 'deploying'
|
||||
errorMessage.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
deployStatus.value = 'success'
|
||||
workflowStore.updateNodeData(props.nodeId, {
|
||||
certificateContent: certificateContent.value,
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={configStyles.configContainer}>
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.nodeData.label}
|
||||
onInput={(e) => updateNodeLabel((e.target as HTMLInputElement).value)}
|
||||
class={configStyles.configInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>证书内容</div>
|
||||
<textarea
|
||||
value={certificateContent.value}
|
||||
onInput={(e) => updateCertificateContent((e.target as HTMLTextAreaElement).value)}
|
||||
class={configStyles.configTextarea}
|
||||
placeholder="请输入要部署的证书内容"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{errorMessage.value && <div class={configStyles.configError}>{errorMessage.value}</div>}
|
||||
|
||||
<div class={configStyles.configActions}>
|
||||
<button
|
||||
class={configStyles.configButton}
|
||||
onClick={deployCertificate}
|
||||
disabled={deployStatus.value === 'deploying'}
|
||||
>
|
||||
{deployStatus.value === 'deploying' ? '部署中...' : '部署证书'}
|
||||
</button>
|
||||
|
||||
{deployStatus.value === 'success' && <div class={configStyles.configSuccess}>部署成功</div>}
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configInfo}>
|
||||
<div class={configStyles.configInfoTitle}>节点说明</div>
|
||||
<div class={configStyles.configInfoContent}>
|
||||
部署证书节点用于部署证书。部署成功后,证书内容将保存到节点中。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
45
frontend/apps/vue-flow/components/configs/EndNodeConfig.tsx
Normal file
45
frontend/apps/vue-flow/components/configs/EndNodeConfig.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { useWorkflowStore } from '../../store/workflow'
|
||||
import { EndNodeData } from '../../types'
|
||||
import configStyles from './Config.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EndNodeConfig',
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodeData: {
|
||||
type: Object as () => EndNodeData,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const updateNodeLabel = (value: string) => {
|
||||
workflowStore.updateNodeData(props.nodeId, { label: value })
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={configStyles.configContainer}>
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.nodeData.label}
|
||||
onInput={(e) => updateNodeLabel((e.target as HTMLInputElement).value)}
|
||||
class={configStyles.configInput}
|
||||
/>
|
||||
</div>
|
||||
<div class={configStyles.configInfo}>
|
||||
<div class={configStyles.configInfoTitle}>节点说明</div>
|
||||
<div class={configStyles.configInfoContent}>
|
||||
结束节点作为工作流的终点,不可移动,不可删除,不可更改节点类型,只能有一个入口。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useWorkflowStore } from '../../store/workflow'
|
||||
import { NormalNodeData } from '../../types'
|
||||
import configStyles from './Config.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NormalNodeConfig',
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodeData: {
|
||||
type: Object as () => NormalNodeData,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const message = ref(props.nodeData.message || '')
|
||||
const status = ref(props.nodeData.status || 'info')
|
||||
|
||||
// 更新节点标签
|
||||
const updateNodeLabel = (value: string) => {
|
||||
workflowStore.updateNodeData(props.nodeId, { label: value })
|
||||
}
|
||||
|
||||
// 更新消息内容
|
||||
const updateMessage = (value: string) => {
|
||||
message.value = value
|
||||
workflowStore.updateNodeData(props.nodeId, { message: value })
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const updateStatus = (value: string) => {
|
||||
status.value = value as 'success' | 'error' | 'info'
|
||||
workflowStore.updateNodeData(props.nodeId, { status: value as 'success' | 'error' | 'info' })
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={configStyles.configContainer}>
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.nodeData.label}
|
||||
onInput={(e) => updateNodeLabel((e.target as HTMLInputElement).value)}
|
||||
class={configStyles.configInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>状态</div>
|
||||
<select
|
||||
value={status.value}
|
||||
onChange={(e) => updateStatus((e.target as HTMLSelectElement).value)}
|
||||
class={configStyles.configSelect}
|
||||
>
|
||||
<option value="info">信息</option>
|
||||
<option value="success">成功</option>
|
||||
<option value="error">失败</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>消息内容</div>
|
||||
<textarea
|
||||
value={message.value}
|
||||
onInput={(e) => updateMessage((e.target as HTMLTextAreaElement).value)}
|
||||
class={configStyles.configTextarea}
|
||||
placeholder="请输入消息内容"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configInfo}>
|
||||
<div class={configStyles.configInfoTitle}>节点说明</div>
|
||||
<div class={configStyles.configInfoContent}>普通节点用于显示文本或者成功/失败提示。</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useWorkflowStore } from '../../store/workflow'
|
||||
import { NotifyNodeData } from '../../types'
|
||||
import configStyles from './Config.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NotifyNodeConfig',
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodeData: {
|
||||
type: Object as () => NotifyNodeData,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const message = ref(props.nodeData.message || '')
|
||||
const notifyType = ref(props.nodeData.notifyType || 'email')
|
||||
|
||||
// 更新节点标签
|
||||
const updateNodeLabel = (value: string) => {
|
||||
workflowStore.updateNodeData(props.nodeId, { label: value })
|
||||
}
|
||||
|
||||
// 更新通知消息
|
||||
const updateMessage = (value: string) => {
|
||||
message.value = value
|
||||
workflowStore.updateNodeData(props.nodeId, { message: value })
|
||||
}
|
||||
|
||||
// 更新通知类型
|
||||
const updateNotifyType = (value: string) => {
|
||||
notifyType.value = value
|
||||
workflowStore.updateNodeData(props.nodeId, { notifyType: value })
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={configStyles.configContainer}>
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.nodeData.label}
|
||||
onInput={(e) => updateNodeLabel((e.target as HTMLInputElement).value)}
|
||||
class={configStyles.configInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>通知类型</div>
|
||||
<select
|
||||
value={notifyType.value}
|
||||
onChange={(e) => updateNotifyType((e.target as HTMLSelectElement).value)}
|
||||
class={configStyles.configSelect}
|
||||
>
|
||||
<option value="email">邮件通知</option>
|
||||
<option value="sms">短信通知</option>
|
||||
<option value="wechat">微信通知</option>
|
||||
<option value="dingding">钉钉通知</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>通知内容</div>
|
||||
<textarea
|
||||
value={message.value}
|
||||
onInput={(e) => updateMessage((e.target as HTMLTextAreaElement).value)}
|
||||
class={configStyles.configTextarea}
|
||||
placeholder="请输入通知内容"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configInfo}>
|
||||
<div class={configStyles.configInfoTitle}>节点说明</div>
|
||||
<div class={configStyles.configInfoContent}>
|
||||
通知节点具备两个子节点,一个为成功节点,一个为失败节点,成功节点和失败节点各具备一个出口。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { useWorkflowStore } from '../../store/workflow'
|
||||
import { StartNodeData } from '../../types'
|
||||
import configStyles from './Config.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StartNodeConfig',
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodeData: {
|
||||
type: Object as () => StartNodeData,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const updateNodeLabel = (value: string) => {
|
||||
workflowStore.updateNodeData(props.nodeId, { label: value })
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={configStyles.configContainer}>
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.nodeData.label}
|
||||
onInput={(e) => updateNodeLabel((e.target as HTMLInputElement).value)}
|
||||
class={configStyles.configInput}
|
||||
/>
|
||||
</div>
|
||||
<div class={configStyles.configInfo}>
|
||||
<div class={configStyles.configInfoTitle}>节点说明</div>
|
||||
<div class={configStyles.configInfoContent}>
|
||||
开始节点作为工作流的起点,不可移动,不可删除,不可更改节点类型,只能有一个出口。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
123
frontend/apps/vue-flow/components/configs/UploadNodeConfig.tsx
Normal file
123
frontend/apps/vue-flow/components/configs/UploadNodeConfig.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useWorkflowStore } from '../../store/workflow'
|
||||
import { UploadNodeData } from '../../types'
|
||||
import configStyles from './Config.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UploadNodeConfig',
|
||||
props: {
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodeData: {
|
||||
type: Object as () => UploadNodeData,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const certificateContent = ref(props.nodeData.certificateContent || '')
|
||||
const uploadStatus = ref<'idle' | 'uploading' | 'success' | 'error'>('idle')
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 更新节点标签
|
||||
const updateNodeLabel = (value: string) => {
|
||||
workflowStore.updateNodeData(props.nodeId, { label: value })
|
||||
}
|
||||
|
||||
// 更新证书内容
|
||||
const updateCertificateContent = (value: string) => {
|
||||
certificateContent.value = value
|
||||
}
|
||||
|
||||
// 上传证书
|
||||
const uploadCertificate = () => {
|
||||
if (!certificateContent.value.trim()) {
|
||||
errorMessage.value = '请输入证书内容'
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟上传过程
|
||||
uploadStatus.value = 'uploading'
|
||||
errorMessage.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
uploadStatus.value = 'success'
|
||||
workflowStore.updateNodeData(props.nodeId, {
|
||||
certificateContent: certificateContent.value,
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const fileInput = event.target as HTMLInputElement
|
||||
const file = fileInput.files?.[0]
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string
|
||||
certificateContent.value = content
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={configStyles.configContainer}>
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>节点名称</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.nodeData.label}
|
||||
onInput={(e) => updateNodeLabel((e.target as HTMLInputElement).value)}
|
||||
class={configStyles.configInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>上传证书文件</div>
|
||||
<input
|
||||
type="file"
|
||||
class={configStyles.configFileInput}
|
||||
onChange={handleFileUpload}
|
||||
accept=".pem,.crt,.key,.cert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configField}>
|
||||
<div class={configStyles.configLabel}>或粘贴证书内容</div>
|
||||
<textarea
|
||||
value={certificateContent.value}
|
||||
onInput={(e) => updateCertificateContent((e.target as HTMLTextAreaElement).value)}
|
||||
class={configStyles.configTextarea}
|
||||
placeholder="请粘贴证书内容"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{errorMessage.value && <div class={configStyles.configError}>{errorMessage.value}</div>}
|
||||
|
||||
<div class={configStyles.configActions}>
|
||||
<button
|
||||
class={configStyles.configButton}
|
||||
onClick={uploadCertificate}
|
||||
disabled={uploadStatus.value === 'uploading'}
|
||||
>
|
||||
{uploadStatus.value === 'uploading' ? '上传中...' : '上传证书'}
|
||||
</button>
|
||||
|
||||
{uploadStatus.value === 'success' && <div class={configStyles.configSuccess}>上传成功</div>}
|
||||
</div>
|
||||
|
||||
<div class={configStyles.configInfo}>
|
||||
<div class={configStyles.configInfoTitle}>节点说明</div>
|
||||
<div class={configStyles.configInfoContent}>
|
||||
上传证书节点用于上传证书文件或粘贴证书内容。上传成功后,证书内容将保存到节点中。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
25
frontend/apps/vue-flow/components/nodes/ApplyNode.tsx
Normal file
25
frontend/apps/vue-flow/components/nodes/ApplyNode.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ApplyNode',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<div class={`${styles.node} ${styles.applyNode}`}>
|
||||
<div class={styles.nodeContent}>
|
||||
<div class={styles.nodeIcon}>📝</div>
|
||||
<div class={styles.nodeLabel}>{props.data.label}</div>
|
||||
</div>
|
||||
<Handle id="target" type="target" position={Position.Top} class={styles.handle} />
|
||||
<Handle id="source" type="source" position={Position.Bottom} class={styles.handle} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,192 @@
|
||||
import { defineComponent, ref, computed } from 'vue'
|
||||
import { Handle, Position, useVueFlow, NodeProps } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
import { NodeData, ICON_MAP, NodeType } from '../../types'
|
||||
|
||||
// 节点类型定义
|
||||
export const NODE_TYPES = {
|
||||
APPLY: 'apply',
|
||||
BRANCH: 'branch',
|
||||
DEPLOY: 'deploy',
|
||||
UPLOAD: 'upload',
|
||||
NOTIFY: 'notify',
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseNodeWithAddButton',
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
data: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
connectable: { type: Boolean, default: true },
|
||||
nodeClassName: { type: String, default: '' },
|
||||
icon: { type: String, default: '' },
|
||||
isClickable: { type: Boolean, default: false },
|
||||
onClick: { type: Function, default: () => {} },
|
||||
},
|
||||
emits: ['add-node'],
|
||||
setup(props, { slots, emit }) {
|
||||
const showMenu = ref(false)
|
||||
const vueFlowInstance = useVueFlow()
|
||||
|
||||
// 获取节点图标
|
||||
const nodeIcon = computed(() => {
|
||||
const nodeType = props.data.type as NodeType
|
||||
return props.icon || props.data.icon || ICON_MAP[nodeType] || '📄'
|
||||
})
|
||||
|
||||
// 处理添加按钮点击
|
||||
const handleAddClick = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
showMenu.value = !showMenu.value
|
||||
}
|
||||
|
||||
// 处理节点点击
|
||||
const handleNodeClick = (e: MouseEvent) => {
|
||||
if (props.isClickable) {
|
||||
e.stopPropagation()
|
||||
props.onClick(props.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新节点
|
||||
const addNewNode = (type: string) => {
|
||||
// 获取当前节点信息
|
||||
const currentNode = vueFlowInstance.findNode(props.id)
|
||||
if (!currentNode) return
|
||||
|
||||
// 创建新节点ID
|
||||
const newNodeId = `${type}-${Date.now()}`
|
||||
|
||||
// 根据不同类型节点设置不同图标和标签
|
||||
let nodeLabel = '新节点'
|
||||
|
||||
switch (type) {
|
||||
case NODE_TYPES.APPLY:
|
||||
nodeLabel = '申请证书'
|
||||
break
|
||||
case NODE_TYPES.BRANCH:
|
||||
nodeLabel = '分支节点'
|
||||
break
|
||||
case NODE_TYPES.DEPLOY:
|
||||
nodeLabel = '部署证书'
|
||||
break
|
||||
case NODE_TYPES.UPLOAD:
|
||||
nodeLabel = '上传证书'
|
||||
break
|
||||
case NODE_TYPES.NOTIFY:
|
||||
nodeLabel = '通知'
|
||||
break
|
||||
}
|
||||
|
||||
// 计算新节点位置
|
||||
const nodeHeight = 120 // 节点固定高度
|
||||
const verticalGap = 150 // 节点之间的垂直间距
|
||||
const newY = currentNode.position.y + nodeHeight + verticalGap
|
||||
|
||||
// 创建新节点
|
||||
const newNode = {
|
||||
id: newNodeId,
|
||||
type: type,
|
||||
position: {
|
||||
x: currentNode.position.x,
|
||||
y: newY,
|
||||
},
|
||||
data: {
|
||||
id: newNodeId,
|
||||
type: type as NodeType,
|
||||
label: nodeLabel,
|
||||
icon: ICON_MAP[type as NodeType],
|
||||
canMove: false,
|
||||
canDelete: true,
|
||||
canChangeType: true,
|
||||
},
|
||||
}
|
||||
|
||||
// 创建连接边
|
||||
const newEdge = {
|
||||
id: `${props.id}-${newNodeId}`,
|
||||
source: props.id,
|
||||
target: newNodeId,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
style: { strokeWidth: 2 },
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'top',
|
||||
}
|
||||
|
||||
// 添加节点和边
|
||||
vueFlowInstance.addNodes([newNode])
|
||||
vueFlowInstance.addEdges([newEdge])
|
||||
|
||||
// 关闭菜单
|
||||
showMenu.value = false
|
||||
|
||||
// 触发添加节点事件
|
||||
emit('add-node', { nodeId: newNodeId, nodeType: type })
|
||||
}
|
||||
|
||||
// 处理点击外部关闭菜单
|
||||
const handleOutsideClick = () => {
|
||||
showMenu.value = false
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div
|
||||
class={`
|
||||
${styles.node}
|
||||
${props.nodeClassName}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
class={`
|
||||
${styles.nodeBody}
|
||||
${props.selected ? styles.nodeSelected : ''}
|
||||
${props.isClickable ? styles.nodeClickable : ''}
|
||||
`}
|
||||
onClick={handleNodeClick}
|
||||
>
|
||||
<div class={styles.nodeHeader}>
|
||||
<span class={styles.nodeIcon}>{nodeIcon.value}</span>
|
||||
<span class={styles.nodeLabel}>{props.data.label}</span>
|
||||
</div>
|
||||
|
||||
<div class={styles.nodeBody}>{slots.default && slots.default()}</div>
|
||||
|
||||
{/* 添加节点按钮 */}
|
||||
<div class={styles.addNodeBtn} onClick={handleAddClick}>
|
||||
+
|
||||
</div>
|
||||
|
||||
{/* 菜单选项 - 使用 v-show 控制显示 */}
|
||||
<div class={styles.nodeMenu} style={{ display: showMenu.value ? 'flex' : 'none', zIndex: 99999 }}>
|
||||
<div class={styles.menuItem} onClick={() => addNewNode(NODE_TYPES.APPLY)}>
|
||||
<span class={styles.menuItemIcon}>{ICON_MAP['apply' as NodeType]}</span>
|
||||
<span class={styles.menuItemLabel}>申请证书节点</span>
|
||||
</div>
|
||||
<div class={styles.menuItem} onClick={() => addNewNode(NODE_TYPES.DEPLOY)}>
|
||||
<span class={styles.menuItemIcon}>{ICON_MAP['deploy' as NodeType]}</span>
|
||||
<span class={styles.menuItemLabel}>部署证书节点</span>
|
||||
</div>
|
||||
<div class={styles.menuItem} onClick={() => addNewNode(NODE_TYPES.UPLOAD)}>
|
||||
<span class={styles.menuItemIcon}>{ICON_MAP['upload' as NodeType]}</span>
|
||||
<span class={styles.menuItemLabel}>上传证书节点</span>
|
||||
</div>
|
||||
<div class={styles.menuItem} onClick={() => addNewNode(NODE_TYPES.NOTIFY)}>
|
||||
<span class={styles.menuItemIcon}>{ICON_MAP['notify' as NodeType]}</span>
|
||||
<span class={styles.menuItemLabel}>通知节点</span>
|
||||
</div>
|
||||
<div class={styles.menuItem} onClick={() => addNewNode(NODE_TYPES.BRANCH)}>
|
||||
<span class={styles.menuItemIcon}>{ICON_MAP['branch' as NodeType]}</span>
|
||||
<span class={styles.menuItemLabel}>分支节点</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 连接点 */}
|
||||
<Handle id="target" type="target" position={Position.Top} class={styles.handle} />
|
||||
<Handle id="bottom" type="source" position={Position.Bottom} class={styles.handle} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
25
frontend/apps/vue-flow/components/nodes/DeployNode.tsx
Normal file
25
frontend/apps/vue-flow/components/nodes/DeployNode.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DeployNode',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<div class={`${styles.node} ${styles.deployNode}`}>
|
||||
<div class={styles.nodeContent}>
|
||||
<div class={styles.nodeIcon}>🚀</div>
|
||||
<div class={styles.nodeLabel}>{props.data.label}</div>
|
||||
</div>
|
||||
<Handle id="target" type="target" position={Position.Top} class={styles.handle} />
|
||||
<Handle id="source" type="source" position={Position.Bottom} class={styles.handle} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
25
frontend/apps/vue-flow/components/nodes/EndNode.tsx
Normal file
25
frontend/apps/vue-flow/components/nodes/EndNode.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EndNode',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<div class={`${styles.node} ${styles.endNode}`}>
|
||||
<div class={styles.nodeContent}>
|
||||
<div class={styles.nodeIcon}>🏁</div>
|
||||
<div class={styles.nodeLabel}>{props.data.label}</div>
|
||||
</div>
|
||||
{/* 只有入口,没有出口 */}
|
||||
<Handle id="target" type="target" position={Position.Top} class={styles.handle} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
55
frontend/apps/vue-flow/components/nodes/NewApplyNode.tsx
Normal file
55
frontend/apps/vue-flow/components/nodes/NewApplyNode.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import BaseNodeWithAddButton from './BaseNodeWithAddButton'
|
||||
import styles from './Node.module.css'
|
||||
import { NodeProps } from '@vue-flow/core'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewApplyNode',
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
data: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
connectable: { type: Boolean, default: true },
|
||||
},
|
||||
setup(props) {
|
||||
const handleNodeClick = (nodeData: any) => {
|
||||
// 在这里可以打开节点配置面板
|
||||
console.log('配置申请证书节点', nodeData)
|
||||
// 触发事件通知父组件打开配置面板
|
||||
const event = new CustomEvent('open-node-config', {
|
||||
detail: {
|
||||
nodeId: nodeData.id,
|
||||
nodeType: 'apply',
|
||||
nodeData: nodeData,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
const handleAddNode = (data: any) => {
|
||||
console.log('添加节点成功', data)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<BaseNodeWithAddButton
|
||||
id={props.id}
|
||||
data={props.data}
|
||||
selected={props.selected}
|
||||
connectable={props.connectable}
|
||||
nodeClassName={styles.applyNode}
|
||||
icon="📝"
|
||||
isClickable={true}
|
||||
onClick={handleNodeClick}
|
||||
onAdd-node={handleAddNode}
|
||||
>
|
||||
{/* 节点内容部分可以在这里添加 */}
|
||||
{props.data.applicationContent && (
|
||||
<div class={styles.nodeMessage}>
|
||||
{props.data.applicationContent.substring(0, 20)}
|
||||
{props.data.applicationContent.length > 20 ? '...' : ''}
|
||||
</div>
|
||||
)}
|
||||
</BaseNodeWithAddButton>
|
||||
)
|
||||
},
|
||||
})
|
||||
50
frontend/apps/vue-flow/components/nodes/NewBranchNode.tsx
Normal file
50
frontend/apps/vue-flow/components/nodes/NewBranchNode.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import BaseNodeWithAddButton from './BaseNodeWithAddButton'
|
||||
import styles from './Node.module.css'
|
||||
import { NodeProps } from '@vue-flow/core'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewBranchNode',
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
data: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
connectable: { type: Boolean, default: true },
|
||||
},
|
||||
setup(props) {
|
||||
const handleNodeClick = (nodeData: any) => {
|
||||
// 在这里可以打开节点配置面板
|
||||
console.log('配置分支节点', nodeData)
|
||||
// 触发事件通知父组件打开配置面板
|
||||
const event = new CustomEvent('open-node-config', {
|
||||
detail: {
|
||||
nodeId: nodeData.id,
|
||||
nodeType: 'branch',
|
||||
nodeData: nodeData,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
const handleAddNode = (data: any) => {
|
||||
console.log('添加节点成功', data)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<BaseNodeWithAddButton
|
||||
id={props.id}
|
||||
data={props.data}
|
||||
selected={props.selected}
|
||||
connectable={props.connectable}
|
||||
nodeClassName={styles.branchNode}
|
||||
icon="🔀"
|
||||
isClickable={true}
|
||||
onClick={handleNodeClick}
|
||||
onAdd-node={handleAddNode}
|
||||
>
|
||||
{/* 分支节点可以显示分支数量 */}
|
||||
{props.data.branchCount && <div class={styles.nodeMessage}>分支数: {props.data.branchCount}</div>}
|
||||
</BaseNodeWithAddButton>
|
||||
)
|
||||
},
|
||||
})
|
||||
50
frontend/apps/vue-flow/components/nodes/NewDeployNode.tsx
Normal file
50
frontend/apps/vue-flow/components/nodes/NewDeployNode.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import BaseNodeWithAddButton from './BaseNodeWithAddButton'
|
||||
import styles from './Node.module.css'
|
||||
import { NodeProps } from '@vue-flow/core'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewDeployNode',
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
data: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
connectable: { type: Boolean, default: true },
|
||||
},
|
||||
setup(props) {
|
||||
const handleNodeClick = (nodeData: any) => {
|
||||
// 在这里可以打开节点配置面板
|
||||
console.log('配置部署证书节点', nodeData)
|
||||
// 触发事件通知父组件打开配置面板
|
||||
const event = new CustomEvent('open-node-config', {
|
||||
detail: {
|
||||
nodeId: nodeData.id,
|
||||
nodeType: 'deploy',
|
||||
nodeData: nodeData,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
const handleAddNode = (data: any) => {
|
||||
console.log('添加节点成功', data)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<BaseNodeWithAddButton
|
||||
id={props.id}
|
||||
data={props.data}
|
||||
selected={props.selected}
|
||||
connectable={props.connectable}
|
||||
nodeClassName={styles.deployNode}
|
||||
icon="🚀"
|
||||
isClickable={true}
|
||||
onClick={handleNodeClick}
|
||||
onAdd-node={handleAddNode}
|
||||
>
|
||||
{/* 部署节点可以显示部署目标 */}
|
||||
{props.data.deployTarget && <div class={styles.nodeMessage}>部署到: {props.data.deployTarget}</div>}
|
||||
</BaseNodeWithAddButton>
|
||||
)
|
||||
},
|
||||
})
|
||||
70
frontend/apps/vue-flow/components/nodes/NewNotifyNode.tsx
Normal file
70
frontend/apps/vue-flow/components/nodes/NewNotifyNode.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import BaseNodeWithAddButton from './BaseNodeWithAddButton'
|
||||
import styles from './Node.module.css'
|
||||
import { NodeProps } from '@vue-flow/core'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewNotifyNode',
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
data: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
connectable: { type: Boolean, default: true },
|
||||
},
|
||||
setup(props) {
|
||||
const handleNodeClick = (nodeData: any) => {
|
||||
// 在这里可以打开节点配置面板
|
||||
console.log('配置通知节点', nodeData)
|
||||
// 触发事件通知父组件打开配置面板
|
||||
const event = new CustomEvent('open-node-config', {
|
||||
detail: {
|
||||
nodeId: nodeData.id,
|
||||
nodeType: 'notify',
|
||||
nodeData: nodeData,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
const handleAddNode = (data: any) => {
|
||||
console.log('添加节点成功', data)
|
||||
}
|
||||
|
||||
// 获取通知类型显示文本
|
||||
const getNotifyTypeText = () => {
|
||||
switch (props.data.notifyType) {
|
||||
case 'email':
|
||||
return '邮件通知'
|
||||
case 'sms':
|
||||
return '短信通知'
|
||||
case 'webhook':
|
||||
return 'Webhook通知'
|
||||
default:
|
||||
return props.data.notifyType || '默认通知'
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<BaseNodeWithAddButton
|
||||
id={props.id}
|
||||
data={props.data}
|
||||
selected={props.selected}
|
||||
connectable={props.connectable}
|
||||
nodeClassName={styles.notifyNode}
|
||||
icon="📣"
|
||||
isClickable={true}
|
||||
onClick={handleNodeClick}
|
||||
onAdd-node={handleAddNode}
|
||||
>
|
||||
{/* 通知节点可以显示通知类型和内容 */}
|
||||
{props.data.notifyType && <div class={styles.nodeMessage}>类型: {getNotifyTypeText()}</div>}
|
||||
{props.data.message && (
|
||||
<div class={styles.nodeMessage}>
|
||||
{props.data.message.substring(0, 20)}
|
||||
{props.data.message.length > 20 ? '...' : ''}
|
||||
</div>
|
||||
)}
|
||||
</BaseNodeWithAddButton>
|
||||
)
|
||||
},
|
||||
})
|
||||
34
frontend/apps/vue-flow/components/nodes/NewStartNode.tsx
Normal file
34
frontend/apps/vue-flow/components/nodes/NewStartNode.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { NodeProps } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
import BaseNodeWithAddButton from './BaseNodeWithAddButton'
|
||||
import { NodeType } from '../../types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewStartNode',
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
data: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
connectable: { type: Boolean, default: true },
|
||||
},
|
||||
emits: ['add-node'],
|
||||
setup(props, { emit }) {
|
||||
// 处理添加节点事件
|
||||
const handleAddNode = (nodeData: any) => {
|
||||
console.log('添加节点成功:', nodeData)
|
||||
emit('add-node', nodeData)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<BaseNodeWithAddButton
|
||||
id={props.id}
|
||||
data={props.data}
|
||||
selected={props.selected}
|
||||
connectable={props.connectable}
|
||||
nodeClassName={styles.startNode}
|
||||
onAdd-node={handleAddNode}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
55
frontend/apps/vue-flow/components/nodes/NewUploadNode.tsx
Normal file
55
frontend/apps/vue-flow/components/nodes/NewUploadNode.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import BaseNodeWithAddButton from './BaseNodeWithAddButton'
|
||||
import styles from './Node.module.css'
|
||||
import { NodeProps } from '@vue-flow/core'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewUploadNode',
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
data: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
connectable: { type: Boolean, default: true },
|
||||
},
|
||||
setup(props) {
|
||||
const handleNodeClick = (nodeData: any) => {
|
||||
// 在这里可以打开节点配置面板
|
||||
console.log('配置上传证书节点', nodeData)
|
||||
// 触发事件通知父组件打开配置面板
|
||||
const event = new CustomEvent('open-node-config', {
|
||||
detail: {
|
||||
nodeId: nodeData.id,
|
||||
nodeType: 'upload',
|
||||
nodeData: nodeData,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
const handleAddNode = (data: any) => {
|
||||
console.log('添加节点成功', data)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<BaseNodeWithAddButton
|
||||
id={props.id}
|
||||
data={props.data}
|
||||
selected={props.selected}
|
||||
connectable={props.connectable}
|
||||
nodeClassName={styles.uploadNode}
|
||||
icon="📤"
|
||||
isClickable={true}
|
||||
onClick={handleNodeClick}
|
||||
onAdd-node={handleAddNode}
|
||||
>
|
||||
{/* 上传节点可以显示文件状态 */}
|
||||
{props.data.certificateFile && (
|
||||
<div class={styles.nodeMessage}>已上传: {props.data.certificateFile.name || '证书文件'}</div>
|
||||
)}
|
||||
{!props.data.certificateFile && props.data.certificateContent && (
|
||||
<div class={styles.nodeMessage}>已填写证书内容</div>
|
||||
)}
|
||||
</BaseNodeWithAddButton>
|
||||
)
|
||||
},
|
||||
})
|
||||
230
frontend/apps/vue-flow/components/nodes/Node.module.css
Normal file
230
frontend/apps/vue-flow/components/nodes/Node.module.css
Normal file
@@ -0,0 +1,230 @@
|
||||
.node {
|
||||
border-radius: 5px;
|
||||
width: 240px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.nodeHeader {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 10px;
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nodeLabel {
|
||||
font-weight: 500;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nodeBody {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
color: #5a5e66;
|
||||
box-sizing: border-box;
|
||||
min-height: 60px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nodeMessage {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #64748b;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.handle-top {
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.handle-bottom {
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
/* 添加节点按钮 */
|
||||
.addNodeBtn {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: #fff;
|
||||
border: 2px solid #1e83e9;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
color: #1e83e9;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.addNodeBtn:hover {
|
||||
background-color: #1e83e9;
|
||||
color: white;
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
/* 添加节点选项菜单 */
|
||||
.nodeMenu {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 180px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.menuItem:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.menuItemIcon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menuItemLabel {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 节点交互样式 */
|
||||
.nodeClickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nodeClickable:hover::after {
|
||||
content: '点击配置';
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 节点类型样式 */
|
||||
.startNode .nodeHeader {
|
||||
background-color: #1e83e9;
|
||||
}
|
||||
|
||||
.endNode .nodeHeader {
|
||||
background-color: #7855ce;
|
||||
}
|
||||
|
||||
.uploadNode .nodeHeader {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
.deployNode .nodeHeader {
|
||||
background-color: #f97316;
|
||||
}
|
||||
|
||||
.notifyNode .nodeHeader {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.applyNode .nodeHeader {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
.normalNode .nodeHeader {
|
||||
background-color: #64748b;
|
||||
}
|
||||
|
||||
.branchNode .nodeHeader {
|
||||
background-color: #597ef7;
|
||||
}
|
||||
|
||||
.nodeStatus {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.statusItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.statusLabel {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.handleSuccess {
|
||||
background-color: #10b981;
|
||||
left: 20%;
|
||||
}
|
||||
|
||||
.handleFailure {
|
||||
background-color: #ef4444;
|
||||
left: 80%;
|
||||
}
|
||||
38
frontend/apps/vue-flow/components/nodes/NormalNode.tsx
Normal file
38
frontend/apps/vue-flow/components/nodes/NormalNode.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NormalNode',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 根据状态获取图标
|
||||
const getStatusIcon = () => {
|
||||
switch (props.data.status) {
|
||||
case 'success':
|
||||
return '✅'
|
||||
case 'error':
|
||||
return '❌'
|
||||
default:
|
||||
return 'ℹ️'
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={`${styles.node} ${styles.normalNode}`}>
|
||||
<div class={styles.nodeContent}>
|
||||
<div class={styles.nodeIcon}>{getStatusIcon()}</div>
|
||||
<div class={styles.nodeLabel}>{props.data.label}</div>
|
||||
{props.data.message && <div class={styles.nodeMessage}>{props.data.message}</div>}
|
||||
</div>
|
||||
<Handle id="target" type="target" position={Position.Top} class={styles.handle} />
|
||||
<Handle id="source" type="source" position={Position.Bottom} class={styles.handle} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
47
frontend/apps/vue-flow/components/nodes/NotifyNode.tsx
Normal file
47
frontend/apps/vue-flow/components/nodes/NotifyNode.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NotifyNode',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<div class={`${styles.node} ${styles.notifyNode}`}>
|
||||
<div class={styles.nodeContent}>
|
||||
<div class={styles.nodeIcon}>📢</div>
|
||||
<div class={styles.nodeLabel}>{props.data.label}</div>
|
||||
<div class={styles.nodeStatus}>
|
||||
<div class={styles.statusItem}>
|
||||
<div class={styles.statusLabel}>成功</div>
|
||||
<div class={styles.statusDot} style={{ backgroundColor: '#10b981' }}></div>
|
||||
</div>
|
||||
<div class={styles.statusItem}>
|
||||
<div class={styles.statusLabel}>失败</div>
|
||||
<div class={styles.statusDot} style={{ backgroundColor: '#ef4444' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Handle id="target" type="target" position={Position.Top} class={styles.handle} />
|
||||
<Handle
|
||||
id="source-success"
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
class={`${styles.handle} ${styles.handleSuccess}`}
|
||||
/>
|
||||
<Handle
|
||||
id="source-failure"
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
class={`${styles.handle} ${styles.handleFailure}`}
|
||||
style={{ left: '80%' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
25
frontend/apps/vue-flow/components/nodes/StartNode.tsx
Normal file
25
frontend/apps/vue-flow/components/nodes/StartNode.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StartNode',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<div class={`${styles.node} ${styles.startNode}`}>
|
||||
<div class={styles.nodeContent}>
|
||||
<div class={styles.nodeIcon}>🏁</div>
|
||||
<div class={styles.nodeLabel}>{props.data.label}</div>
|
||||
</div>
|
||||
{/* 只有出口,没有入口 */}
|
||||
<Handle id="source" type="source" position={Position.Bottom} class={styles.handle} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
24
frontend/apps/vue-flow/components/nodes/UploadNode.tsx
Normal file
24
frontend/apps/vue-flow/components/nodes/UploadNode.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import styles from './Node.module.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UploadNode',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<div class={`${styles.node} ${styles.uploadNode}`}>
|
||||
<div class={styles.nodeContent}>
|
||||
<div class={styles.nodeIcon}>📄</div>
|
||||
<div class={styles.nodeLabel}>{props.data.label}</div>
|
||||
</div>
|
||||
<Handle id="target" type="target" position={Position.Top} class={styles.handle} />
|
||||
<Handle id="source" type="source" position={Position.Bottom} class={styles.handle} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
213
frontend/apps/vue-flow/flow.css
Normal file
213
frontend/apps/vue-flow/flow.css
Normal file
@@ -0,0 +1,213 @@
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 8px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.node-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background-color: white;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #f56c6c;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background-color: #e64242;
|
||||
}
|
||||
|
||||
.duplicate-btn {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.duplicate-btn:hover {
|
||||
background-color: #2a8ce8;
|
||||
}
|
||||
|
||||
/* Vue Flow 自定义样式 */
|
||||
|
||||
/* 自定义节点通用样式 */
|
||||
.vue-flow__node {
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
background-color: white;
|
||||
width: 240px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
z-index: -99 !important;
|
||||
}
|
||||
|
||||
.vue-flow__node:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.vue-flow__node.selected {
|
||||
box-shadow: 0 0 0 2px #18a0fb;
|
||||
}
|
||||
|
||||
/* 开始节点样式 */
|
||||
.vue-flow__node.start {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 结束节点样式 */
|
||||
.vue-flow__node.end {
|
||||
background-color: #f6ffed;
|
||||
border-color: #52c41a;
|
||||
color: #52c41a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 申请证书节点样式 */
|
||||
.vue-flow__node.apply {
|
||||
background-color: #fff7e6;
|
||||
border-color: #fa8c16;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 部署证书节点样式 */
|
||||
.vue-flow__node.deploy {
|
||||
background-color: #f9f0ff;
|
||||
border-color: #722ed1;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
/* 通知节点样式 */
|
||||
.vue-flow__node.notify {
|
||||
background-color: #fcf4f6;
|
||||
border-color: #eb2f96;
|
||||
color: #eb2f96;
|
||||
}
|
||||
|
||||
/* 边的样式 */
|
||||
.vue-flow__edge {
|
||||
stroke: #b1b1b7;
|
||||
stroke-width: 2;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.vue-flow__edge.selected {
|
||||
stroke: #18a0fb;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.vue-flow__edge-path {
|
||||
stroke: #b1b1b7;
|
||||
stroke-width: 2;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.vue-flow__edge-path:hover {
|
||||
stroke: #18a0fb;
|
||||
}
|
||||
|
||||
.vue-flow__edge.selected .vue-flow__edge-path {
|
||||
stroke: #18a0fb;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* 连接点样式 */
|
||||
.vue-flow__handle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #1890ff;
|
||||
border-color: white;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 工具栏样式 */
|
||||
.node-toolbar {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 3px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 3px 8px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: #fff1f0;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #ffa39e;
|
||||
}
|
||||
|
||||
.duplicate-btn {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.duplicate-btn:hover {
|
||||
background: #bae7ff;
|
||||
}
|
||||
|
||||
/* 迷你地图样式 */
|
||||
.vue-flow__minimap {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* 背景样式 */
|
||||
.vue-flow__background {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 控制面板样式 */
|
||||
.vue-flow__controls {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.vue-flow__controls-button {
|
||||
background: white;
|
||||
border: 1px solid #eee;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.vue-flow__controls-button:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
24
frontend/apps/vue-flow/index.module.css
Normal file
24
frontend/apps/vue-flow/index.module.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.workflowContainer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workflowEditor {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workflowConfig {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 280px;
|
||||
max-width: 350px;
|
||||
border-left: 1px solid #e8e8e8;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
26
frontend/apps/vue-flow/index.tsx
Normal file
26
frontend/apps/vue-flow/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import WorkflowEditor from './components/WorkflowEditor'
|
||||
import WorkflowNodeConfig from './components/WorkflowNodeConfig'
|
||||
import styles from './index.module.css'
|
||||
import '@vue-flow/core/dist/style.css'
|
||||
import '@vue-flow/core/dist/theme-default.css'
|
||||
import './flow.css'
|
||||
|
||||
/**
|
||||
* 工作流组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'VueFlowWorkflow',
|
||||
setup() {
|
||||
return () => (
|
||||
<div class={styles.workflowContainer}>
|
||||
<div class={styles.workflowEditor}>
|
||||
<WorkflowEditor />
|
||||
</div>
|
||||
<div class={styles.workflowConfig}>
|
||||
<WorkflowNodeConfig />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
686
frontend/apps/vue-flow/store/transformFlowData.ts
Normal file
686
frontend/apps/vue-flow/store/transformFlowData.ts
Normal file
@@ -0,0 +1,686 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { WorkflowNode, WorkflowEdge, NodeData } from '../types'
|
||||
import { MarkerType } from '@vue-flow/core'
|
||||
|
||||
// 节点类型映射
|
||||
const NODE_TYPE_MAP = {
|
||||
start: 'start',
|
||||
apply: 'apply',
|
||||
branch: 'normal', // 使用普通节点作为分支节点
|
||||
condition: 'normal', // 条件节点
|
||||
deploy: 'deploy',
|
||||
execute_result_branch: 'normal', // 执行结果分支节点
|
||||
execute_result_condition: 'normal', // 执行结果条件节点
|
||||
notify: 'notify',
|
||||
}
|
||||
|
||||
// 节点标签映射
|
||||
const NODE_LABEL_MAP = {
|
||||
start: '开始',
|
||||
apply: '申请证书',
|
||||
branch: '分支节点',
|
||||
condition: '条件节点',
|
||||
deploy: '部署证书',
|
||||
execute_result_branch: '执行结果',
|
||||
execute_result_condition: '执行结果条件',
|
||||
notify: '通知',
|
||||
}
|
||||
|
||||
// 布局配置
|
||||
const LAYOUT_CONFIG = {
|
||||
// 节点尺寸
|
||||
nodeWidth: 180,
|
||||
nodeHeight: 40,
|
||||
// 节点间距
|
||||
horizontalGap: 220, // 水平间距,增加防止重叠
|
||||
verticalGap: 150, // 垂直间距,增加更多空间
|
||||
// 分支节点的水平间距
|
||||
branchHorizontalGap: 280, // 增加分支节点间距
|
||||
// 多分支的间距调整系数
|
||||
multiBranchSpacingFactor: 0.9, // 多分支时适当减小间距,确保视野内能容纳
|
||||
// 初始位置
|
||||
initialX: 600, // 增加初始X坐标,使整个图更居中
|
||||
initialY: 80,
|
||||
}
|
||||
|
||||
interface NestedNode {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
config?: any
|
||||
childNode?: NestedNode
|
||||
conditionNodes?: NestedNode[]
|
||||
inputs?: Array<{ name: string; fromNodeId: string }>
|
||||
}
|
||||
|
||||
interface ProcessResult {
|
||||
nodes: WorkflowNode[]
|
||||
edges: WorkflowEdge[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点子树的大小信息
|
||||
*/
|
||||
interface SubtreeSize {
|
||||
width: number // 子树宽度
|
||||
height: number // 子树高度
|
||||
childCount: number // 子节点数量
|
||||
}
|
||||
|
||||
/**
|
||||
* 将嵌套节点数据结构转换为VueFlow所需的节点和边
|
||||
* @param data 嵌套的节点数据
|
||||
* @returns VueFlow的节点和边
|
||||
*/
|
||||
export function transformNestedFlowData(data: NestedNode): ProcessResult {
|
||||
const result: ProcessResult = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
}
|
||||
|
||||
if (!data) return result
|
||||
|
||||
// 垂直对齐布局的实现
|
||||
const nodePositions = new Map<string, { x: number; y: number }>()
|
||||
const yLevels = new Map<number, number>() // 每个层级的y坐标
|
||||
|
||||
// 计算层级
|
||||
const nodeLevels = new Map<string, number>()
|
||||
const nodeParents = new Map<string, string>() // 保存节点的父节点
|
||||
const nodeTrees = new Map<string, NestedNode>() // 保存所有节点引用,便于访问
|
||||
const nodeSubtreeSize = new Map<string, SubtreeSize>() // 保存每个节点子树的大小信息
|
||||
const conditionEndNodes = new Map<string, string[]>() // 保存分支节点的所有结束节点IDs
|
||||
|
||||
// 第一步:遍历节点树,确定每个节点的层级和父节点,并收集所有节点
|
||||
function assignLevels(node: NestedNode | undefined, level: number = 0, parentId: string | null = null) {
|
||||
if (!node) return
|
||||
|
||||
// 保存节点信息
|
||||
nodeLevels.set(node.id, level)
|
||||
nodeTrees.set(node.id, node)
|
||||
|
||||
if (parentId) {
|
||||
nodeParents.set(node.id, parentId)
|
||||
}
|
||||
|
||||
// 处理条件分支节点
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
const hasChildNode = node.childNode !== undefined
|
||||
|
||||
// 条件节点层级 +1
|
||||
for (const condNode of node.conditionNodes) {
|
||||
assignLevels(condNode, level + 1, node.id)
|
||||
|
||||
// 递归处理条件节点的子节点
|
||||
if (condNode.childNode) {
|
||||
// 条件节点的子节点层级 +2
|
||||
assignLevels(condNode.childNode, level + 2, condNode.id)
|
||||
|
||||
// 如果条件分支有子节点,要递归找到所有末端节点
|
||||
findAllEndNodes(condNode.childNode, node.id)
|
||||
} else {
|
||||
// 如果条件节点没有子节点,它自己就是终止节点
|
||||
if (!conditionEndNodes.has(node.id)) {
|
||||
conditionEndNodes.set(node.id, [])
|
||||
}
|
||||
conditionEndNodes.get(node.id)?.push(condNode.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理普通子节点 - 如果同时存在conditionNodes和childNode
|
||||
if (hasChildNode && node.childNode) {
|
||||
// 如果节点同时有条件分支和子节点,子节点级别设为独立的,不直接关联到父节点
|
||||
// 放在比所有条件分支末端节点更低的层级
|
||||
const maxConditionLevel = findMaxConditionEndLevel(node)
|
||||
assignLevels(node.childNode, maxConditionLevel + 1, null)
|
||||
}
|
||||
} else if (node.childNode) {
|
||||
// 普通子节点,层级+1
|
||||
assignLevels(node.childNode, level + 1, node.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 查找条件分支的最大末端层级
|
||||
function findMaxConditionEndLevel(node: NestedNode): number {
|
||||
let maxLevel = nodeLevels.get(node.id) || 0
|
||||
|
||||
// 获取当前分支的所有末端节点
|
||||
const endNodeIds = conditionEndNodes.get(node.id) || []
|
||||
|
||||
// 找出最大层级
|
||||
for (const endId of endNodeIds) {
|
||||
const level = nodeLevels.get(endId) || 0
|
||||
maxLevel = Math.max(maxLevel, level)
|
||||
}
|
||||
|
||||
// 至少比父节点高2个层级
|
||||
return Math.max(maxLevel, nodeLevels.get(node.id)! + 2)
|
||||
}
|
||||
|
||||
// 递归查找分支下所有末端节点
|
||||
function findAllEndNodes(node: NestedNode, branchParentId: string) {
|
||||
// 初始化终止节点集合
|
||||
if (!conditionEndNodes.has(branchParentId)) {
|
||||
conditionEndNodes.set(branchParentId, [])
|
||||
}
|
||||
|
||||
// 如果有条件分支,则不是末端节点,需要继续递归
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
for (const condNode of node.conditionNodes) {
|
||||
if (condNode.childNode) {
|
||||
findAllEndNodes(condNode.childNode, branchParentId)
|
||||
} else {
|
||||
// 条件节点没有子节点,它是末端节点
|
||||
conditionEndNodes.get(branchParentId)?.push(condNode.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还有子节点,继续递归查找
|
||||
if (node.childNode) {
|
||||
findAllEndNodes(node.childNode, branchParentId)
|
||||
}
|
||||
}
|
||||
// 如果还有子节点,不是末端节点,继续递归
|
||||
else if (node.childNode) {
|
||||
findAllEndNodes(node.childNode, branchParentId)
|
||||
}
|
||||
// 没有子节点,也没有条件分支,它是末端节点
|
||||
else {
|
||||
conditionEndNodes.get(branchParentId)?.push(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:计算每个节点子树的大小,自底向上
|
||||
function calculateSubtreeSizes(nodeId: string): SubtreeSize {
|
||||
const node = nodeTrees.get(nodeId)
|
||||
if (!node) {
|
||||
return { width: 0, height: 0, childCount: 0 }
|
||||
}
|
||||
|
||||
// 如果已经计算过,直接返回
|
||||
if (nodeSubtreeSize.has(nodeId)) {
|
||||
return nodeSubtreeSize.get(nodeId)!
|
||||
}
|
||||
|
||||
// 默认大小
|
||||
let subtreeWidth = LAYOUT_CONFIG.nodeWidth
|
||||
let maxChildWidth = 0
|
||||
let totalChildWidth = 0
|
||||
let childCount = 0
|
||||
|
||||
// 计算条件分支的子树大小
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
for (const condNode of node.conditionNodes) {
|
||||
// 计算条件节点子树大小
|
||||
const condSize = calculateSubtreeSizes(condNode.id)
|
||||
totalChildWidth += condSize.width
|
||||
maxChildWidth = Math.max(maxChildWidth, condSize.width)
|
||||
childCount += condSize.childCount + 1 // +1 是条件节点本身
|
||||
|
||||
// 如果条件节点有子节点,也要计算
|
||||
if (condNode.childNode) {
|
||||
const childSize = calculateSubtreeSizes(condNode.childNode.id)
|
||||
totalChildWidth += childSize.width
|
||||
maxChildWidth = Math.max(maxChildWidth, childSize.width)
|
||||
childCount += childSize.childCount + 1 // +1 是子节点本身
|
||||
}
|
||||
}
|
||||
|
||||
// 多个条件分支的总宽度
|
||||
if (node.conditionNodes.length > 1) {
|
||||
// 条件分支节点之间需要间距
|
||||
subtreeWidth = Math.max(
|
||||
subtreeWidth,
|
||||
totalChildWidth + (node.conditionNodes.length - 1) * LAYOUT_CONFIG.branchHorizontalGap,
|
||||
)
|
||||
} else {
|
||||
subtreeWidth = Math.max(subtreeWidth, maxChildWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算普通子节点的子树大小
|
||||
if (node.childNode) {
|
||||
const childSize = calculateSubtreeSizes(node.childNode.id)
|
||||
subtreeWidth = Math.max(subtreeWidth, childSize.width)
|
||||
childCount += childSize.childCount + 1 // +1 是子节点本身
|
||||
}
|
||||
|
||||
// 保存并返回结果
|
||||
const result = { width: subtreeWidth, height: 0, childCount }
|
||||
nodeSubtreeSize.set(nodeId, result)
|
||||
return result
|
||||
}
|
||||
|
||||
// 第三步:计算每个层级的Y坐标
|
||||
function calculateYCoordinates() {
|
||||
const maxLevel = Math.max(...Array.from(nodeLevels.values()))
|
||||
|
||||
for (let i = 0; i <= maxLevel; i++) {
|
||||
yLevels.set(i, LAYOUT_CONFIG.initialY + i * LAYOUT_CONFIG.verticalGap)
|
||||
}
|
||||
}
|
||||
|
||||
// 第四步:计算节点的X坐标位置,考虑子树宽度避免重叠
|
||||
function positionNodes(nodeId: string, leftBoundary: number = 0): number {
|
||||
const node = nodeTrees.get(nodeId)
|
||||
if (!node) return leftBoundary
|
||||
|
||||
const level = nodeLevels.get(nodeId) || 0
|
||||
const subtreeSize = nodeSubtreeSize.get(nodeId) || { width: LAYOUT_CONFIG.nodeWidth, height: 0, childCount: 0 }
|
||||
|
||||
// 计算节点的x坐标 - 居中于其子树
|
||||
const x = leftBoundary + subtreeSize.width / 2
|
||||
|
||||
// 保存节点位置
|
||||
nodePositions.set(nodeId, {
|
||||
x: x,
|
||||
y: yLevels.get(level)!,
|
||||
})
|
||||
|
||||
// 初始左边界,用于子节点布局
|
||||
let childLeftBoundary = leftBoundary
|
||||
|
||||
// 处理条件分支
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
const conditionCount = node.conditionNodes.length
|
||||
|
||||
// 计算所有条件分支以及它们子节点的总宽度
|
||||
let totalConditionWidth = 0
|
||||
const conditionSizes: { nodeId: string; width: number }[] = []
|
||||
|
||||
// 先收集所有条件分支的宽度信息
|
||||
for (const condNode of node.conditionNodes) {
|
||||
const condSize = nodeSubtreeSize.get(condNode.id) || {
|
||||
width: LAYOUT_CONFIG.nodeWidth,
|
||||
height: 0,
|
||||
childCount: 0,
|
||||
}
|
||||
|
||||
// 计算这个条件分支的总宽度(包括所有子节点)
|
||||
let branchWidth = condSize.width
|
||||
|
||||
// 如果有子节点,需要考虑子节点的宽度
|
||||
if (condNode.childNode) {
|
||||
// 递归计算子树的宽度
|
||||
const childTreeWidth = calculateBranchWidth(condNode.childNode)
|
||||
branchWidth = Math.max(branchWidth, childTreeWidth)
|
||||
}
|
||||
|
||||
conditionSizes.push({ nodeId: condNode.id, width: branchWidth })
|
||||
totalConditionWidth += branchWidth
|
||||
}
|
||||
|
||||
// 根据分支数量动态调整间距
|
||||
let adjustedGap = LAYOUT_CONFIG.branchHorizontalGap
|
||||
// 当分支数量大于2时,适当减小间距
|
||||
if (conditionCount > 2) {
|
||||
adjustedGap =
|
||||
LAYOUT_CONFIG.branchHorizontalGap * Math.pow(LAYOUT_CONFIG.multiBranchSpacingFactor, conditionCount - 2)
|
||||
}
|
||||
|
||||
// 添加分支之间的间距
|
||||
totalConditionWidth += (conditionCount - 1) * adjustedGap
|
||||
|
||||
// 计算条件分支区域的起始位置,确保条件分支居中于父节点
|
||||
childLeftBoundary = x - totalConditionWidth / 2
|
||||
|
||||
// 布局每个条件分支
|
||||
for (const condInfo of conditionSizes) {
|
||||
const condNode = nodeTrees.get(condInfo.nodeId)
|
||||
if (!condNode) continue
|
||||
|
||||
// 定位条件节点
|
||||
const condX = childLeftBoundary + condInfo.width / 2
|
||||
|
||||
// 保存条件节点位置
|
||||
nodePositions.set(condNode.id, {
|
||||
x: condX,
|
||||
y: yLevels.get(level + 1)!,
|
||||
})
|
||||
|
||||
// 处理条件子节点
|
||||
if (condNode.childNode) {
|
||||
// 递归布局条件子节点及其子树
|
||||
positionBranch(condNode.childNode, condX, level + 2, condInfo.width)
|
||||
}
|
||||
|
||||
// 更新下一个条件分支的位置
|
||||
childLeftBoundary += condInfo.width + adjustedGap
|
||||
}
|
||||
|
||||
// 如果节点同时有条件分支和普通子节点,独立处理子节点的位置
|
||||
if (node.childNode) {
|
||||
const childNodeId = node.childNode.id
|
||||
// 获取子节点的层级(已经在assignLevels中计算为条件分支末端之后)
|
||||
const childLevel = nodeLevels.get(childNodeId) || level + 3
|
||||
|
||||
// 保存子节点位置 - 与父节点垂直对齐
|
||||
nodePositions.set(childNodeId, {
|
||||
x: x,
|
||||
y: yLevels.get(childLevel)!,
|
||||
})
|
||||
|
||||
// 递归处理子节点的子节点
|
||||
if (node.childNode.childNode || (node.childNode.conditionNodes && node.childNode.conditionNodes.length > 0)) {
|
||||
positionNodes(childNodeId, x - subtreeSize.width / 2)
|
||||
}
|
||||
}
|
||||
} else if (node.childNode) {
|
||||
// 处理普通子节点 - 垂直对齐于父节点下方
|
||||
const childNodeId = node.childNode.id
|
||||
const childLevel = level + 1
|
||||
|
||||
// 保存节点位置
|
||||
nodePositions.set(childNodeId, {
|
||||
x: x, // 与父节点垂直对齐
|
||||
y: yLevels.get(childLevel)!,
|
||||
})
|
||||
|
||||
// 递归处理其子节点
|
||||
if (node.childNode.childNode || (node.childNode.conditionNodes && node.childNode.conditionNodes.length > 0)) {
|
||||
positionNodes(childNodeId, leftBoundary)
|
||||
}
|
||||
}
|
||||
|
||||
return leftBoundary + subtreeSize.width
|
||||
}
|
||||
|
||||
// 计算分支的总宽度(包括所有子节点)
|
||||
function calculateBranchWidth(node: NestedNode): number {
|
||||
const nodeSize = nodeSubtreeSize.get(node.id) || {
|
||||
width: LAYOUT_CONFIG.nodeWidth,
|
||||
height: 0,
|
||||
childCount: 0,
|
||||
}
|
||||
|
||||
let totalWidth = nodeSize.width
|
||||
|
||||
// 如果有条件分支,计算所有分支的总宽度
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
let branchesWidth = 0
|
||||
const branchCount = node.conditionNodes.length
|
||||
|
||||
// 获取每个分支的宽度
|
||||
const branchWidths: number[] = []
|
||||
for (const condNode of node.conditionNodes) {
|
||||
const condWidth = calculateBranchWidth(condNode)
|
||||
branchWidths.push(condWidth)
|
||||
branchesWidth += condWidth
|
||||
}
|
||||
|
||||
// 根据分支数量动态调整间距
|
||||
let adjustedGap = LAYOUT_CONFIG.branchHorizontalGap
|
||||
// 当分支数量大于2时,适当减小间距,避免超出视野
|
||||
if (branchCount > 2) {
|
||||
adjustedGap =
|
||||
LAYOUT_CONFIG.branchHorizontalGap * Math.pow(LAYOUT_CONFIG.multiBranchSpacingFactor, branchCount - 2)
|
||||
}
|
||||
|
||||
// 加上分支间距
|
||||
if (branchCount > 1) {
|
||||
branchesWidth += (branchCount - 1) * adjustedGap
|
||||
}
|
||||
|
||||
totalWidth = Math.max(totalWidth, branchesWidth)
|
||||
}
|
||||
|
||||
// 如果有子节点,递归计算
|
||||
if (node.childNode) {
|
||||
const childWidth = calculateBranchWidth(node.childNode)
|
||||
totalWidth = Math.max(totalWidth, childWidth)
|
||||
}
|
||||
|
||||
return totalWidth
|
||||
}
|
||||
|
||||
// 递归定位分支中的所有节点
|
||||
function positionBranch(node: NestedNode, centerX: number, level: number, availableWidth: number): void {
|
||||
// 保存节点位置 - 垂直对齐于父节点
|
||||
nodePositions.set(node.id, {
|
||||
x: centerX,
|
||||
y: yLevels.get(level)!,
|
||||
})
|
||||
|
||||
// 如果有条件分支,递归处理
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
// 类似于positionNodes中的处理逻辑
|
||||
const conditionCount = node.conditionNodes.length
|
||||
let totalWidth = 0
|
||||
const condSizes: { nodeId: string; width: number }[] = []
|
||||
|
||||
// 计算所有条件分支的宽度
|
||||
for (const condNode of node.conditionNodes) {
|
||||
const width = calculateBranchWidth(condNode)
|
||||
condSizes.push({ nodeId: condNode.id, width })
|
||||
totalWidth += width
|
||||
}
|
||||
|
||||
// 根据分支数量动态调整间距
|
||||
let adjustedGap = LAYOUT_CONFIG.branchHorizontalGap
|
||||
// 当分支数量大于2时,适当减小间距
|
||||
if (conditionCount > 2) {
|
||||
adjustedGap =
|
||||
LAYOUT_CONFIG.branchHorizontalGap * Math.pow(LAYOUT_CONFIG.multiBranchSpacingFactor, conditionCount - 2)
|
||||
}
|
||||
|
||||
// 添加分支间距
|
||||
totalWidth += (conditionCount - 1) * adjustedGap
|
||||
|
||||
// 计算起始位置,确保居中
|
||||
let startX = centerX - totalWidth / 2
|
||||
|
||||
// 处理每个条件分支
|
||||
for (const condInfo of condSizes) {
|
||||
const condNode = nodeTrees.get(condInfo.nodeId)
|
||||
if (!condNode) continue
|
||||
|
||||
// 分支节点居中
|
||||
const condX = startX + condInfo.width / 2
|
||||
|
||||
// 保存位置
|
||||
nodePositions.set(condNode.id, {
|
||||
x: condX,
|
||||
y: yLevels.get(level + 1)!,
|
||||
})
|
||||
|
||||
// 处理子节点
|
||||
if (condNode.childNode) {
|
||||
positionBranch(condNode.childNode, condX, level + 2, condInfo.width)
|
||||
}
|
||||
|
||||
// 更新下一个分支的位置
|
||||
startX += condInfo.width + adjustedGap
|
||||
}
|
||||
|
||||
// 处理子节点
|
||||
if (node.childNode) {
|
||||
// 获取子节点层级
|
||||
const childLevel =
|
||||
Math.max(
|
||||
...Array.from(nodeLevels.entries())
|
||||
.filter(([id]) => nodeParents.get(id) === node.id)
|
||||
.map(([, level]) => level),
|
||||
) + 1
|
||||
|
||||
// 定位子节点
|
||||
positionBranch(node.childNode, centerX, childLevel, availableWidth)
|
||||
}
|
||||
}
|
||||
// 如果有子节点但没有条件分支
|
||||
else if (node.childNode) {
|
||||
positionBranch(node.childNode, centerX, level + 1, availableWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// 第五步:创建节点和边
|
||||
function createNodesAndEdges(node: NestedNode | undefined, parentId: string | null = null) {
|
||||
if (!node) return
|
||||
|
||||
// 获取节点位置
|
||||
const position = nodePositions.get(node.id)
|
||||
if (!position) return // 跳过没有位置信息的节点
|
||||
|
||||
// 节点类型和标签
|
||||
const nodeType = NODE_TYPE_MAP[node.type as keyof typeof NODE_TYPE_MAP] || 'normal'
|
||||
const nodeLabel = node.name || NODE_LABEL_MAP[node.type as keyof typeof NODE_LABEL_MAP] || '未知节点'
|
||||
|
||||
// 创建节点数据
|
||||
const nodeData: NodeData = {
|
||||
id: node.id,
|
||||
type: nodeType as any,
|
||||
label: nodeLabel,
|
||||
canMove: true,
|
||||
canDelete: true,
|
||||
canChangeType: true,
|
||||
}
|
||||
|
||||
// 添加配置信息
|
||||
if (node.config) {
|
||||
Object.assign(nodeData, { config: node.config })
|
||||
}
|
||||
|
||||
// 创建节点
|
||||
const flowNode: WorkflowNode = {
|
||||
id: node.id,
|
||||
type: nodeType,
|
||||
position: { x: position.x, y: position.y },
|
||||
data: nodeData,
|
||||
}
|
||||
|
||||
// 添加节点
|
||||
result.nodes.push(flowNode)
|
||||
|
||||
// 添加从父节点到当前节点的边(如果有父节点)
|
||||
if (parentId) {
|
||||
const edgeId = `${parentId}-${node.id}`
|
||||
const parentPosition = nodePositions.get(parentId)
|
||||
if (parentPosition) {
|
||||
// 计算边缘路径的偏移量,避免与节点重叠
|
||||
const offset = LAYOUT_CONFIG.nodeHeight / 2 + 10 // 节点高度的一半加上额外间距
|
||||
const sourceY = parentPosition.y + offset
|
||||
const targetY = position.y - offset
|
||||
|
||||
const edge: WorkflowEdge = {
|
||||
id: edgeId,
|
||||
source: parentId,
|
||||
target: node.id,
|
||||
type: 'step',
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: 5,
|
||||
},
|
||||
animated: true,
|
||||
// 添加箭头标记
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 15,
|
||||
height: 15,
|
||||
color: '#b1b1b7',
|
||||
},
|
||||
// 添加路径偏移
|
||||
sourceY,
|
||||
targetY,
|
||||
}
|
||||
result.edges.push(edge)
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理 - 同时有条件分支和子节点的情况
|
||||
const hasConditionNodesAndChildNode = node.conditionNodes && node.conditionNodes.length > 0 && node.childNode
|
||||
|
||||
// 处理条件分支节点
|
||||
if (node.conditionNodes && node.conditionNodes.length > 0) {
|
||||
// 处理每个条件分支
|
||||
for (const condNode of node.conditionNodes) {
|
||||
// 创建条件节点及其子节点
|
||||
createNodesAndEdges(condNode, node.id)
|
||||
|
||||
if (condNode.childNode) {
|
||||
createNodesAndEdges(condNode.childNode, condNode.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果同时存在子节点,将所有条件分支末端节点连接到该子节点
|
||||
if (hasConditionNodesAndChildNode) {
|
||||
const childNodeId = node.childNode!.id
|
||||
const endNodeIds = conditionEndNodes.get(node.id) || []
|
||||
|
||||
// 为每个条件分支的末端节点创建到子节点的连接
|
||||
for (const endNodeId of endNodeIds) {
|
||||
// 避免重复创建边
|
||||
const edgeId = `${endNodeId}-${childNodeId}`
|
||||
|
||||
// 检查是否已经存在此边
|
||||
const edgeExists = result.edges.some((edge) => edge.id === edgeId)
|
||||
|
||||
if (!edgeExists) {
|
||||
const endNodePosition = nodePositions.get(endNodeId)
|
||||
const childNodePosition = nodePositions.get(childNodeId)
|
||||
|
||||
if (endNodePosition && childNodePosition) {
|
||||
// 计算边缘路径的偏移量
|
||||
const offset = LAYOUT_CONFIG.nodeHeight / 2 + 10
|
||||
const sourceY = endNodePosition.y + offset
|
||||
const targetY = childNodePosition.y - offset
|
||||
|
||||
const edge: WorkflowEdge = {
|
||||
id: edgeId,
|
||||
source: endNodeId,
|
||||
target: childNodeId,
|
||||
type: 'step',
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: 5,
|
||||
},
|
||||
animated: true,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 15,
|
||||
height: 15,
|
||||
color: '#b1b1b7',
|
||||
},
|
||||
// 添加路径偏移
|
||||
sourceY,
|
||||
targetY,
|
||||
}
|
||||
result.edges.push(edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 独立创建子节点,不从父节点连接
|
||||
createNodesAndEdges(node.childNode, null)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理常规子节点(如果不是与条件分支共存的情况)
|
||||
if (node.childNode && !hasConditionNodesAndChildNode) {
|
||||
createNodesAndEdges(node.childNode, node.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行布局算法
|
||||
assignLevels(data)
|
||||
// 计算子树大小(从根节点开始)
|
||||
calculateSubtreeSizes(data.id)
|
||||
calculateYCoordinates()
|
||||
// 计算节点位置,从根节点开始,初始左边界为0
|
||||
positionNodes(data.id, 0)
|
||||
createNodesAndEdges(data)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理工作流数据,从 JSON 结构转换为 VueFlow 所需的节点和边
|
||||
* @param workflowData 工作流 JSON 数据
|
||||
* @returns VueFlow的节点和边
|
||||
*/
|
||||
export function processWorkflowData(workflowData: any): ProcessResult {
|
||||
if (!workflowData) {
|
||||
return { nodes: [], edges: [] }
|
||||
}
|
||||
|
||||
// 处理嵌套数据结构
|
||||
return transformNestedFlowData(workflowData)
|
||||
}
|
||||
284
frontend/apps/vue-flow/store/workflow.ts
Normal file
284
frontend/apps/vue-flow/store/workflow.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useVueFlow } from '@vue-flow/core'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowNode,
|
||||
WorkflowEdge,
|
||||
NodeData,
|
||||
NODE_START,
|
||||
NODE_END,
|
||||
NODE_UPLOAD,
|
||||
NODE_DEPLOY,
|
||||
NODE_NOTIFY,
|
||||
NODE_APPLY,
|
||||
NODE_NORMAL,
|
||||
NodeType,
|
||||
} from '../types'
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
// 初始化Vue Flow
|
||||
const { onNodesChange, onEdgesChange, onConnect, findNode } = useVueFlow()
|
||||
// 节点和边的状态
|
||||
const nodes = ref<WorkflowNode[]>([])
|
||||
const edges = ref<WorkflowEdge[]>([])
|
||||
// 当前选中的节点
|
||||
const selectedNode = ref<WorkflowNode | null>(null)
|
||||
// 工作流标题
|
||||
const workflowTitle = ref('新建工作流')
|
||||
// 是否显示保存按钮
|
||||
const isDataChanged = ref(false)
|
||||
|
||||
/**
|
||||
* 初始化工作流数据
|
||||
*/
|
||||
function initWorkflow() {
|
||||
// 创建初始节点
|
||||
const startNode: WorkflowNode = {
|
||||
id: 'start-node',
|
||||
type: NODE_START,
|
||||
position: { x: 250, y: 50 },
|
||||
data: {
|
||||
id: 'start-node',
|
||||
type: NODE_START,
|
||||
label: '开始',
|
||||
canMove: false,
|
||||
canDelete: false,
|
||||
canChangeType: false,
|
||||
},
|
||||
}
|
||||
|
||||
const endNode: WorkflowNode = {
|
||||
id: 'end-node',
|
||||
type: NODE_END,
|
||||
position: { x: 250, y: 350 },
|
||||
data: {
|
||||
id: 'end-node',
|
||||
type: NODE_END,
|
||||
label: '结束',
|
||||
canMove: false,
|
||||
canDelete: false,
|
||||
canChangeType: false,
|
||||
},
|
||||
}
|
||||
|
||||
// 设置初始节点
|
||||
nodes.value = [startNode, endNode]
|
||||
edges.value = []
|
||||
|
||||
// 标记数据已变更
|
||||
isDataChanged.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择节点
|
||||
* @param node 节点
|
||||
*/
|
||||
function selectNode(node: WorkflowNode | null) {
|
||||
selectedNode.value = node
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加节点
|
||||
* @param nodeType 节点类型
|
||||
* @param sourceNodeId 源节点ID
|
||||
*/
|
||||
function addNode(nodeType: NodeType, sourceNodeId: string) {
|
||||
// 创建新节点ID
|
||||
const newNodeId = uuidv4()
|
||||
|
||||
// 获取源节点
|
||||
const sourceNode = findNode(sourceNodeId)
|
||||
if (!sourceNode) return
|
||||
|
||||
// 计算新节点位置 (在源节点下方100px)
|
||||
const position = {
|
||||
x: sourceNode.position.x,
|
||||
y: sourceNode.position.y + 100,
|
||||
}
|
||||
|
||||
// 创建节点基础数据
|
||||
const baseNodeData = {
|
||||
id: newNodeId,
|
||||
type: nodeType,
|
||||
label: getNodeLabel(nodeType),
|
||||
canMove: true,
|
||||
canDelete: true,
|
||||
canChangeType: true,
|
||||
}
|
||||
|
||||
// 根据节点类型创建不同的节点数据
|
||||
let nodeData: NodeData
|
||||
|
||||
switch (nodeType) {
|
||||
case NODE_UPLOAD:
|
||||
nodeData = { ...baseNodeData, type: NODE_UPLOAD }
|
||||
break
|
||||
case NODE_DEPLOY:
|
||||
nodeData = { ...baseNodeData, type: NODE_DEPLOY }
|
||||
break
|
||||
case NODE_NOTIFY:
|
||||
nodeData = { ...baseNodeData, type: NODE_NOTIFY }
|
||||
break
|
||||
case NODE_APPLY:
|
||||
nodeData = { ...baseNodeData, type: NODE_APPLY }
|
||||
break
|
||||
case NODE_NORMAL:
|
||||
nodeData = { ...baseNodeData, type: NODE_NORMAL, status: 'info' }
|
||||
break
|
||||
default:
|
||||
nodeData = baseNodeData as NodeData
|
||||
}
|
||||
|
||||
// 创建新节点
|
||||
const newNode: WorkflowNode = {
|
||||
id: newNodeId,
|
||||
type: nodeType,
|
||||
position,
|
||||
data: nodeData,
|
||||
}
|
||||
|
||||
// 添加节点
|
||||
nodes.value = [...nodes.value, newNode]
|
||||
|
||||
// 创建连接边
|
||||
const newEdge: WorkflowEdge = {
|
||||
id: `${sourceNodeId}-${newNodeId}`,
|
||||
source: sourceNodeId,
|
||||
target: newNodeId,
|
||||
}
|
||||
|
||||
// 添加边
|
||||
edges.value = [...edges.value, newEdge]
|
||||
|
||||
// 选中新节点
|
||||
selectNode(newNode)
|
||||
|
||||
// 标记数据已变更
|
||||
isDataChanged.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点数据
|
||||
* @param nodeId 节点ID
|
||||
* @param data 节点数据
|
||||
*/
|
||||
function updateNodeData(nodeId: string, data: Partial<NodeData>) {
|
||||
// 查找节点
|
||||
const node = findNode(nodeId)
|
||||
if (!node) return
|
||||
|
||||
// 更新节点数据
|
||||
nodes.value = nodes.value.map((n) => {
|
||||
if (n.id === nodeId) {
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
...data,
|
||||
} as NodeData,
|
||||
}
|
||||
}
|
||||
return n
|
||||
})
|
||||
|
||||
// 更新选中节点
|
||||
if (selectedNode.value?.id === nodeId) {
|
||||
selectNode({
|
||||
...selectedNode.value,
|
||||
data: {
|
||||
...selectedNode.value.data,
|
||||
...data,
|
||||
} as NodeData,
|
||||
} as WorkflowNode)
|
||||
}
|
||||
|
||||
// 标记数据已变更
|
||||
isDataChanged.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点标签
|
||||
* @param nodeType 节点类型
|
||||
* @returns 节点标签
|
||||
*/
|
||||
function getNodeLabel(nodeType: NodeType): string {
|
||||
switch (nodeType) {
|
||||
case NODE_START:
|
||||
return '开始'
|
||||
case NODE_END:
|
||||
return '结束'
|
||||
case NODE_UPLOAD:
|
||||
return '上传证书'
|
||||
case NODE_DEPLOY:
|
||||
return '部署证书'
|
||||
case NODE_NOTIFY:
|
||||
return '通知'
|
||||
case NODE_APPLY:
|
||||
return '申请'
|
||||
case NODE_NORMAL:
|
||||
return '普通节点'
|
||||
default:
|
||||
return '未知节点'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存工作流
|
||||
*/
|
||||
function saveWorkflow() {
|
||||
// // 这里可以实现保存逻辑,如API调用等
|
||||
// console.log('保存工作流', {
|
||||
// title: workflowTitle.value,
|
||||
// nodes: nodes.value,
|
||||
// edges: edges.value,
|
||||
// })
|
||||
|
||||
// 重置数据变更标记
|
||||
isDataChanged.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载工作流数据
|
||||
* @param data 工作流数据
|
||||
*/
|
||||
function loadWorkflow(data: WorkflowData) {
|
||||
if (data && data.nodes && data.edges) {
|
||||
nodes.value = data.nodes
|
||||
edges.value = data.edges
|
||||
isDataChanged.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工作流标题
|
||||
* @param title 标题
|
||||
*/
|
||||
function setWorkflowTitle(title: string) {
|
||||
workflowTitle.value = title
|
||||
isDataChanged.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
nodes,
|
||||
edges,
|
||||
selectedNode,
|
||||
workflowTitle,
|
||||
isDataChanged,
|
||||
|
||||
// 方法
|
||||
initWorkflow,
|
||||
selectNode,
|
||||
addNode,
|
||||
updateNodeData,
|
||||
saveWorkflow,
|
||||
loadWorkflow,
|
||||
setWorkflowTitle,
|
||||
|
||||
// Vue Flow方法
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onConnect,
|
||||
}
|
||||
})
|
||||
146
frontend/apps/vue-flow/types.ts
Normal file
146
frontend/apps/vue-flow/types.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Node, Edge, DefaultEdge, MarkerType } from '@vue-flow/core'
|
||||
|
||||
// 节点类型常量
|
||||
export const NODE_START = 'start'
|
||||
export const NODE_END = 'end'
|
||||
export const NODE_UPLOAD = 'upload'
|
||||
export const NODE_DEPLOY = 'deploy'
|
||||
export const NODE_NOTIFY = 'notify'
|
||||
export const NODE_APPLY = 'apply'
|
||||
export const NODE_NORMAL = 'normal'
|
||||
export const NODE_BRANCH = 'branch'
|
||||
|
||||
// 节点类型
|
||||
export type NodeType =
|
||||
| typeof NODE_START
|
||||
| typeof NODE_END
|
||||
| typeof NODE_UPLOAD
|
||||
| typeof NODE_DEPLOY
|
||||
| typeof NODE_NOTIFY
|
||||
| typeof NODE_APPLY
|
||||
| typeof NODE_NORMAL
|
||||
| typeof NODE_BRANCH
|
||||
|
||||
// 图标映射
|
||||
export const ICON_MAP = {
|
||||
[NODE_START]: '▶️',
|
||||
[NODE_END]: '⏹️',
|
||||
[NODE_UPLOAD]: '📤',
|
||||
[NODE_DEPLOY]: '🚀',
|
||||
[NODE_NOTIFY]: '📣',
|
||||
[NODE_APPLY]: '📝',
|
||||
[NODE_NORMAL]: '📄',
|
||||
[NODE_BRANCH]: '🔀',
|
||||
}
|
||||
|
||||
// 基础节点数据
|
||||
export interface BaseNodeData {
|
||||
id: string
|
||||
type: NodeType
|
||||
label: string
|
||||
canMove?: boolean
|
||||
canDelete?: boolean
|
||||
canChangeType?: boolean
|
||||
icon?: string
|
||||
}
|
||||
|
||||
// 开始节点数据
|
||||
export interface StartNodeData extends BaseNodeData {
|
||||
type: typeof NODE_START
|
||||
}
|
||||
|
||||
// 结束节点数据
|
||||
export interface EndNodeData extends BaseNodeData {
|
||||
type: typeof NODE_END
|
||||
}
|
||||
|
||||
// 上传证书节点数据
|
||||
export interface UploadNodeData extends BaseNodeData {
|
||||
type: typeof NODE_UPLOAD
|
||||
certificateContent?: string
|
||||
certificateFile?: File
|
||||
}
|
||||
|
||||
// 部署证书节点数据
|
||||
export interface DeployNodeData extends BaseNodeData {
|
||||
type: typeof NODE_DEPLOY
|
||||
certificateContent?: string
|
||||
deployTarget?: string
|
||||
}
|
||||
|
||||
// 通知节点数据
|
||||
export interface NotifyNodeData extends BaseNodeData {
|
||||
type: typeof NODE_NOTIFY
|
||||
message?: string
|
||||
notifyType?: string
|
||||
}
|
||||
|
||||
// 申请节点数据
|
||||
export interface ApplyNodeData extends BaseNodeData {
|
||||
type: typeof NODE_APPLY
|
||||
applicationContent?: string
|
||||
}
|
||||
|
||||
// 分支节点数据
|
||||
export interface BranchNodeData extends BaseNodeData {
|
||||
type: typeof NODE_BRANCH
|
||||
branchCount?: number
|
||||
conditions?: Array<{
|
||||
id: string
|
||||
label: string
|
||||
condition: string
|
||||
}>
|
||||
}
|
||||
|
||||
// 普通节点数据
|
||||
export interface NormalNodeData extends BaseNodeData {
|
||||
type: typeof NODE_NORMAL
|
||||
message?: string
|
||||
status?: 'success' | 'error' | 'info'
|
||||
}
|
||||
|
||||
// 所有节点数据类型
|
||||
export type NodeData =
|
||||
| StartNodeData
|
||||
| EndNodeData
|
||||
| UploadNodeData
|
||||
| DeployNodeData
|
||||
| NotifyNodeData
|
||||
| ApplyNodeData
|
||||
| NormalNodeData
|
||||
| BranchNodeData
|
||||
|
||||
// 工作流节点
|
||||
export type WorkflowNode = Node<NodeData>
|
||||
|
||||
// 工作流边
|
||||
export interface WorkflowEdge extends DefaultEdge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
type: string
|
||||
style?: Record<string, any>
|
||||
animated?: boolean
|
||||
markerEnd?: {
|
||||
type: MarkerType
|
||||
width: number
|
||||
height: number
|
||||
color: string
|
||||
}
|
||||
// 源和目标连接点处理
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
// 路径控制数据
|
||||
data?: {
|
||||
pathPoints?: Array<{
|
||||
x: number
|
||||
y: number
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
// 工作流数据
|
||||
export interface WorkflowData {
|
||||
nodes: WorkflowNode[]
|
||||
edges: WorkflowEdge[]
|
||||
}
|
||||
Reference in New Issue
Block a user