491 lines
11 KiB
TypeScript
491 lines
11 KiB
TypeScript
/**
|
||
* 工作流 Store
|
||
* 管理工作流状态、节点类型、字段映射等
|
||
*/
|
||
|
||
import { defineStore } from 'pinia'
|
||
import { ref, computed } from 'vue'
|
||
import type {
|
||
NodeTypeMetadata,
|
||
WorkflowNode,
|
||
WorkflowEdge,
|
||
WorkflowDefinition,
|
||
UpstreamNodeOutput,
|
||
FieldPathNode,
|
||
OutputParameter,
|
||
HistoryItem,
|
||
} from '#/types/workflow'
|
||
import { MOCK_NODE_TYPES, getNodeTypeMetadata } from '#/views/workflow/vue-flow-design/mock/nodeTypes'
|
||
|
||
export const useWorkflowStore = defineStore('workflow', () => {
|
||
// ============== 状态 ==============
|
||
|
||
/** 所有可用的节点类型 */
|
||
const nodeTypes = ref<NodeTypeMetadata[]>(MOCK_NODE_TYPES)
|
||
|
||
/** 当前工作流的节点 */
|
||
const nodes = ref<WorkflowNode[]>([])
|
||
|
||
/** 当前工作流的边 */
|
||
const edges = ref<WorkflowEdge[]>([])
|
||
|
||
/** 选中的节点 ID */
|
||
const selectedNodeId = ref<string | null>(null)
|
||
|
||
/** 选中的边 ID */
|
||
const selectedEdgeId = ref<string | null>(null)
|
||
|
||
/** 历史记录 */
|
||
const history = ref<HistoryItem[]>([])
|
||
|
||
/** 历史记录索引 */
|
||
const historyIndex = ref(-1)
|
||
|
||
/** 最大历史记录数 */
|
||
const maxHistorySize = 50
|
||
|
||
/** 工作流元数据 */
|
||
const workflowMeta = ref({
|
||
name: '未命名工作流',
|
||
description: '',
|
||
id: '',
|
||
})
|
||
|
||
// ============== 计算属性 ==============
|
||
|
||
/** 选中的节点 */
|
||
const selectedNode = computed(() => {
|
||
if (!selectedNodeId.value) return null
|
||
return nodes.value.find((n) => n.id === selectedNodeId.value)
|
||
})
|
||
|
||
/** 选中的边 */
|
||
const selectedEdge = computed(() => {
|
||
if (!selectedEdgeId.value) return null
|
||
return edges.value.find((e) => e.id === selectedEdgeId.value)
|
||
})
|
||
|
||
/** 是否可以撤销 */
|
||
const canUndo = computed(() => historyIndex.value > 0)
|
||
|
||
/** 是否可以重做 */
|
||
const canRedo = computed(() => historyIndex.value < history.value.length - 1)
|
||
|
||
/** 节点统计 */
|
||
const statistics = computed(() => ({
|
||
nodeCount: nodes.value.length,
|
||
edgeCount: edges.value.length,
|
||
nodesByType: nodes.value.reduce((acc, node) => {
|
||
acc[node.type] = (acc[node.type] || 0) + 1
|
||
return acc
|
||
}, {} as Record<string, number>),
|
||
}))
|
||
|
||
// ============== 节点操作 ==============
|
||
|
||
/**
|
||
* 添加节点
|
||
*/
|
||
function addNode(node: WorkflowNode) {
|
||
nodes.value.push(node)
|
||
saveHistory()
|
||
}
|
||
|
||
/**
|
||
* 删除节点
|
||
*/
|
||
function removeNode(nodeId: string) {
|
||
const index = nodes.value.findIndex((n) => n.id === nodeId)
|
||
if (index > -1) {
|
||
nodes.value.splice(index, 1)
|
||
// 同时删除相关的边
|
||
edges.value = edges.value.filter(
|
||
(e) => e.source !== nodeId && e.target !== nodeId
|
||
)
|
||
saveHistory()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新节点数据
|
||
*/
|
||
function updateNode(nodeId: string, data: Partial<WorkflowNode>) {
|
||
const node = nodes.value.find((n) => n.id === nodeId)
|
||
if (node) {
|
||
Object.assign(node, data)
|
||
saveHistory()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新节点的 data 字段
|
||
*/
|
||
function updateNodeData(nodeId: string, data: any) {
|
||
const node = nodes.value.find((n) => n.id === nodeId)
|
||
if (node) {
|
||
node.data = { ...node.data, ...data }
|
||
saveHistory()
|
||
}
|
||
}
|
||
|
||
// ============== 边操作 ==============
|
||
|
||
/**
|
||
* 添加边
|
||
*/
|
||
function addEdge(edge: WorkflowEdge) {
|
||
edges.value.push(edge)
|
||
saveHistory()
|
||
}
|
||
|
||
/**
|
||
* 删除边
|
||
*/
|
||
function removeEdge(edgeId: string) {
|
||
const index = edges.value.findIndex((e) => e.id === edgeId)
|
||
if (index > -1) {
|
||
edges.value.splice(index, 1)
|
||
saveHistory()
|
||
}
|
||
}
|
||
|
||
// ============== 选择操作 ==============
|
||
|
||
/**
|
||
* 选择节点
|
||
*/
|
||
function selectNode(nodeId: string | null) {
|
||
selectedNodeId.value = nodeId
|
||
selectedEdgeId.value = null
|
||
}
|
||
|
||
/**
|
||
* 选择边
|
||
*/
|
||
function selectEdge(edgeId: string | null) {
|
||
selectedEdgeId.value = edgeId
|
||
selectedNodeId.value = null
|
||
}
|
||
|
||
/**
|
||
* 清除选择
|
||
*/
|
||
function clearSelection() {
|
||
selectedNodeId.value = null
|
||
selectedEdgeId.value = null
|
||
}
|
||
|
||
// ============== 历史记录 ==============
|
||
|
||
/**
|
||
* 保存历史记录
|
||
*/
|
||
function saveHistory() {
|
||
const currentState: HistoryItem = {
|
||
nodes: JSON.parse(JSON.stringify(nodes.value)),
|
||
edges: JSON.parse(JSON.stringify(edges.value)),
|
||
timestamp: Date.now(),
|
||
}
|
||
|
||
// 移除当前索引之后的历史记录
|
||
history.value = history.value.slice(0, historyIndex.value + 1)
|
||
|
||
// 添加新的历史记录
|
||
history.value.push(currentState)
|
||
|
||
// 限制历史记录大小
|
||
if (history.value.length > maxHistorySize) {
|
||
history.value.shift()
|
||
} else {
|
||
historyIndex.value++
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 撤销
|
||
*/
|
||
function undo() {
|
||
if (canUndo.value) {
|
||
historyIndex.value--
|
||
const state = history.value[historyIndex.value]
|
||
nodes.value = JSON.parse(JSON.stringify(state.nodes))
|
||
edges.value = JSON.parse(JSON.stringify(state.edges))
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重做
|
||
*/
|
||
function redo() {
|
||
if (canRedo.value) {
|
||
historyIndex.value++
|
||
const state = history.value[historyIndex.value]
|
||
nodes.value = JSON.parse(JSON.stringify(state.nodes))
|
||
edges.value = JSON.parse(JSON.stringify(state.edges))
|
||
}
|
||
}
|
||
|
||
// ============== 字段映射相关 ==============
|
||
|
||
/**
|
||
* 获取节点的上游节点输出(用于字段映射)
|
||
*/
|
||
function getUpstreamOutputs(nodeId: string): UpstreamNodeOutput[] {
|
||
const upstreamNodes: UpstreamNodeOutput[] = []
|
||
|
||
// 找到所有连接到当前节点的边
|
||
const incomingEdges = edges.value.filter((e) => e.target === nodeId)
|
||
|
||
incomingEdges.forEach((edge) => {
|
||
const sourceNode = nodes.value.find((n) => n.id === edge.source)
|
||
if (sourceNode) {
|
||
const nodeType = getNodeTypeMetadata(sourceNode.type)
|
||
if (nodeType) {
|
||
const fieldTree = buildFieldTree(nodeType.outputs, edge.source)
|
||
upstreamNodes.push({
|
||
nodeId: sourceNode.id,
|
||
nodeName: sourceNode.data.label,
|
||
outputs: nodeType.outputs,
|
||
fieldTree,
|
||
})
|
||
}
|
||
}
|
||
})
|
||
|
||
return upstreamNodes
|
||
}
|
||
|
||
/**
|
||
* 从输出参数构建字段树
|
||
*/
|
||
function buildFieldTree(
|
||
outputs: OutputParameter[],
|
||
nodeId: string
|
||
): FieldPathNode[] {
|
||
const tree: FieldPathNode[] = []
|
||
|
||
outputs.forEach((output) => {
|
||
const rootPath = `nodes.${nodeId}.output.${output.id}`
|
||
|
||
if (output.schema) {
|
||
// 有 schema,构建详细的字段树
|
||
const children = buildFieldTreeFromSchema(output.schema, rootPath)
|
||
tree.push({
|
||
key: output.id,
|
||
path: rootPath,
|
||
type: output.type,
|
||
description: output.description,
|
||
children: children.length > 0 ? children : undefined,
|
||
isLeaf: children.length === 0,
|
||
})
|
||
} else {
|
||
// 没有 schema,只显示顶级字段
|
||
tree.push({
|
||
key: output.id,
|
||
path: rootPath,
|
||
type: output.type,
|
||
description: output.description,
|
||
isLeaf: true,
|
||
})
|
||
}
|
||
})
|
||
|
||
return tree
|
||
}
|
||
|
||
/**
|
||
* 从 Schema 递归构建字段树
|
||
*/
|
||
function buildFieldTreeFromSchema(
|
||
schema: any,
|
||
basePath: string
|
||
): FieldPathNode[] {
|
||
const nodes: FieldPathNode[] = []
|
||
|
||
if (schema.type === 'object' && schema.properties) {
|
||
Object.entries(schema.properties).forEach(([key, propSchema]: [string, any]) => {
|
||
const path = `${basePath}.${key}`
|
||
const children = buildFieldTreeFromSchema(propSchema, path)
|
||
|
||
nodes.push({
|
||
key,
|
||
path,
|
||
type: propSchema.type || 'any',
|
||
description: propSchema.description,
|
||
example: propSchema.example,
|
||
children: children.length > 0 ? children : undefined,
|
||
isLeaf: children.length === 0,
|
||
})
|
||
})
|
||
} else if (schema.type === 'array' && schema.items) {
|
||
// 数组类型,添加 [0] 示例
|
||
const path = `${basePath}[0]`
|
||
const children = buildFieldTreeFromSchema(schema.items, path)
|
||
|
||
if (children.length > 0) {
|
||
nodes.push({
|
||
key: '[0]',
|
||
path,
|
||
type: 'object',
|
||
description: '数组元素(示例)',
|
||
children,
|
||
isLeaf: false,
|
||
})
|
||
}
|
||
}
|
||
|
||
return nodes
|
||
}
|
||
|
||
/**
|
||
* 生成字段表达式
|
||
*/
|
||
function generateFieldExpression(fieldPath: string): string {
|
||
return `\${${fieldPath}}`
|
||
}
|
||
|
||
// ============== 工作流操作 ==============
|
||
|
||
/**
|
||
* 加载工作流
|
||
*/
|
||
function loadWorkflow(workflow: WorkflowDefinition) {
|
||
nodes.value = workflow.nodes
|
||
edges.value = workflow.edges
|
||
workflowMeta.value = {
|
||
name: workflow.name,
|
||
description: workflow.description || '',
|
||
id: workflow.id || '',
|
||
}
|
||
|
||
// 重置历史记录
|
||
history.value = [{
|
||
nodes: JSON.parse(JSON.stringify(nodes.value)),
|
||
edges: JSON.parse(JSON.stringify(edges.value)),
|
||
timestamp: Date.now(),
|
||
}]
|
||
historyIndex.value = 0
|
||
}
|
||
|
||
/**
|
||
* 导出工作流定义
|
||
*/
|
||
function exportWorkflow(): WorkflowDefinition {
|
||
return {
|
||
id: workflowMeta.value.id,
|
||
name: workflowMeta.value.name,
|
||
description: workflowMeta.value.description,
|
||
nodes: nodes.value,
|
||
edges: edges.value,
|
||
version: '1.0',
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清空工作流
|
||
*/
|
||
function clearWorkflow() {
|
||
nodes.value = []
|
||
edges.value = []
|
||
selectedNodeId.value = null
|
||
selectedEdgeId.value = null
|
||
history.value = []
|
||
historyIndex.value = -1
|
||
workflowMeta.value = {
|
||
name: '未命名工作流',
|
||
description: '',
|
||
id: '',
|
||
}
|
||
}
|
||
|
||
// ============== 节点类型查询 ==============
|
||
|
||
/**
|
||
* 根据类型获取节点元数据
|
||
*/
|
||
function getNodeType(type: string): NodeTypeMetadata | undefined {
|
||
return nodeTypes.value.find((t) => t.type === type)
|
||
}
|
||
|
||
/**
|
||
* 创建节点实例
|
||
*/
|
||
function createNodeInstance(
|
||
type: string,
|
||
position: { x: number; y: number }
|
||
): WorkflowNode | null {
|
||
const nodeType = getNodeType(type)
|
||
if (!nodeType) return null
|
||
|
||
const nodeId = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||
|
||
return {
|
||
id: nodeId,
|
||
type: nodeType.type,
|
||
position,
|
||
data: {
|
||
label: nodeType.label,
|
||
type: nodeType.type,
|
||
description: nodeType.description,
|
||
inputs: JSON.parse(JSON.stringify(nodeType.inputs)),
|
||
outputs: JSON.parse(JSON.stringify(nodeType.outputs)),
|
||
config: {},
|
||
},
|
||
}
|
||
}
|
||
|
||
return {
|
||
// 状态
|
||
nodeTypes,
|
||
nodes,
|
||
edges,
|
||
selectedNodeId,
|
||
selectedEdgeId,
|
||
workflowMeta,
|
||
|
||
// 计算属性
|
||
selectedNode,
|
||
selectedEdge,
|
||
canUndo,
|
||
canRedo,
|
||
statistics,
|
||
|
||
// 节点操作
|
||
addNode,
|
||
removeNode,
|
||
updateNode,
|
||
updateNodeData,
|
||
|
||
// 边操作
|
||
addEdge,
|
||
removeEdge,
|
||
|
||
// 选择操作
|
||
selectNode,
|
||
selectEdge,
|
||
clearSelection,
|
||
|
||
// 历史记录
|
||
saveHistory,
|
||
undo,
|
||
redo,
|
||
|
||
// 字段映射
|
||
getUpstreamOutputs,
|
||
buildFieldTree,
|
||
generateFieldExpression,
|
||
|
||
// 工作流操作
|
||
loadWorkflow,
|
||
exportWorkflow,
|
||
clearWorkflow,
|
||
|
||
// 节点类型
|
||
getNodeType,
|
||
createNodeInstance,
|
||
}
|
||
})
|
||
|