【新增】私有证书

This commit is contained in:
cai
2025-09-03 15:15:59 +08:00
parent efd052a297
commit 954cd1638d
442 changed files with 76787 additions and 7483 deletions

View 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;
}

View 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>
)
},
})

View 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;
}

View 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>
)
}
},
})

View File

@@ -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>
)
},
})

View 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;
}

View File

@@ -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>
)
},
})

View 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>
)
},
})

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View File

@@ -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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})

View File

@@ -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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})

View 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}
/>
)
},
})

View 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>
)
},
})

View 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%;
}

View 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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})

View 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>
)
},
})