可以正常添加工作流节点,但无法拖拽

This commit is contained in:
2025-08-22 13:45:58 +08:00
parent f4a88349f8
commit e1c0e7f633
14 changed files with 14117 additions and 13019 deletions

View File

@@ -0,0 +1,155 @@
# Design Document
## Overview
The drag and drop functionality issue stems from incorrect vuedraggable configuration between the right panel (node library) and left panel (workflow). The current implementation has mismatched group configurations and improper clone handling that prevents nodes from being successfully transferred from the library to the workflow.
## Architecture
The fix involves correcting the vuedraggable configuration to properly support:
1. **Clone-based dragging** from right panel to left panel
2. **Proper group configuration** to allow cross-panel transfers
3. **Unique ID generation** for cloned nodes
4. **State management** for workflow updates
## Components and Interfaces
### Vuedraggable Configuration
#### Right Panel (Node Library)
- **Group**: `{ name: 'workflow-nodes', pull: 'clone', put: false }`
- **Clone function**: Custom clone function that generates unique IDs
- **Sort**: `false` (prevent reordering in library)
- **List**: Static node definitions (read-only)
#### Left Panel (Workflow)
- **Group**: `{ name: 'workflow-nodes', pull: true, put: true }`
- **Sort**: `true` (allow reordering)
- **List**: Reactive workflow nodes array
- **Animation**: 200ms for smooth transitions
### Node Cloning Logic
```typescript
const cloneNode = (original: FlowNode): FlowNode => {
return {
...original,
id: `${original.id}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
config: original.config ? { ...original.config } : undefined
};
};
```
### Event Handling
#### Workflow Change Handler
- Triggered when nodes are added, removed, or reordered
- Updates reactive state
- Logs changes for debugging
- Validates workflow integrity
## Data Models
### FlowNode Interface (Existing)
```typescript
interface FlowNode {
id: string; // Unique identifier
name: string; // Display name
type: string; // Node type category
taskId: string; // Task execution identifier
icon: string; // Icon name
backgroundColor: string;
shape: "square" | "circle" | "rounded";
textColor: string;
category: "控制类" | "设备类" | "媒体类" | "灯光类" | "延时类";
config?: Record<string, any>;
}
```
### Workflow State
```typescript
const workflowNodes = ref<FlowNode[]>([]);
```
## Error Handling
### Invalid Drop Operations
- Validate node structure before adding to workflow
- Handle malformed node data gracefully
- Provide user feedback for failed operations
### ID Collision Prevention
- Generate unique IDs using timestamp and random string
- Validate uniqueness before adding to workflow
- Handle edge cases where ID generation might fail
### Configuration Preservation
- Deep clone node configurations to prevent reference issues
- Validate configuration structure
- Handle missing or invalid configuration data
## Testing Strategy
### Unit Tests
1. **Clone Function Tests**
- Verify unique ID generation
- Confirm property preservation
- Test configuration deep cloning
2. **Drag and Drop Tests**
- Mock drag events
- Verify node addition to workflow
- Test reordering functionality
### Integration Tests
1. **Cross-Panel Transfer**
- Test dragging from right to left panel
- Verify node appears in workflow
- Confirm original node remains in library
2. **Workflow Management**
- Test node reordering
- Verify workflow state updates
- Test node removal functionality
### Visual Tests
1. **Drag Feedback**
- Verify visual drag indicators
- Test drop zone highlighting
- Confirm cursor changes during drag
2. **Node Rendering**
- Test node appearance in workflow
- Verify icon and color preservation
- Test configuration display
## Implementation Notes
### Key Changes Required
1. **Fix Group Configuration**
- Ensure consistent group names between panels
- Configure proper pull/put permissions
- Set up clone functionality for right panel
2. **Improve Clone Function**
- Generate more robust unique IDs
- Ensure deep cloning of configurations
- Handle edge cases in node structure
3. **Update Event Handlers**
- Properly handle workflow change events
- Add validation for dropped nodes
- Improve error handling and user feedback
4. **Visual Enhancements**
- Add drag state indicators
- Improve drop zone visual feedback
- Enhance cursor states during operations
### Performance Considerations
- Use efficient ID generation to avoid blocking
- Minimize re-renders during drag operations
- Optimize large workflow handling
- Cache node definitions for better performance

View File

@@ -0,0 +1,50 @@
# Requirements Document
## Introduction
This feature addresses the drag and drop functionality issue in the existing workflow visualization page where nodes from the right panel (node library) cannot be properly dragged to the left panel (task workflow). The current implementation has configuration issues with vuedraggable that prevent proper cloning and dropping of nodes between panels.
## Requirements
### Requirement 1
**User Story:** As a user, I want to drag nodes from the right panel node library to the left panel task workflow, so that I can create a visual workflow sequence.
#### Acceptance Criteria
1. WHEN I drag a node from the right panel THEN the system SHALL create a visual clone that follows my cursor
2. WHEN I drop a cloned node onto the left panel THEN the system SHALL add the node to the workflow list
3. WHEN I drop a cloned node onto the left panel THEN the system SHALL preserve all node properties (name, icon, colors, configuration)
4. WHEN I drop a cloned node onto the left panel THEN the system SHALL generate a unique ID for the new instance
5. IF the original node has editable configuration THEN the cloned node SHALL maintain the editable configuration capability
### Requirement 2
**User Story:** As a user, I want to reorder nodes within the left panel workflow, so that I can adjust the execution sequence of my tasks.
#### Acceptance Criteria
1. WHEN I drag a node within the left panel THEN the system SHALL allow reordering of workflow nodes
2. WHEN I reorder nodes in the left panel THEN the system SHALL maintain all node properties and configurations
3. WHEN I reorder nodes in the left panel THEN the system SHALL update the workflow sequence immediately
### Requirement 3
**User Story:** As a user, I want the right panel nodes to remain unchanged when dragging, so that I can reuse the same node type multiple times.
#### Acceptance Criteria
1. WHEN I drag a node from the right panel THEN the original node SHALL remain in the right panel
2. WHEN I drag a node from the right panel THEN the system SHALL create a copy rather than moving the original
3. WHEN I drag multiple instances of the same node type THEN each instance SHALL have a unique identifier
### Requirement 4
**User Story:** As a user, I want visual feedback during drag operations, so that I understand where I can drop nodes and what will happen.
#### Acceptance Criteria
1. WHEN I start dragging a node THEN the system SHALL provide visual feedback indicating drag state
2. WHEN I hover over a valid drop zone THEN the system SHALL highlight the drop zone
3. WHEN I hover over an invalid drop zone THEN the system SHALL indicate the zone is not droppable
4. WHEN dragging is in progress THEN the cursor SHALL change to indicate drag operation

View File

@@ -0,0 +1,36 @@
# Implementation Plan
- [x] 1. Fix vuedraggable group configuration for cross-panel transfers
- Update right panel draggable configuration to use proper group settings with clone functionality
- Update left panel draggable configuration to accept nodes from right panel
- Ensure group names are consistent between both panels
- _Requirements: 1.1, 1.2, 3.1, 3.2_
- [x] 2. Improve node cloning function for unique ID generation
- Enhance cloneNode function to generate more robust unique IDs using timestamp and random string
- Implement deep cloning of node configurations to prevent reference issues
- Add validation to ensure cloned nodes maintain all required properties
- _Requirements: 1.4, 3.3_
- [x] 3. Fix draggable list binding and event handling
- Correct the list binding for right panel draggable to prevent modification of original nodes
- Update workflow change handler to properly process added nodes
- Add validation for dropped nodes to ensure data integrity
- _Requirements: 1.2, 1.3, 2.2_
- [x] 4. Add visual feedback and drag state indicators
- Implement drag state visual feedback with appropriate cursor changes
- Add drop zone highlighting for better user experience
- Enhance drag operation visual indicators
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [x] 5. Test and validate drag and drop functionality
- Test dragging nodes from right panel to left panel
- Verify node reordering within left panel works correctly
- Validate that original nodes remain unchanged in right panel
- Test configuration preservation and editing functionality
- _Requirements: 1.1, 1.2, 1.3, 1.5, 2.1, 2.2, 3.1, 3.2, 3.3_

View File

@@ -0,0 +1,466 @@
<template>
<div class="h-screen flex flex-col bg-gray-50">
<!-- 顶部工具栏 -->
<div class="bg-white border-b border-gray-200 px-4 py-3">
<div class="flex items-center space-x-4">
<button
class="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
@click="goBack"
>
<ArrowLeft class="w-4 h-4" />
<span>返回</span>
</button>
<div class="flex-1" />
<div class="flex items-center space-x-3">
<button
class="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700"
@click="startTask"
>
<Play class="w-4 h-4" />
<span>开始任务</span>
</button>
<button
class="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
@click="saveTask"
>
<Save class="w-4 h-4" />
<span>保存任务</span>
</button>
<button
class="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-md hover:bg-purple-700"
@click="exportTask"
>
<Download class="w-4 h-4" />
<span>导出任务</span>
</button>
<button
class="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
@click="importTask"
>
<Upload class="w-4 h-4" />
<span>导入任务</span>
</button>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div class="flex-1 flex">
<!-- 左侧工作流面板 -->
<div
class="bg-white shadow-lg transition-all duration-300 overflow-hidden"
:class="{ 'w-0': !showLeftPanel, 'w-280px': showLeftPanel }"
>
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">
任务流
</h2>
<button
v-if="workflowNodes.length > 0"
class="text-xs text-red-600 hover:text-red-800"
@click="clearWorkflow"
>
清空
</button>
</div>
<div class="p-4 h-[calc(100vh-120px)] overflow-y-auto">
<draggable
v-model="workflowNodes"
:animation="200"
class="space-y-2 min-h-full"
@change="onWorkflowChange"
>
<template #item="{ element, index }">
<div
class="p-3 border rounded-lg cursor-move hover:shadow-md transition-shadow"
:style="{ backgroundColor: element.backgroundColor, color: element.textColor }"
>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<component
:is="getIconComponent(element.icon)"
class="w-4 h-4"
/>
<span class="text-sm font-medium">{{ element.name }}</span>
</div>
<div class="flex items-center space-x-1">
<button
v-if="element.config?.editable"
class="p-1 rounded hover:bg-white/20"
@click.stop="editNodeConfig(element)"
>
<Settings class="w-3 h-3" />
</button>
<button
class="p-1 rounded hover:bg-white/20"
@click.stop="removeNode(index)"
>
<X class="w-3 h-3" />
</button>
</div>
</div>
<div v-if="element.config?.duration" class="mt-2 text-xs opacity-80">
延时: {{ element.config.duration }}ms
</div>
</div>
</template>
</draggable>
<div
v-if="workflowNodes.length === 0" class="text-center py-8 transition-colors"
:class="dragOverWorkflow ? 'text-blue-600' : 'text-gray-400'"
>
<Move class="w-8 h-8 mx-auto mb-2" />
<p class="text-sm">
{{ dragOverWorkflow ? '松开鼠标添加节点' : '从右侧拖拽节点到此处' }}
</p>
</div>
</div>
</div>
<!-- 左侧面板折叠按钮 -->
<button
class="absolute left-0 top-1/2 transform -translate-y-1/2 bg-white border border-gray-200 rounded-r-md px-1 py-4 shadow-lg hover:bg-gray-50"
@click="showLeftPanel = !showLeftPanel"
>
<ChevronLeft v-if="showLeftPanel" class="w-4 h-4" />
<ChevronRight v-else class="w-4 h-4" />
</button>
<!-- 右侧节点库 -->
<div class="flex-1 p-6 bg-gray-50">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">
节点库
</h1>
<p class="text-gray-600 mt-1">
拖拽节点到左侧创建任务流
</p>
</div>
<!-- 节点库 - 改为点击按钮 -->
<div class="space-y-6">
<div v-for="(nodes, category) in categorizedNodes" :key="category">
<h3 class="text-lg font-semibold text-gray-800 mb-3">
{{ category }}
</h3>
<div class="grid grid-cols-4 gap-4">
<button
v-for="element in nodes"
:key="element.id"
class="p-4 border rounded-lg cursor-pointer hover:shadow-lg transition-all hover:scale-105 text-center"
:style="{ backgroundColor: element.backgroundColor, color: element.textColor }"
@click="addNodeToWorkflow(element)"
>
<component
:is="getIconComponent(element.icon)"
class="w-8 h-8 mx-auto mb-2"
/>
<p class="text-sm font-medium">
{{ element.name }}
</p>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 配置编辑对话框 -->
<div
v-if="editingNode"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
@click="cancelEdit"
>
<div
class="bg-white rounded-lg p-6 w-80 max-w-sm"
@click.stop
>
<h3 class="text-lg font-semibold mb-4">
编辑配置
</h3>
<div v-if="editingNode.config?.editable" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">
延时时间 (毫秒)
</label>
<input
v-model.number="editingNode.config.duration"
type="number"
min="100"
max="60000"
step="100"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
</div>
<div class="flex justify-end space-x-2">
<button
class="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
@click="cancelEdit"
>
取消
</button>
<button
class="px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700"
@click="saveEdit"
>
保存
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { FlowNode } from "~/components/flow-nodes";
import {
ArrowLeft,
ChevronLeft,
ChevronRight,
Clock,
Computer,
Download,
Monitor,
Move,
Pause,
Play,
Play as PlayIcon,
PlaySquare,
Power,
Save,
Settings,
SkipBack,
SkipForward,
Square,
Upload,
Volume2,
VolumeX,
X
} from "lucide-vue-next";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import draggable from "vuedraggable";
import { getNodesByCategory, nodeDefinitions } from "~/components/flow-nodes";
const router = useRouter();
const workflowNodes = ref<FlowNode[]>([]);
const showLeftPanel = ref(true);
const editingNode = ref<FlowNode | null>(null);
const isDragging = ref(false);
const dragOverWorkflow = ref(false);
const categorizedNodes = computed(() => getNodesByCategory());
const getIconComponent = (iconName: string) => {
const iconMap: Record<string, any> = {
power: Power,
computer: Computer,
monitor: Monitor,
play: PlayIcon,
pause: Pause,
square: Square,
"skip-back": SkipBack,
"skip-forward": SkipForward,
"volume-2": Volume2,
"volume-x": VolumeX,
"play-square": PlaySquare,
clock: Clock
};
return iconMap[iconName] || Square;
};
const cloneNode = (original: FlowNode): FlowNode => {
// 生成更健壮的唯一ID
const timestamp = Date.now();
const randomString = Math.random().toString(36).substr(2, 9);
const uniqueId = `${original.id}-${timestamp}-${randomString}`;
return {
...original,
id: uniqueId,
config: original.config ? { ...original.config } : undefined
};
};
// 点击添加节点到工作流
const addNodeToWorkflow = (node: FlowNode) => {
const clonedNode = cloneNode(node);
workflowNodes.value.push(clonedNode);
console.log("节点已添加到工作流:", clonedNode.name);
};
const removeNode = (index: number) => {
workflowNodes.value.splice(index, 1);
};
const clearWorkflow = () => {
workflowNodes.value = [];
};
const onWorkflowChange = (event: any) => {
console.log("工作流变化事件:", event);
// 验证工作流完整性
if (event.added) {
const addedNode = event.added.element;
console.log("添加的节点数据:", addedNode);
// 验证添加的节点具有所有必需属性
if (!addedNode.id || !addedNode.name || !addedNode.type) {
console.error("无效节点添加到工作流:", addedNode);
return;
}
console.log("节点已成功添加到工作流:", addedNode.name);
}
if (event.moved) {
console.log("工作流节点已重新排序");
}
if (event.removed) {
console.log("节点已从工作流中移除");
}
console.log("工作流已更新,当前节点数量:", workflowNodes.value.length);
console.log("当前工作流节点:", workflowNodes.value);
};
// 拖拽事件处理器用于视觉反馈
const onDragStart = (event: any) => {
isDragging.value = true;
console.log("开始拖拽", event);
};
const onDragEnd = (event: any) => {
isDragging.value = false;
dragOverWorkflow.value = false;
console.log("拖拽结束", event);
};
const onDragEnter = (event: any) => {
if (isDragging.value) {
dragOverWorkflow.value = true;
console.log("进入工作流区域", event);
}
};
const onDragLeave = (event: any) => {
dragOverWorkflow.value = false;
console.log("离开工作流区域", event);
};
const editNodeConfig = (node: FlowNode) => {
editingNode.value = { ...node };
};
const cancelEdit = () => {
editingNode.value = null;
};
const saveEdit = () => {
if (editingNode.value) {
const index = workflowNodes.value.findIndex((n) => n.id === editingNode.value!.id);
if (index !== -1) {
workflowNodes.value[index] = { ...editingNode.value };
}
cancelEdit();
}
};
const goBack = () => {
router.push("/");
};
const startTask = () => {
if (workflowNodes.value.length === 0) {
console.log("请先添加任务节点");
return;
}
const taskData = {
name: `任务_${new Date().toLocaleTimeString()}`,
nodes: workflowNodes.value.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
taskId: node.taskId,
config: node.config || {}
}))
};
console.log("执行", taskData);
console.log("任务已开始,请查看控制台");
};
const saveTask = () => {
const taskData = {
name: `任务_${new Date().toLocaleTimeString()}`,
nodes: workflowNodes.value.map((node) => ({
id: node.id,
name: node.name,
type: node.type,
taskId: node.taskId,
config: node.config || {}
}))
};
const blob = new Blob([JSON.stringify(taskData, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `任务_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
const exportTask = () => {
saveTask();
};
const importTask = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
if (data.nodes && Array.isArray(data.nodes)) {
workflowNodes.value = data.nodes.map((node: any) => {
const definition = nodeDefinitions[node.id.replace(/-\d+$/, "")];
return {
...definition,
id: node.id,
config: node.config || {}
};
});
}
} catch (error) {
console.error("文件格式错误", error);
}
};
reader.readAsText(file);
}
};
input.click();
};
</script>
<style scoped>
.w-280px {
width: 280px;
}
</style>

View File

@@ -0,0 +1,351 @@
export interface FlowNode {
id: string
name: string
type: string
taskId: string
icon: string
backgroundColor: string
shape: "square" | "circle" | "rounded"
textColor: string
category: "控制类" | "设备类" | "媒体类" | "灯光类" | "延时类"
config?: Record<string, any>
}
export const nodeDefinitions: Record<string, FlowNode> = {
// 控制类
"control-poweroff": {
id: "control-poweroff",
name: "一键关机",
type: "control",
taskId: "power_off_all",
icon: "power",
backgroundColor: "#ef4444",
shape: "rounded",
textColor: "#ffffff",
category: "控制类"
},
"control-poweron": {
id: "control-poweron",
name: "一键开机",
type: "control",
taskId: "power_on_all",
icon: "power",
backgroundColor: "#22c55e",
shape: "rounded",
textColor: "#ffffff",
category: "控制类"
},
// 设备类
"device-poweroff-host1": {
id: "device-poweroff-host1",
name: "关 [!] 主机1",
type: "device",
taskId: "power_off_host1",
icon: "computer",
backgroundColor: "#ef4444",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweron-host1": {
id: "device-poweron-host1",
name: "开 [!] 主机1",
type: "device",
taskId: "power_on_host1",
icon: "computer",
backgroundColor: "#22c55e",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweroff-host2": {
id: "device-poweroff-host2",
name: "关 [!] 主机2",
type: "device",
taskId: "power_off_host2",
icon: "computer",
backgroundColor: "#ef4444",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweron-host2": {
id: "device-poweron-host2",
name: "开 [!] 主机2",
type: "device",
taskId: "power_on_host2",
icon: "computer",
backgroundColor: "#22c55e",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweron-projector1": {
id: "device-poweron-projector1",
name: "开 [!] 投影1",
type: "device",
taskId: "power_on_projector1",
icon: "monitor",
backgroundColor: "#22c55e",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweroff-projector1": {
id: "device-poweroff-projector1",
name: "关 [!] 投影1",
type: "device",
taskId: "power_off_projector1",
icon: "monitor",
backgroundColor: "#ef4444",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweron-projector2": {
id: "device-poweron-projector2",
name: "开 [!] 投影2",
type: "device",
taskId: "power_on_projector2",
icon: "monitor",
backgroundColor: "#22c55e",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweroff-projector2": {
id: "device-poweroff-projector2",
name: "关 [!] 投影2",
type: "device",
taskId: "power_off_projector2",
icon: "monitor",
backgroundColor: "#ef4444",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweron-projector3": {
id: "device-poweron-projector3",
name: "开 [!] 投影3",
type: "device",
taskId: "power_on_projector3",
icon: "monitor",
backgroundColor: "#22c55e",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
"device-poweroff-projector3": {
id: "device-poweroff-projector3",
name: "关 [!] 投影3",
type: "device",
taskId: "power_off_projector3",
icon: "monitor",
backgroundColor: "#ef4444",
shape: "square",
textColor: "#ffffff",
category: "设备类"
},
// 媒体类
"media-button3": {
id: "media-button3",
name: "BUTTON3",
type: "media",
taskId: "media_button3",
icon: "square",
backgroundColor: "#3b82f6",
shape: "rounded",
textColor: "#ffffff",
category: "媒体类"
},
"media-play": {
id: "media-play",
name: "播放",
type: "media",
taskId: "media_play",
icon: "play",
backgroundColor: "#22c55e",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-pause": {
id: "media-pause",
name: "暂停",
type: "media",
taskId: "media_pause",
icon: "pause",
backgroundColor: "#f97316",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-stop": {
id: "media-stop",
name: "停止",
type: "media",
taskId: "media_stop",
icon: "square",
backgroundColor: "#ef4444",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-prev": {
id: "media-prev",
name: "上一曲",
type: "media",
taskId: "media_prev",
icon: "skip-back",
backgroundColor: "#8b5cf6",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-next": {
id: "media-next",
name: "下一曲",
type: "media",
taskId: "media_next",
icon: "skip-forward",
backgroundColor: "#8b5cf6",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-volume-down": {
id: "media-volume-down",
name: "音量减",
type: "media",
taskId: "media_volume_down",
icon: "volume-2",
backgroundColor: "#06b6d4",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-volume-up": {
id: "media-volume-up",
name: "音量加",
type: "media",
taskId: "media_volume_up",
icon: "volume-2",
backgroundColor: "#06b6d4",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-mute": {
id: "media-mute",
name: "静音",
type: "media",
taskId: "media_mute",
icon: "volume-x",
backgroundColor: "#f97316",
shape: "circle",
textColor: "#ffffff",
category: "媒体类"
},
"media-video1": {
id: "media-video1",
name: "视频一",
type: "media",
taskId: "media_video1",
icon: "play-square",
backgroundColor: "#6366f1",
shape: "rounded",
textColor: "#ffffff",
category: "媒体类"
},
"media-video2": {
id: "media-video2",
name: "视频二",
type: "media",
taskId: "media_video2",
icon: "play-square",
backgroundColor: "#6366f1",
shape: "rounded",
textColor: "#ffffff",
category: "媒体类"
},
"media-video3": {
id: "media-video3",
name: "视频三",
type: "media",
taskId: "media_video3",
icon: "play-square",
backgroundColor: "#6366f1",
shape: "rounded",
textColor: "#ffffff",
category: "媒体类"
},
"media-button1": {
id: "media-button1",
name: "BUTTON1",
type: "media",
taskId: "media_button1",
icon: "square",
backgroundColor: "#3b82f6",
shape: "rounded",
textColor: "#ffffff",
category: "媒体类"
},
// 延时类
"delay-2s": {
id: "delay-2s",
name: "延时2秒",
type: "delay",
taskId: "delay_2s",
icon: "clock",
backgroundColor: "#f59e0b",
shape: "rounded",
textColor: "#ffffff",
category: "延时类",
config: { duration: 2000 }
},
"delay-5s": {
id: "delay-5s",
name: "延时5秒",
type: "delay",
taskId: "delay_5s",
icon: "clock",
backgroundColor: "#f59e0b",
shape: "rounded",
textColor: "#ffffff",
category: "延时类",
config: { duration: 5000 }
},
"delay-custom": {
id: "delay-custom",
name: "自定义延时时间",
type: "delay",
taskId: "delay_custom",
icon: "clock",
backgroundColor: "#f59e0b",
shape: "rounded",
textColor: "#ffffff",
category: "延时类",
config: { duration: 1000, editable: true }
}
};
export const getNodesByCategory = () => {
const categories = {
: [] as FlowNode[],
: [] as FlowNode[],
: [] as FlowNode[],
: [] as FlowNode[],
: [] as FlowNode[]
};
Object.values(nodeDefinitions).forEach((node) => {
if (categories[node.category]) {
categories[node.category].push(node);
}
});
return categories;
};

View File

@@ -44,7 +44,7 @@
<div class="flex justify-center">
<UButton
:to="app.repo"
to="/workflows"
>
Star on GitHub
</UButton>

View File

@@ -0,0 +1,7 @@
<template>
<WorkflowEditor />
</template>
<script setup lang="ts">
// 简单的页面包装器使用WorkflowEditor组件
</script>

View File

@@ -30,9 +30,11 @@
"@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"@tauri-apps/plugin-store": "^2.3.0",
"lucide-vue-next": "^0.540.0",
"nuxt": "^4.0.0",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"vuedraggable": "^4.1.0",
"zod": "^4.0.5"
},
"devDependencies": {

43
pnpm-lock.yaml generated
View File

@@ -13,7 +13,7 @@ importers:
dependencies:
'@nuxt/ui-pro':
specifier: ^3.2.0
version: 3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)
version: 3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(sortablejs@1.14.0)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)
'@tauri-apps/api':
specifier: ^2.6.0
version: 2.6.0
@@ -32,6 +32,9 @@ importers:
'@tauri-apps/plugin-store':
specifier: ^2.3.0
version: 2.3.0
lucide-vue-next:
specifier: ^0.540.0
version: 0.540.0(vue@3.5.17(tslite@5.7.3))
nuxt:
specifier: ^4.0.0
version: 4.0.0(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@24.0.13)(@vue/compiler-sfc@3.5.17)(db0@0.3.2)(eslint@9.31.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.45.0)(terser@5.43.1)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(yaml@2.8.0)
@@ -41,6 +44,9 @@ importers:
vue-router:
specifier: ^4.5.1
version: 4.5.1(vue@3.5.17(tslite@5.7.3))
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.5.17(tslite@5.7.3))
zod:
specifier: ^4.0.5
version: 4.0.5
@@ -3924,6 +3930,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-vue-next@0.540.0:
resolution: {integrity: sha512-H7qhKVNKLyoFMo05pWcGSWBiLPiI3zJmWV65SuXWHlrIGIcvDer10xAyWcRJ0KLzIH5k5+yi7AGw/Xi1VF8Pbw==}
peerDependencies:
vue: '>=3.0.1'
luxon@3.7.1:
resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==}
engines: {node: '>=12'}
@@ -4991,6 +5002,9 @@ packages:
smob@1.5.0:
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -5613,6 +5627,11 @@ packages:
typescript:
optional: true
vuedraggable@4.1.0:
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
vue: ^3.0.1
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@@ -6928,12 +6947,12 @@ snapshots:
transitivePeerDependencies:
- magicast
'@nuxt/ui-pro@3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)':
'@nuxt/ui-pro@3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(sortablejs@1.14.0)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)':
dependencies:
'@ai-sdk/vue': 1.2.12(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)
'@nuxt/kit': 4.0.3(magicast@0.3.5)
'@nuxt/schema': 4.0.3
'@nuxt/ui': 3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)
'@nuxt/ui': 3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(sortablejs@1.14.0)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)
'@standard-schema/spec': 1.0.0
'@vueuse/core': 13.7.0(vue@3.5.17(tslite@5.7.3))
consola: 3.4.2
@@ -6996,7 +7015,7 @@ snapshots:
- vue
- vue-router
'@nuxt/ui@3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)':
'@nuxt/ui@3.3.2(@babel/parser@7.28.0)(@netlify/blobs@9.1.2)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(sortablejs@1.14.0)(tslite@5.7.3)(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.17(tslite@5.7.3)))(vue@3.5.17(tslite@5.7.3))(zod@4.0.5)':
dependencies:
'@iconify/vue': 5.0.0(vue@3.5.17(tslite@5.7.3))
'@internationalized/date': 3.8.2
@@ -7012,7 +7031,7 @@ snapshots:
'@tanstack/vue-table': 8.21.3(vue@3.5.17(tslite@5.7.3))
'@unhead/vue': 2.0.14(vue@3.5.17(tslite@5.7.3))
'@vueuse/core': 13.7.0(vue@3.5.17(tslite@5.7.3))
'@vueuse/integrations': 13.7.0(fuse.js@7.1.0)(jwt-decode@4.0.0)(vue@3.5.17(tslite@5.7.3))
'@vueuse/integrations': 13.7.0(fuse.js@7.1.0)(jwt-decode@4.0.0)(sortablejs@1.14.0)(vue@3.5.17(tslite@5.7.3))
colortranslator: 5.0.0
consola: 3.4.2
defu: 6.1.4
@@ -8157,7 +8176,7 @@ snapshots:
'@vueuse/shared': 13.7.0(vue@3.5.17(tslite@5.7.3))
vue: 3.5.17(tslite@5.7.3)
'@vueuse/integrations@13.7.0(fuse.js@7.1.0)(jwt-decode@4.0.0)(vue@3.5.17(tslite@5.7.3))':
'@vueuse/integrations@13.7.0(fuse.js@7.1.0)(jwt-decode@4.0.0)(sortablejs@1.14.0)(vue@3.5.17(tslite@5.7.3))':
dependencies:
'@vueuse/core': 13.7.0(vue@3.5.17(tslite@5.7.3))
'@vueuse/shared': 13.7.0(vue@3.5.17(tslite@5.7.3))
@@ -8165,6 +8184,7 @@ snapshots:
optionalDependencies:
fuse.js: 7.1.0
jwt-decode: 4.0.0
sortablejs: 1.14.0
'@vueuse/metadata@10.11.1': {}
@@ -9999,6 +10019,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-vue-next@0.540.0(vue@3.5.17(tslite@5.7.3)):
dependencies:
vue: 3.5.17(tslite@5.7.3)
luxon@3.7.1: {}
magic-regexp@0.10.0:
@@ -11496,6 +11520,8 @@ snapshots:
smob@1.5.0: {}
sortablejs@1.14.0: {}
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@@ -12127,6 +12153,11 @@ snapshots:
optionalDependencies:
typescript: tslite@5.7.3
vuedraggable@4.1.0(vue@3.5.17(tslite@5.7.3)):
dependencies:
sortablejs: 1.14.0
vue: 3.5.17(tslite@5.7.3)
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"main":{"identifier":"main","description":"Capabilities for the app window","local":true,"windows":["main","secondary"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":["-c",{"validator":"\\S+"}],"cmd":"sh","name":"exec-sh","sidecar":false}]},"notification:default","os:allow-platform","os:allow-arch","os:allow-family","os:allow-version","os:allow-locale","fs:allow-document-read","fs:allow-document-write","store:default","core:webview:allow-create-webview","core:webview:allow-create-webview-window"]}}
{ "main": { "identifier": "main", "description": "Capabilities for the app window", "local": true, "windows": ["main", "secondary"], "permissions": ["core:path:default", "core:event:default", "core:window:default", "core:app:default", "core:resources:default", "core:menu:default", "core:tray:default", "shell:allow-open", { "identifier": "shell:allow-execute", "allow": [{ "args": ["-c", { "validator": "\\S+" }], "cmd": "sh", "name": "exec-sh", "sidecar": false }] }, "notification:default", "os:allow-platform", "os:allow-arch", "os:allow-family", "os:allow-version", "os:allow-locale", "fs:allow-document-read", "fs:allow-document-write", "store:default", "core:webview:allow-create-webview", "core:webview:allow-create-webview-window"] } }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

0
test-drag-drop.md Normal file
View File