lj1559651600@163.com hace 4 meses
padre
commit
3574f24314
Se han modificado 39 ficheros con 4716 adiciones y 1367 borrados
  1. 48 3
      .idea/workspace.xml
  2. 14 0
      apps/web/components.d.ts
  3. 5 1
      apps/web/package.json
  4. 1 0
      apps/web/src/assets/icons/sparkle.svg
  5. 126 0
      apps/web/src/components/Chart/ExecutionChart.vue
  6. 1 1
      apps/web/src/components/SearchDialog/index.vue
  7. 80 20
      apps/web/src/components/Sidebar/index.vue
  8. 245 28
      apps/web/src/components/setter/DatabaseSetter.vue
  9. 85 27
      apps/web/src/components/setter/index.vue
  10. 12 0
      apps/web/src/composables/useI18n.ts
  11. 30 0
      apps/web/src/features/editorFooter/index.vue
  12. 115 0
      apps/web/src/features/selectTableModal/index.vue
  13. 44 0
      apps/web/src/i18n/index.ts
  14. 35 0
      apps/web/src/i18n/locales/zh-cn.ts
  15. 8 41
      apps/web/src/layouts/MainLayout.vue
  16. 55 55
      apps/web/src/main.ts
  17. 22 2
      apps/web/src/router/index.ts
  18. 96 0
      apps/web/src/store/modules/chat.store.ts
  19. 39 0
      apps/web/src/stores/dashboard.ts
  20. 576 0
      apps/web/src/views/Chat.vue
  21. 198 68
      apps/web/src/views/Dashboard.vue
  22. 158 264
      apps/web/src/views/Editor.vue
  23. 373 0
      apps/web/src/views/Statistics.vue
  24. 2 1
      packages/nodes/materials/start.ts
  25. 83 63
      packages/nodes/materials/toolbar.ts
  26. 17 5
      packages/ui/components/icon-button/IconButton.vue
  27. 1 0
      packages/ui/components/sticky-note/StickyNote.vue
  28. 2 0
      packages/workflow/src/Interface.ts
  29. 32 6
      packages/workflow/src/components/Canvas.vue
  30. 13 2
      packages/workflow/src/components/elements/CanvasControlBar.vue
  31. 231 175
      packages/workflow/src/components/elements/node-temp/CodeNode.vue
  32. 175 130
      packages/workflow/src/components/elements/node-temp/ConditionNode.vue
  33. 227 179
      packages/workflow/src/components/elements/node-temp/DataBaseNode.vue
  34. 60 46
      packages/workflow/src/components/elements/node-temp/EndNode.vue
  35. 114 84
      packages/workflow/src/components/elements/node-temp/HttpNode1.vue
  36. 60 47
      packages/workflow/src/components/elements/node-temp/StartNode.vue
  37. 10 1
      packages/workflow/src/components/elements/nodes/CanvasNode.vue
  38. 23 5
      packages/workflow/src/components/elements/nodes/render-types/NodeStickyNote.vue
  39. 1300 113
      pnpm-lock.yaml

+ 48 - 3
.idea/workspace.xml

@@ -5,9 +5,44 @@
   </component>
   <component name="ChangeListManager">
     <list default="true" id="41cd2400-3735-4e1a-9a77-7ea2e4a1c041" name="更改" comment="合并冲突">
-      <change afterPath="$PROJECT_DIR$/apps/web/store/componentsMap.ts" afterDir="false" />
-      <change beforePath="$PROJECT_DIR$/apps/web/src/components/setter/HttpSetter.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/components/setter/HttpSetter.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/assets/icons/sparkle.svg" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/components/Chart/ExecutionChart.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/composables/useI18n.ts" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/features/editorFooter/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/features/selectTableModal/index.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/i18n/index.ts" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/i18n/locales/zh-cn.ts" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/store/modules/chat.store.ts" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/stores/dashboard.ts" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/views/Chat.vue" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/apps/web/src/views/Statistics.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/components.d.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/components.d.ts" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/package.json" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/components/SearchDialog/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/components/SearchDialog/index.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/components/Sidebar/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/components/Sidebar/index.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/components/setter/DatabaseSetter.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/components/setter/DatabaseSetter.vue" afterDir="false" />
       <change beforePath="$PROJECT_DIR$/apps/web/src/components/setter/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/components/setter/index.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/layouts/MainLayout.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/layouts/MainLayout.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/main.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/main.ts" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/router/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/router/index.ts" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/views/Dashboard.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/views/Dashboard.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/apps/web/src/views/Editor.vue" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/views/Editor.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/nodes/materials/start.ts" beforeDir="false" afterPath="$PROJECT_DIR$/packages/nodes/materials/start.ts" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/nodes/materials/toolbar.ts" beforeDir="false" afterPath="$PROJECT_DIR$/packages/nodes/materials/toolbar.ts" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/ui/components/icon-button/IconButton.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/ui/components/icon-button/IconButton.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/ui/components/sticky-note/StickyNote.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/ui/components/sticky-note/StickyNote.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/Interface.ts" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/Interface.ts" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/Canvas.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/Canvas.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/CanvasControlBar.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/CanvasControlBar.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/CodeNode.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/CodeNode.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/ConditionNode.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/ConditionNode.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/DataBaseNode.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/DataBaseNode.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/EndNode.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/EndNode.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/HttpNode1.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/HttpNode1.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/StartNode.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/node-temp/StartNode.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/nodes/CanvasNode.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/nodes/CanvasNode.vue" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/packages/workflow/src/components/elements/nodes/render-types/NodeStickyNote.vue" beforeDir="false" afterPath="$PROJECT_DIR$/packages/workflow/src/components/elements/nodes/render-types/NodeStickyNote.vue" afterDir="false" />
       <change beforePath="$PROJECT_DIR$/pnpm-lock.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/pnpm-lock.yaml" afterDir="false" />
     </list>
     <option name="SHOW_DIALOG" value="false" />
@@ -30,6 +65,9 @@
   &quot;associatedIndex&quot;: 7
 }</component>
   <component name="ProjectId" id="38m4mc5E2lcGRA9UjefZP9wo9yp" />
+  <component name="ProjectLevelVcsManager" settingsEditedManually="true">
+    <ConfirmationsSetting value="2" id="Add" />
+  </component>
   <component name="ProjectViewState">
     <option name="hideEmptyMiddlePackages" value="true" />
     <option name="showLibraryContents" value="true" />
@@ -37,7 +75,7 @@
   <component name="PropertiesComponent"><![CDATA[{
   "keyToString": {
     "RunOnceActivity.ShowReadmeOnStart": "true",
-    "git-widget-placeholder": "feature-0123-Ai-workFlow",
+    "git-widget-placeholder": "正在合并 feature-0123-Ai-workFlow",
     "last_opened_file_path": "/Users/liujie/Desktop/shalu/shalu-agent-workflow",
     "node.js.detected.package.eslint": "true",
     "node.js.detected.package.tslint": "true",
@@ -49,6 +87,12 @@
     "vue.rearranger.settings.migration": "true"
   }
 }]]></component>
+  <component name="RecentsManager">
+    <key name="MoveFile.RECENT_KEYS">
+      <recent name="$PROJECT_DIR$/apps/web/src/components/SetterCommonComponents/Code" />
+      <recent name="$PROJECT_DIR$/apps/web/src/components/RunWorkflow" />
+    </key>
+  </component>
   <component name="SharedIndexes">
     <attachedChunks>
       <set>
@@ -69,6 +113,7 @@
       <updated>1769391572649</updated>
       <workItem from="1769391573741" duration="223000" />
       <workItem from="1769391800519" duration="10090000" />
+      <workItem from="1769518660683" duration="3027000" />
     </task>
     <task id="LOCAL-00001" summary="合并冲突">
       <option name="closed" value="true" />

+ 14 - 0
apps/web/components.d.ts

@@ -17,20 +17,29 @@ declare module 'vue' {
     CustomDropdown: typeof import('./src/components/CustomDropdown/index.vue')['default']
     DatabaseSetter: typeof import('./src/components/setter/DatabaseSetter.vue')['default']
     ElAside: typeof import('element-plus/es')['ElAside']
+    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
+    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElCollapse: typeof import('element-plus/es')['ElCollapse']
+    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElDorpdown: typeof import('element-plus/es')['ElDorpdown']
+    ElDrawer: typeof import('element-plus/es')['ElDrawer']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
     ElEmpty: typeof import('element-plus/es')['ElEmpty']
+    ElFooter: typeof import('element-plus/es')['ElFooter']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -39,16 +48,21 @@ declare module 'vue' {
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
+    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSlider: typeof import('element-plus/es')['ElSlider']
+    ElSplitter: typeof import('element-plus/es')['ElSplitter']
+    ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
+    ElTab: typeof import('element-plus/es')['ElTab']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    ExecutionChart: typeof import('./src/components/Chart/ExecutionChart.vue')['default']
     HttpSetter: typeof import('./src/components/setter/HttpSetter.vue')['default']
     InputVariables: typeof import('./src/components/SetterCommon/Code/InputVariables.vue')['default']
     OutputVariables: typeof import('./src/components/SetterCommon/Code/OutputVariables.vue')['default']

+ 5 - 1
apps/web/package.json

@@ -9,16 +9,20 @@
     "preview": "vite preview"
   },
   "dependencies": {
-    "@repo/nodes": "workspace:^",
     "@element-plus/icons-vue": "^2.3.2",
+    "@repo/nodes": "workspace:^",
+    "echarts": "^6.0.0",
     "element-plus": "^2.13.1",
     "normalize.css": "^8.0.1",
     "pinia": "^3.0.4",
+    "uuid": "^13.0.0",
     "vue": "^3.5.24",
+    "vue-element-plus-x": "^1.3.98",
     "vue-router": "4"
   },
   "devDependencies": {
     "@repo/nodes": "workspace:*",
+    "@repo/ui": "workspace:*",
     "@repo/workflow": "workspace:*",
     "@vitejs/plugin-vue": "^6.0.1",
     "@vue/tsconfig": "^0.8.1",

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
apps/web/src/assets/icons/sparkle.svg


+ 126 - 0
apps/web/src/components/Chart/ExecutionChart.vue

@@ -0,0 +1,126 @@
+<template>
+	<div ref="chartContainer" style="width: 100%; height: 300px"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
+import * as echarts from 'echarts'
+
+interface ChartData {
+	categories: string[]
+	successful: number[]
+	failed: number[]
+}
+
+interface Props {
+	data: ChartData
+	title?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+	title: '执行统计'
+})
+
+const chartContainer = ref<HTMLElement>()
+let chart: echarts.ECharts | null = null
+
+const initChart = () => {
+	if (!chartContainer.value) return
+
+	chart = echarts.init(chartContainer.value)
+
+	const option: echarts.EChartsOption = {
+		tooltip: {
+			trigger: 'axis',
+			axisPointer: {
+				type: 'shadow'
+			}
+		},
+		legend: {
+			data: ['Successful', 'Failed'],
+			top: 0
+		},
+		xAxis: {
+			type: 'category',
+			data: props.data.categories,
+			boundaryGap: true
+		},
+		yAxis: {
+			type: 'value',
+			splitLine: {
+				lineStyle: {
+					color: '#f0f0f0'
+				}
+			}
+		},
+		series: [
+			{
+				name: 'Successful',
+				data: props.data.successful,
+				type: 'bar',
+				itemStyle: {
+					color: '#13c2c2'
+				},
+				barWidth: '30%',
+				barGap: '30%'
+			},
+			{
+				name: 'Failed',
+				data: props.data.failed,
+				type: 'bar',
+				itemStyle: {
+					color: '#ff6b6b'
+				},
+				barWidth: '30%',
+				barGap: '30%'
+			}
+		],
+		grid: {
+			left: '3%',
+			right: '4%',
+			bottom: '3%',
+			top: '50px',
+			containLabel: true
+		}
+	}
+
+	chart.setOption(option)
+}
+
+const handleResize = () => {
+	chart?.resize()
+}
+
+watch(
+	() => props.data,
+	() => {
+		if (chart) {
+			const option: echarts.EChartsOption = {
+				xAxis: {
+					data: props.data.categories
+				},
+				series: [
+					{
+						data: props.data.successful
+					},
+					{
+						data: props.data.failed
+					}
+				]
+			}
+			chart.setOption(option)
+		}
+	},
+	{ deep: true }
+)
+
+onMounted(() => {
+	initChart()
+	window.addEventListener('resize', handleResize)
+})
+
+onBeforeUnmount(() => {
+	window.removeEventListener('resize', handleResize)
+	chart?.dispose()
+})
+</script>

+ 1 - 1
apps/web/src/components/SearchDialog/index.vue

@@ -38,7 +38,7 @@
 				<div class="search-items">
 					<div class="search-item action-item" @click="handleCreateWorkflow">
 						<span class="item-icon">+</span>
-						<span class="item-text">在个人环境中创建工作流程</span>
+						<span class="item-text">创建工作流程</span>
 					</div>
 				</div>
 			</div>

+ 80 - 20
apps/web/src/components/Sidebar/index.vue

@@ -12,7 +12,9 @@
 				}"
 			>
 				<el-dropdown placement="bottom-start" trigger="click">
-					<SvgIcon name="Plus" style="cursor: pointer" />
+					<span style="cursor: pointer">
+						<SvgIcon name="Plus" />
+					</span>
 					<template #dropdown>
 						<el-dropdown-menu>
 							<el-dropdown-item @click="createWorkflow">工作流程</el-dropdown-item>
@@ -30,7 +32,9 @@
 							<span class="desc">快速搜索</span>
 						</div>
 					</template>
-					<SvgIcon name="Search" @click="showSearchDialog = true" style="cursor: pointer" />
+					<span style="cursor: pointer" @click="showSearchDialog = true">
+						<SvgIcon name="Search" />
+					</span>
 				</el-tooltip>
 				<el-tooltip placement="bottom">
 					<template #content>
@@ -39,15 +43,17 @@
 							<span class="desc">折叠侧边栏</span>
 						</div>
 					</template>
-					<SvgIcon name="Fold" @click="toggle" />
+					<span style="cursor: pointer" @click="toggle">
+						<SvgIcon name="Fold" />
+					</span>
 				</el-tooltip>
 			</div>
 		</div>
 
-		<el-menu default-active="/" router class="el-menu-vertical-demo main-menu">
+		<el-menu :default-active="activeMenu" router class="el-menu-vertical-demo main-menu">
 			<el-menu-item index="/">
 				<el-tooltip v-if="collapsed" content="概览" placement="right">
-					<SvgIcon name="home" />
+					<span><SvgIcon name="home" /></span>
 				</el-tooltip>
 				<SvgIcon v-else name="home" />
 				<span v-if="!collapsed" class="label">概览</span>
@@ -55,7 +61,7 @@
 
 			<el-menu-item index="/chat">
 				<el-tooltip v-if="collapsed" content="聊天" placement="right">
-					<SvgIcon name="chatMessage" />
+					<span><SvgIcon name="chatMessage" /></span>
 				</el-tooltip>
 				<SvgIcon v-else name="chatMessage" />
 				<span v-if="!collapsed" class="label">聊天 <small class="beta">beta</small></span>
@@ -65,41 +71,61 @@
 		<div class="spacer"></div>
 
 		<div class="bottom-menu">
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/management' }"
+				@click="$router.push('/management')"
+			>
 				<el-tooltip v-if="collapsed" content="管理面板" placement="right">
-					<SvgIcon name="platForm" />
+					<span><SvgIcon name="platForm" /></span>
 				</el-tooltip>
 				<SvgIcon v-else name="platForm" />
 				<span v-if="!collapsed" class="label">管理面板</span>
 			</div>
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/templates' }"
+				@click="$router.push('/templates')"
+			>
 				<el-tooltip v-if="collapsed" content="模板" placement="right">
-					<SvgIcon name="box" />
+					<span><SvgIcon name="box" /></span>
 				</el-tooltip>
 				<SvgIcon v-else name="box" />
 				<span v-if="!collapsed" class="label">模板</span>
 			</div>
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/statistics' }"
+				@click="$router.push('/statistics')"
+			>
 				<el-tooltip v-if="collapsed" content="统计" placement="right">
-					<SvgIcon name="line" />
+					<span><SvgIcon name="line" /></span>
 				</el-tooltip>
 				<SvgIcon v-else name="line" />
 				<span v-if="!collapsed" class="label">统计</span>
 			</div>
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/help' }"
+				@click="$router.push('/help')"
+			>
 				<el-tooltip v-if="collapsed" content="帮助" placement="right">
-					<SvgIcon name="help" />
+					<span><SvgIcon name="help" /></span>
 				</el-tooltip>
 				<SvgIcon v-else name="help" />
 				<span v-if="!collapsed" class="label">帮助</span>
 			</div>
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/settings' }"
+				@click="$router.push('/settings')"
+			>
 				<el-tooltip v-if="collapsed" content="设置" placement="right">
-					<SvgIcon name="setting" />
+					<span><SvgIcon name="setting" /></span>
 				</el-tooltip>
 				<SvgIcon v-else name="setting" />
 				<span v-if="!collapsed" class="label">设置</span>
@@ -116,7 +142,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { useRouter } from 'vue-router'
 import SearchDialog from '../SearchDialog/index.vue'
 
@@ -124,6 +150,9 @@ const router = useRouter()
 const collapsed = ref(false)
 const showSearchDialog = ref(false)
 
+// 计算当前活跃的菜单项
+const activeMenu = computed(() => router.currentRoute.value.path)
+
 const toggle = () => {
 	collapsed.value = !collapsed.value
 }
@@ -219,6 +248,21 @@ const handleDropdownSelect = (item: any) => {
 	color: #ff6b6b;
 	background-color: #f0f0f0;
 }
+.top-icons > span {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.top-icons > span svg {
+	padding: 6px 8px;
+	margin: -6px -8px;
+	border-radius: 4px;
+	transition: all 0.2s ease;
+}
+.top-icons > span:hover svg {
+	color: #ff6b6b;
+	background-color: #f0f0f0;
+}
 
 .main-menu {
 	padding-top: 8px;
@@ -232,7 +276,7 @@ const handleDropdownSelect = (item: any) => {
 	height: 32px;
 }
 .el-menu-vertical-demo .el-menu-item:hover {
-	background: #fafafa;
+	background: #f0f0f0;
 }
 .el-menu-vertical-demo .el-menu-item.is-active {
 	background: #f0f0f0 !important;
@@ -249,7 +293,7 @@ const handleDropdownSelect = (item: any) => {
 	flex: 1 1 auto;
 }
 .bottom-menu {
-	padding: 8px 6px;
+	padding-bottom: 8px;
 	border-top: 1px solid #f2f2f2;
 	display: flex;
 	flex-direction: column;
@@ -259,11 +303,19 @@ const handleDropdownSelect = (item: any) => {
 	display: flex;
 	align-items: center;
 	gap: 10px;
-	padding: 8px 12px;
+	padding: 8px 18px;
 	color: #333;
+	cursor: pointer;
+	border-radius: 4px;
+	transition: all 0.2s ease;
 }
 .bottom-item:hover {
 	background: #fafafa;
+	color: #ff6b6b;
+}
+.bottom-item.active {
+	background: #fafafa;
+	color: #ff6b6b;
 }
 .sidebar.collapsed .label {
 	display: none;
@@ -276,6 +328,11 @@ const handleDropdownSelect = (item: any) => {
 }
 .sidebar.collapsed .top-icons {
 	gap: 10px;
+	flex-direction: column;
+}
+.sidebar.collapsed .top-icons > span {
+	width: 100%;
+	justify-content: center;
 }
 :deep(.el-button + .el-button) {
 	margin-left: 0;
@@ -316,4 +373,7 @@ const handleDropdownSelect = (item: any) => {
 		font-size: 11px;
 	}
 }
+:deep(.el-menu-item *) {
+	vertical-align: initial;
+}
 </style>

+ 245 - 28
apps/web/src/components/setter/DatabaseSetter.vue

@@ -6,41 +6,258 @@
  * @Describe: 数据设置器
 -->
 <script lang="ts" setup>
-import { ElDrawer, ElButton } from 'element-plus';
-import { Icon } from '@iconify/vue';
+import { computed, ref } from 'vue'
+import { IconButton, Icon } from '@repo/ui'
+import SelectTableModal from '@/features/selectTableModal/index.vue'
+
 const props = withDefaults(
-    defineProps<{
-        data: any,
-        visible: boolean,
-    }>(),
-    {
-        visible: false,
-        data: {}
-    }
-);
+	defineProps<{
+		data: any
+		visible: boolean
+	}>(),
+	{
+		visible: false,
+		data: {}
+	}
+)
 const emit = defineEmits<{
-    'update:visible': [value: boolean]
+	'update:visible': [value: boolean]
 }>()
+
+const selectTableModalRef = ref<InstanceType<typeof SelectTableModal> | null>(null)
+// 数据表
+const tableList = ref<any[]>([])
+// 查询字段
+const fieldList = ref<any[]>([])
+// 查询条件
+const conditionList = ref<any[]>([])
+// 排序方式
+const sortList = ref<any[]>([])
+// 查询上限
+const limit = ref<number>(100)
+// 输出
+const outputList = ref<any[]>([])
+
+// 异常处理
+const exceptionConfig = ref<{
+	timeout: number
+	retry: number
+	handler: number
+}>({
+	timeout: 60,
+	retry: 3,
+	handler: 0
+})
+
+const columns = computed(() => {
+	return (tableList.value?.[0]?.columns || []).filter(
+		(item: any) => !fieldList.value.find((field: any) => field.name === item.name)
+	)
+})
+
+const addDatabase = () => {
+	selectTableModalRef.value?.open()
+}
+
+const onAddTable = (table: unknown) => {
+	tableList.value = [table]
+}
 </script>
+
 <template>
-    <div class='content'>
-        <ElDrawer :model-value="visible" :show-close="false" size="25%" @close="emit('update:visible', false)">
+	<el-scrollbar class="w-full">
+		<el-collapse expand-icon-position="left" :model-value="['1', '2', '3', '4', '5', '6', '7']">
+			<el-collapse-item name="1">
+				<template #title="{ isActive }">
+					<div class="flex items-center justify-between">
+						<span>数据表</span>
+						<IconButton
+							size="large"
+							v-show="isActive"
+							icon="iconoir:plus"
+							link
+							@click.stop="addDatabase"
+						/>
+					</div>
+				</template>
+				<template v-for="table in tableList" :key="table">
+					<div
+						class="px-12px flex items-center justify-between cursor-pointer mb-12px p-12px hover:bg-gray-100 border border-solid border-gray-300 rounded-8px"
+					>
+						<div class="left flex items-center gap-24px">
+							<div class="icon w-40px h-40px bg-#ffb800 rounded-md grid place-items-center">
+								<Icon icon="iconoir:database-solid" color="#fff" height="24" width="24" />
+							</div>
+							<div>
+								<div class="text-#333 text-xs">{{ table.name }}</div>
+							</div>
+						</div>
+						<IconButton icon="ep:delete" size="small" />
+					</div>
+					<div class="flex gap-4px">
+						<el-tag
+							v-for="column in table.columns"
+							:key="column.name"
+							effect="light"
+							size="small"
+							>{{ column.name }}</el-tag
+						>
+					</div>
+				</template>
+
+				<el-empty v-if="!tableList.length" description="请添加一个数据表到此处" :image-size="40" />
+			</el-collapse-item>
+
+			<el-collapse-item name="2">
+				<template #title="{ isActive }">
+					<div class="flex items-center justify-between">
+						<span>查询字段查询字段</span>
+						<el-dropdown popper-class="w-200px" placement="bottom-end">
+							<IconButton size="large" v-show="isActive" icon="iconoir:plus" link @click.stop />
+							<template #dropdown>
+								<el-dropdown-item
+									v-for="column in columns"
+									:key="column.name"
+									@click="fieldList.push(column)"
+									>{{ column.name }}</el-dropdown-item
+								>
+							</template>
+						</el-dropdown>
+					</div>
+				</template>
+				<div v-for="field in fieldList" :key="field.name" class="flex gap-4px mb-12px">
+					<span>{{ field.name }}</span>
+					<el-tag effect="light">{{ field.type }}</el-tag>
+				</div>
+				<el-empty v-if="!fieldList.length" description="请添加查询字段" :image-size="40" />
+			</el-collapse-item>
+
+			<el-collapse-item title="查询条件" name="3"> </el-collapse-item>
+
+			<el-collapse-item name="5">
+				<template #title="{ isActive }">
+					<div class="flex items-center justify-between">
+						<span>排序方式</span>
+						<el-dropdown popper-class="w-200px" placement="bottom-end">
+							<IconButton size="large" v-show="isActive" icon="iconoir:plus" link @click.stop />
+							<template #dropdown>
+								<el-dropdown-item
+									v-for="column in fieldList"
+									:key="column.name"
+									@click="
+										sortList.push({
+											name: column.name,
+											type: column.type,
+											sort: 'asc'
+										})
+									"
+									>{{ column.name }}</el-dropdown-item
+								>
+							</template>
+						</el-dropdown>
+					</div>
+				</template>
+				<div v-for="field in sortList" :key="field.name" class="flex items-center gap-4px mb-12px">
+					<div class="flex-1 flex justify-between">
+						<span>{{ field.name }}</span>
+						<el-tag effect="light">{{ field.type }}</el-tag>
+					</div>
+					<el-select style="width: 120px" v-model="field.sort" placeholder="请选择">
+						<el-option label="升序" value="asc"></el-option>
+						<el-option label="降序" value="desc"></el-option>
+					</el-select>
+				</div>
+				<el-empty v-if="!sortList.length" description="请添加排序字段" :image-size="40" />
+			</el-collapse-item>
 
-            <template #header>
-                <h4>数据查询</h4>
-                <Icon icon="lucide:x" height="24" width="24"></Icon>
-            </template>
+			<el-collapse-item title="查询上限" name="6">
+				<el-input-number
+					v-model="limit"
+					:min="1"
+					:max="1000"
+					style="width: 100%"
+					controls-position="right"
+					placeholder="请输入查询上限"
+				/>
+			</el-collapse-item>
 
-            <!-- Drawer content -->
-            This is drawer content.
+			<el-collapse-item title="输出" name="7">
+				<el-collapse-item name="7-1">
+					<template #title="{ isActive }">
+						<div class="flex items-center justify-between">
+							<span>outputList</span>
+							<el-dropdown popper-class="w-200px" placement="bottom-end">
+								<IconButton size="large" v-show="isActive" icon="iconoir:plus" link @click.stop />
+								<template #dropdown>
+									<el-dropdown-item
+										v-for="column in fieldList"
+										:key="column.name"
+										@click="
+											outputList.push({
+												name: column.name,
+												type: column.type
+											})
+										"
+										>{{ column.name }}</el-dropdown-item
+									>
+								</template>
+							</el-dropdown>
+						</div>
+					</template>
+					<div
+						v-for="field in outputList"
+						:key="field.name"
+						class="flex items-center gap-4px mb-12px"
+					>
+						<span>{{ field.name }}</span>
+						<el-tag effect="light">{{ field.type }}</el-tag>
+					</div>
+				</el-collapse-item>
+			</el-collapse-item>
 
-            <!-- <template #footer>
-                <ElButton type="success" size="large" class="w-full" @click="emit('update:visible', false)">
-                    运行
-                </ElButton>
-            </template> -->
+			<el-collapse-item title="异常处理" name="8">
+				<el-form>
+					<el-row>
+						<el-col :span="8">
+							<el-form-item label="超时时间">
+								<el-input-number
+									v-model="exceptionConfig.timeout"
+									:min="1"
+									:max="1000"
+									style="width: 100%"
+									controls-position="right"
+									placeholder="请输入"
+									suffix="s"
+								/>
+							</el-form-item>
+						</el-col>
+						<el-col :span="8">
+							<el-form-item label="重试次数">
+								<el-input-number
+									v-model="exceptionConfig.retry"
+									:min="0"
+									:max="5"
+									style="width: 100%"
+									controls-position="right"
+									placeholder="请输入"
+									suffix="s"
+								/>
+							</el-form-item>
+						</el-col>
+						<el-col :span="8">
+							<el-form-item label="异常处理方式">
+								<el-select v-model="exceptionConfig.handler" placeholder="请选择">
+									<el-option label="中断流程" :value="0" />
+									<el-option label="返回设定内容" :value="1" />
+									<el-option label="执行异常流程" :value="2" />
+								</el-select>
+							</el-form-item>
+						</el-col>
+					</el-row>
+				</el-form>
+			</el-collapse-item>
+		</el-collapse>
+	</el-scrollbar>
 
-        </ElDrawer>
-    </div>
+	<SelectTableModal ref="selectTableModalRef" @add="onAddTable" />
 </template>
-<style lang="scss" scoped></style>

+ 85 - 27
apps/web/src/components/setter/index.vue

@@ -2,50 +2,88 @@
  * @Author: liuJie
  * @Date: 2026-01-25 22:13:06
  * @LastEditors: liuJie
- * @LastEditTime: 2026-01-27 18:24:02
+ * @LastEditTime: 2026-01-27 10:01:23
  * @Describe: file describe
 -->
 <script lang="ts" setup>
-import HttpSetter from './HttpSetter.vue';
-import { Icon } from "@iconify/vue";
-import { useComponentMapInspector } from "@/store"
+import { computed } from 'vue'
+import { Icon } from '@iconify/vue'
+import { useComponentMapInspector } from '@/store'
+import type { IWorkflow } from '@repo/workflow'
 
-const store = useComponentMapInspector()
+import HttpSetter from './HttpSetter.vue'
+import CodeSetter from './CodeSetter.vue'
+import ConditionSetter from './ConditionSetter.vue'
+import DatabaseSetter from './DatabaseSetter.vue'
+
+const setterMap = {
+	'http-node': HttpSetter,
+	'code-node': CodeSetter,
+	'condition-node': ConditionSetter,
+	'database-node': DatabaseSetter
+}
 
-console.log(store.$state.componentMap['http'], '节点映射')
+// 异步加载映射
+const store = useComponentMapInspector()
+// console.log(store.$state.componentMap['http'], '节点映射')
 
 interface Props {
-    nodeType: string
-    data: any, // 暂时定义
     visible: boolean
+		id: string
+		workflow: IWorkflow
 }
 const props = withDefaults(defineProps<Props>(), {
-    nodeType: '',
-    data: null,
-    visible: false
+    visible: false,
+		id: '',
 })
 const emit = defineEmits<{
-    'update:visible': [value: boolean]
+	'update:visible': [value: boolean]
 }>()
+
+const node = computed(() => {
+	return props.workflow.nodes.find((node) => node.id === props.id)
+})
+
+const setter = computed(() => {
+	return node.value?.type && setterMap[node.value.type as keyof typeof setterMap]
+})
+
 const closeDrawer = () => {
     emit('update:visible', false)
 }
 </script>
 <template>
-    <div class='setter'>
-        <div class="drawer shadow-2xl" :class="{ 'drawer--open': props.visible }">
+<!--	<div class='setter'>-->
+<!--		<div class="drawer shadow-2xl" :class="{ 'drawer&#45;&#45;open': props.visible }">-->
 
-            <div class="content">
-                <component :is="store.$state.componentMap['code']" :data="props.data" v-model:visible="props.visible">
-                </component>
-                <!--               <component :is="store.$state.componentMap[props.nodeType]" :data="props.data"></component>-->
-            </div>
-        </div>
-    </div>
+<!--			<div class="content">-->
+<!--				<component :is="store.$state.componentMap['code']" :data="props.data" v-model:visible="props.visible">-->
+<!--				</component>-->
+<!--				&lt;!&ndash;               <component :is="store.$state.componentMap[props.nodeType]" :data="props.data"></component>&ndash;&gt;-->
+<!--			</div>-->
+<!--		</div>-->
+<!--	</div>-->
+
+	<div class="setter">
+		<div class="drawer shadow-2xl" :class="{ 'drawer--open': props.visible && setter }">
+			<header class="text-gray-800">
+				<h4>{{ node?.label }}</h4>
+				<Icon
+					icon="lucide:x"
+					height="24"
+					width="24"
+					@click="closeDrawer"
+					class="cursor-pointer"
+				></Icon>
+			</header>
+			<div class="content">
+				<component :is="setter" :data="node?.data"></component>
+			</div>
+		</div>
+	</div>
 </template>
 <style lang="less" scoped>
 .setter {
-
     /* Drawer 主体 */
     .drawer {
         position: fixed;
@@ -70,10 +108,30 @@ const closeDrawer = () => {
         transform: translateX(0);
     }
 
-    /* 内容区 */
-    .drawer .content {
-        padding: 16px;
-        overflow: hidden;
-    }
+	/* Header */
+	.drawer header {
+		height: 56px;
+		padding: 0 16px;
+		border-bottom: 1px solid #eee;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+	}
+
+	/* 内容区 */
+	.drawer .content {
+		flex: 1;
+		// padding: 16px;
+		overflow-y: auto;
+	}
+
+	::v-deep(.el-collapse-item__header) {
+		box-sizing: border-box;
+		padding: 0 8px;
+	}
+	::v-deep(.el-collapse-item__content) {
+		box-sizing: border-box;
+		padding: 0 8px 12px 8px;
+	}
 }
 </style>

+ 12 - 0
apps/web/src/composables/useI18n.ts

@@ -0,0 +1,12 @@
+import { inject } from 'vue'
+import i18n from '@/i18n'
+
+export function useI18n() {
+  const $i18n = inject('i18n', i18n)
+
+  return {
+    t: (key: string) => $i18n.t(key),
+    setLocale: (locale: 'zh-cn' | 'en-us') => $i18n.setLocale(locale),
+    getLocale: () => $i18n.getLocale()
+  }
+}

+ 30 - 0
apps/web/src/features/editorFooter/index.vue

@@ -0,0 +1,30 @@
+<template>
+	<div class="flex w-full h-full overflow-hidden">
+		<div class="flex-1 flex flex-col">
+			<div
+				class="h-32px shrink-0 px-12px flex items-center justify-between border border-solid border-gray-200"
+				@click="onClick"
+			>
+				<span class="text-12px">日志</span>
+				<IconButton :icon="open ? 'lucide:chevron-down' : 'lucide:chevron-up'" link></IconButton>
+			</div>
+			<div class="flex-1 text-12px p-12px">日志内容...</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { IconButton } from '@repo/ui'
+
+const emit = defineEmits<{
+	toggle: [open: boolean]
+}>()
+
+const open = ref(false)
+
+const onClick = () => {
+	open.value = !open.value
+	emit('toggle', open.value)
+}
+</script>

+ 115 - 0
apps/web/src/features/selectTableModal/index.vue

@@ -0,0 +1,115 @@
+<template>
+	<el-dialog
+		v-model="visible"
+		append-to-body="body"
+		title="选择数据表"
+		:width="1000"
+		body-class="h-400px"
+	>
+		<div class="w-full h-full flex">
+			<div class="w-200px">
+				<el-menu class="w-200px h-full">
+					<el-menu-item index="1">业务表</el-menu-item>
+					<el-menu-item index="2">流程表</el-menu-item>
+					<el-menu-item index="3">外部数据库</el-menu-item>
+				</el-menu>
+			</div>
+			<div class="flex-1 p-12px">
+				<el-form inline>
+					<el-form-item label="表名">
+						<el-input placeholder="请输入表名"></el-input>
+					</el-form-item>
+					<el-form-item label="表描述">
+						<el-input placeholder="请输入表描述"></el-input>
+					</el-form-item>
+				</el-form>
+				<ul>
+					<li
+						v-for="table in tableList"
+						:key="table.name"
+						class="flex items-center justify-between cursor-pointer mb-12px p-12px hover:bg-gray-100"
+					>
+						<div class="left flex items-center gap-24px">
+							<div class="icon w-40px h-40px bg-#ffb800 rounded-md grid place-items-center">
+								<Icon icon="iconoir:database-solid" color="#fff" height="24" width="24" />
+							</div>
+							<div>
+								<div class="text-#333 text-xs">{{ table.name }}</div>
+								<div class="text-#666 text-xs">{{ table.desc }}</div>
+							</div>
+						</div>
+						<el-button type="primary" @click="onAdd(table)">添加</el-button>
+					</li>
+				</ul>
+			</div>
+		</div>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { Icon } from '@repo/ui'
+import { ref } from 'vue'
+
+const emit = defineEmits<{
+	add: [tableName: unknown]
+}>()
+const visible = ref(false)
+
+defineExpose({
+	open() {
+		visible.value = true
+	}
+})
+
+const tableList = [
+	{
+		name: 'customer_info',
+		desc: '客户信息表',
+		columns: [
+			{
+				name: 'id',
+				type: 'Nvarchar',
+				desc: 'ID'
+			},
+			{
+				name: 'customer_tenant_id',
+				type: 'Nvarchar',
+				desc: '租户ID'
+			},
+			{
+				name: 'customer_address',
+				type: 'Nvarchar',
+				desc: '客户地址'
+			}
+		]
+	},
+	{
+		name: 'device_info',
+		desc: '设备表',
+		columns: [
+			{
+				name: 'id',
+				type: 'Nvarchar',
+				desc: 'ID'
+			},
+			{
+				name: 'time_interval',
+				type: 'Nvarchar',
+				desc: '时间间隔'
+			},
+			{
+				name: 'iotdevice_describe',
+				type: 'Nvarchar',
+				desc: '设备描述'
+			}
+		]
+	}
+]
+
+const onAdd = (table: unknown) => {
+	emit('add', table)
+	visible.value = false
+}
+</script>
+
+<style scoped></style>

+ 44 - 0
apps/web/src/i18n/index.ts

@@ -0,0 +1,44 @@
+import zhCn from './locales/zh-cn'
+
+export type LocaleType = 'zh-cn' | 'en-us'
+
+const messages = {
+  'zh-cn': zhCn,
+  'en-us': {}
+}
+
+class I18n {
+  private currentLocale: LocaleType = 'zh-cn'
+
+  constructor() {
+    // 从本地存储获取语言设置
+    const savedLocale = localStorage.getItem('locale') as LocaleType
+    if (savedLocale && messages[savedLocale]) {
+      this.currentLocale = savedLocale
+    }
+  }
+
+  setLocale(locale: LocaleType) {
+    if (messages[locale]) {
+      this.currentLocale = locale
+      localStorage.setItem('locale', locale)
+    }
+  }
+
+  getLocale() {
+    return this.currentLocale
+  }
+
+  t(key: string): string {
+    const keys = key.split('.')
+    let value: any = messages[this.currentLocale]
+
+    for (const k of keys) {
+      value = value?.[k]
+    }
+
+    return typeof value === 'string' ? value : key
+  }
+}
+
+export default new I18n()

+ 35 - 0
apps/web/src/i18n/locales/zh-cn.ts

@@ -0,0 +1,35 @@
+export default {
+  // 公共
+  common: {
+    search: '搜索',
+    delete: '删除',
+    edit: '编辑',
+    add: '添加',
+    save: '保存',
+    cancel: '取消',
+    confirm: '确认',
+    close: '关闭',
+    back: '返回'
+  },
+  // 仪表板
+  dashboard: {
+    title: 'AI Agent',
+    subtitle: '概述',
+    workflows: '工作流程',
+    certificates: '证书',
+    executions: '执行',
+    variables: '变量',
+    dataTables: '数据表'
+  },
+  // 统计
+  statistics: {
+    title: '统计',
+    subtitle: '所有项目',
+    productionExecutions: '生产执行',
+    failedExecutions: '生产环境执行失败',
+    failureRate: '故障率',
+    timeSaved: '节省时间',
+    avgRuntime: '运行时间(平均)',
+    last7Days: '过去7天'
+  }
+}

+ 8 - 41
apps/web/src/layouts/MainLayout.vue

@@ -1,60 +1,27 @@
 <template>
 	<el-container style="height: 100vh">
-		<el-aside width="200px">
+		<el-aside width="auto">
 			<Sidebar />
 		</el-aside>
 
 		<el-container>
-			<el-header
-				v-if="!isWorkflowPage"
-				style="
-					height: 64px;
-					display: flex;
-					align-items: center;
-					padding: 0 16px;
-					justify-content: space-between;
-				"
-			>
-				<div style="display: flex; align-items: center; gap: 12px">
-					<div style="font-weight: 700; font-size: 18px">AI Agent</div>
-					<div style="color: #888; font-size: 13px">概述</div>
-				</div>
-
-				<div style="display: flex; align-items: center; gap: 12px">
-					<el-dropdown style="background: #ff6b6b" split-button type="primary" @click="handleClick">
-						创建工作流程
-						<template #dropdown>
-							<el-dropdown-menu>
-								<el-dropdown-item>创建凭证</el-dropdown-item>
-								<el-dropdown-item>创建变量</el-dropdown-item>
-								<el-dropdown-item>创建数据表</el-dropdown-item>
-							</el-dropdown-menu>
-						</template>
-					</el-dropdown>
-				</div>
-			</el-header>
-
-			<el-main style="padding: 16px; overflow: auto">
+			<el-main style="padding: 16px; overflow: auto" :style="mainStyle">
 				<router-view />
 			</el-main>
 		</el-container>
 	</el-container>
 </template>
 <script setup lang="ts">
-import { useRouter, useRoute } from 'vue-router'
-import { computed } from 'vue'
+import { ref, provide, type CSSProperties } from 'vue'
 import Sidebar from '@/components/Sidebar/index.vue'
 
-const router = useRouter()
-const route = useRoute()
+const mainStyle = ref<CSSProperties>()
 
-const isWorkflowPage = computed(() => {
-	return route.path.includes('/workflow/')
+provide('layout', {
+	setMainStyle(style: CSSProperties) {
+		mainStyle.value = style
+	}
 })
-
-const handleClick = () => {
-	router.push('/workflow/0')
-}
 </script>
 
 <style lang="less" scoped></style>

+ 55 - 55
apps/web/src/main.ts

@@ -5,71 +5,71 @@ import store from './store'
 import router from './router'
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import i18n from './i18n'
 import 'virtual:svg-icons-register'
 
 import 'normalize.css'
 import 'virtual:uno.css'
 
+// Theme colors configuration
+const themeColors = {
+	primary: '#ff6b6b',
+	'primary-light-3': '#ff8a8a',
+	'primary-light-5': '#ffa3a3',
+	'primary-light-7': '#ffbcbc',
+	'primary-light-8': '#ffd0d0',
+	'primary-light-9': '#ffe3e3',
+	'primary-dark-2': '#e55555',
+	success: '#67c23a',
+	'success-light-3': '#85ce61',
+	'success-light-5': '#a6e4a1',
+	'success-light-7': '#c6f6d5',
+	'success-light-8': '#d4edda',
+	'success-light-9': '#e1f5e3',
+	'success-dark-2': '#55b82d',
+	warning: '#e6a23c',
+	'warning-light-3': '#edb563',
+	'warning-light-5': '#f3d19e',
+	'warning-light-7': '#f9e4ba',
+	'warning-light-8': '#fce9cc',
+	'warning-light-9': '#fef0d9',
+	'warning-dark-2': '#d68830',
+	danger: '#f56c6c',
+	'danger-light-3': '#f78989',
+	'danger-light-5': '#f9a8a8',
+	'danger-light-7': '#fcc7c7',
+	'danger-light-8': '#fdd9d9',
+	'danger-light-9': '#feebeb',
+	'danger-dark-2': '#dd5960',
+	error: '#f56c6c',
+	'error-light-3': '#f78989',
+	'error-light-5': '#f9a8a8',
+	'error-light-7': '#fcc7c7',
+	'error-light-8': '#fdd9d9',
+	'error-light-9': '#feebeb',
+	info: '#909399',
+	'info-light-3': '#a6a9ad',
+	'info-light-5': '#b1b3b9',
+	'info-light-7': '#d3d4d6',
+	'info-light-8': '#e4e4e7',
+	'info-light-9': '#f2f2f5',
+	'info-dark-2': '#7a7d82'
+}
+
+const root = document.documentElement
+Object.entries(themeColors).forEach(([key, value]) => {
+	root.style.setProperty(`--el-color-${key}`, value)
+})
 
-// Set Element Plus theme colors
 const app = createApp(App)
 app.use(store)
 app.use(router)
-app.use(ElementPlus)
-const root = document.documentElement
-const primaryColor = '#ff6b6b'
-
-// Primary color and variants
-root.style.setProperty('--el-color-primary', primaryColor)
-root.style.setProperty('--el-color-primary-light-3', '#ff8a8a')
-root.style.setProperty('--el-color-primary-light-5', '#ffa3a3')
-root.style.setProperty('--el-color-primary-light-7', '#ffbcbc')
-root.style.setProperty('--el-color-primary-light-8', '#ffd0d0')
-root.style.setProperty('--el-color-primary-light-9', '#ffe3e3')
-root.style.setProperty('--el-color-primary-dark-2', '#e55555')
-
-// Success color (green-based)
-root.style.setProperty('--el-color-success', '#67c23a')
-root.style.setProperty('--el-color-success-light-3', '#85ce61')
-root.style.setProperty('--el-color-success-light-5', '#a6e4a1')
-root.style.setProperty('--el-color-success-light-7', '#c6f6d5')
-root.style.setProperty('--el-color-success-light-8', '#d4edda')
-root.style.setProperty('--el-color-success-light-9', '#e1f5e3')
-root.style.setProperty('--el-color-success-dark-2', '#55b82d')
-
-// Warning color (orange-based)
-root.style.setProperty('--el-color-warning', '#e6a23c')
-root.style.setProperty('--el-color-warning-light-3', '#edb563')
-root.style.setProperty('--el-color-warning-light-5', '#f3d19e')
-root.style.setProperty('--el-color-warning-light-7', '#f9e4ba')
-root.style.setProperty('--el-color-warning-light-8', '#fce9cc')
-root.style.setProperty('--el-color-warning-light-9', '#fef0d9')
-root.style.setProperty('--el-color-warning-dark-2', '#d68830')
-
-// Danger/Error color (red-based, complement primary)
-root.style.setProperty('--el-color-danger', '#f56c6c')
-root.style.setProperty('--el-color-danger-light-3', '#f78989')
-root.style.setProperty('--el-color-danger-light-5', '#f9a8a8')
-root.style.setProperty('--el-color-danger-light-7', '#fcc7c7')
-root.style.setProperty('--el-color-danger-light-8', '#fdd9d9')
-root.style.setProperty('--el-color-danger-light-9', '#feebeb')
-root.style.setProperty('--el-color-danger-dark-2', '#dd5960')
 
-// Error (same as danger)
-root.style.setProperty('--el-color-error', '#f56c6c')
-root.style.setProperty('--el-color-error-light-3', '#f78989')
-root.style.setProperty('--el-color-error-light-5', '#f9a8a8')
-root.style.setProperty('--el-color-error-light-7', '#fcc7c7')
-root.style.setProperty('--el-color-error-light-8', '#fdd9d9')
-root.style.setProperty('--el-color-error-light-9', '#feebeb')
+const currentLocale = zhCn
+app.use(ElementPlus, { locale: currentLocale })
 
-// Info color (blue-based)
-root.style.setProperty('--el-color-info', '#909399')
-root.style.setProperty('--el-color-info-light-3', '#a6a9ad')
-root.style.setProperty('--el-color-info-light-5', '#b1b3b9')
-root.style.setProperty('--el-color-info-light-7', '#d3d4d6')
-root.style.setProperty('--el-color-info-light-8', '#e4e4e7')
-root.style.setProperty('--el-color-info-light-9', '#f2f2f5')
-root.style.setProperty('--el-color-info-dark-2', '#7a7d82')
+app.provide('i18n', i18n)
+app.config.globalProperties.$t = (key: string) => i18n.t(key)
 
 app.mount('#app')

+ 22 - 2
apps/web/src/router/index.ts

@@ -3,14 +3,34 @@ import { createRouter, createWebHistory } from 'vue-router'
 const MainLayout = () => import('@/layouts/MainLayout.vue')
 const Dashboard = () => import('@/views/Dashboard.vue')
 const Editor = () => import('@/views/Editor.vue')
+const Statistics = () => import('@/views/Statistics.vue')
+const Chat = () => import('@/views/Chat.vue')
 
 const routes = [
 	{
 		path: '/',
 		component: MainLayout,
 		children: [
-			{ path: '', name: 'Dashboard', component: Dashboard },
-			{ path: 'workflow/:id', name: 'Editor', component: Editor }
+			{
+				path: '',
+				name: 'Dashboard',
+				component: Dashboard
+			},
+			{
+				path: 'statistics',
+				name: 'Statistics',
+				component: Statistics
+			},
+			{
+				path: 'chat',
+				name: 'Chat',
+				component: Chat
+			},
+			{
+				path: 'workflow/:id',
+				name: 'Editor',
+				component: Editor
+			}
 		]
 	}
 ]

+ 96 - 0
apps/web/src/store/modules/chat.store.ts

@@ -0,0 +1,96 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export interface Message {
+  id: string
+  content: string
+  role: 'user' | 'assistant'
+  timestamp: number
+}
+
+export interface Conversation {
+  id: string
+  title: string
+  createdAt: number
+  updatedAt: number
+  messages: Message[]
+}
+
+export const useChatStore = defineStore('chat', () => {
+  const conversations = ref<Conversation[]>([])
+  const activeConversationId = ref<string>('')
+
+  // 创建新对话
+  const createConversation = () => {
+    const id = `conv_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
+    const newConversation: Conversation = {
+      id,
+      title: `对话 ${conversations.value.length + 1}`,
+      createdAt: Date.now(),
+      updatedAt: Date.now(),
+      messages: []
+    }
+    conversations.value.unshift(newConversation)
+    activeConversationId.value = id
+    return id
+  }
+
+  // 获取活跃对话
+  const getActiveConversation = () => {
+    return conversations.value.find(c => c.id === activeConversationId.value)
+  }
+
+  // 添加消息
+  const addMessage = (conversationId: string, content: string, role: 'user' | 'assistant') => {
+    const conversation = conversations.value.find(c => c.id === conversationId)
+    if (conversation) {
+      const message: Message = {
+        id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
+        content,
+        role,
+        timestamp: Date.now()
+      }
+      conversation.messages.push(message)
+      conversation.updatedAt = Date.now()
+
+      // 更新标题(第一条消息)
+      if (conversation.messages.length === 1 && role === 'user') {
+        conversation.title = content.slice(0, 30) || '新对话'
+      }
+    }
+  }
+
+  // 删除对话
+  const deleteConversation = (id: string) => {
+    const index = conversations.value.findIndex(c => c.id === id)
+    if (index > -1) {
+      conversations.value.splice(index, 1)
+      if (activeConversationId.value === id) {
+        activeConversationId.value = conversations.value[0]?.id || ''
+      }
+    }
+  }
+
+  // 设置活跃对话
+  const setActiveConversation = (id: string) => {
+    activeConversationId.value = id
+  }
+
+  // 初始化:创建默认对话
+  const initializeChat = () => {
+    if (conversations.value.length === 0) {
+      createConversation()
+    }
+  }
+
+  return {
+    conversations,
+    activeConversationId,
+    createConversation,
+    getActiveConversation,
+    addMessage,
+    deleteConversation,
+    setActiveConversation,
+    initializeChat
+  }
+})

+ 39 - 0
apps/web/src/stores/dashboard.ts

@@ -0,0 +1,39 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useDashboardStore = defineStore('dashboard', () => {
+	const activeTab = ref<string>('flows')
+	const showVarDialog = ref(false)
+	const showTableDialog = ref(false)
+
+	const setActiveTab = (tab: string) => {
+		activeTab.value = tab
+	}
+
+	const openVarDialog = () => {
+		showVarDialog.value = true
+	}
+
+	const closeVarDialog = () => {
+		showVarDialog.value = false
+	}
+
+	const openTableDialog = () => {
+		showTableDialog.value = true
+	}
+
+	const closeTableDialog = () => {
+		showTableDialog.value = false
+	}
+
+	return {
+		activeTab,
+		setActiveTab,
+		showVarDialog,
+		openVarDialog,
+		closeVarDialog,
+		showTableDialog,
+		openTableDialog,
+		closeTableDialog
+	}
+})

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 576 - 0
apps/web/src/views/Chat.vue


+ 198 - 68
apps/web/src/views/Dashboard.vue

@@ -1,5 +1,45 @@
 <template>
-	<div>
+	<!-- 顶部栏 -->
+	<div
+		style="
+			height: 64px;
+			display: flex;
+			align-items: center;
+			padding: 0 16px;
+			justify-content: space-between;
+			background: #fff;
+			border-bottom: 1px solid #f0f0f0;
+		"
+	>
+		<div style="display: flex; align-items: center; gap: 12px">
+			<div style="font-weight: 700; font-size: 18px">AI Agent</div>
+			<div style="color: #888; font-size: 13px">概述</div>
+		</div>
+
+		<div style="display: flex; align-items: center; gap: 12px">
+			<el-dropdown
+				style="background: #ff6b6b"
+				split-button
+				type="primary"
+				@click="handleMenuClick(buttonConfig.text)"
+			>
+				{{ buttonConfig.text }}
+				<template #dropdown>
+					<el-dropdown-menu>
+						<el-dropdown-item
+							v-for="item in buttonConfig.items"
+							:key="item"
+							@click="handleMenuClick(item)"
+						>
+							{{ item }}
+						</el-dropdown-item>
+					</el-dropdown-menu>
+				</template>
+			</el-dropdown>
+		</div>
+	</div>
+
+	<div style="padding: 16px">
 		<el-card shadow="never" style="padding: 18px">
 			<el-row :gutter="16" justify="start">
 				<el-col :span="4" v-for="(card, idx) in cards" :key="idx">
@@ -20,7 +60,7 @@
 		</el-card>
 
 		<el-card style="margin-top: 16px">
-			<el-tabs v-model="activeTab">
+			<el-tabs v-model="dashboardStore.activeTab">
 				<el-tab-pane label="工作流程" name="flows"></el-tab-pane>
 				<el-tab-pane label="证书" name="certs"></el-tab-pane>
 				<el-tab-pane label="执行" name="execs"></el-tab-pane>
@@ -50,7 +90,7 @@
 				</div>
 
 				<div>
-					<el-button type="text">筛选</el-button>
+					<el-button text>筛选</el-button>
 				</div>
 			</div>
 
@@ -215,11 +255,11 @@
 						<div style="font-size: 13px; color: #999; margin-bottom: 16px">
 							变量可用于存储可在多个工作流程中轻松引用的数据。
 						</div>
-						<el-button @click="showVarDialog = true">添加第一个变量</el-button>
+						<el-button @click="dashboardStore.openVarDialog">添加第一个变量</el-button>
 					</div>
 					<el-table v-else :data="filteredVariables" style="width: 100%; margin-top: 12px">
-						<el-table-column prop="key" label="钥匙" />
-						<el-table-column prop="value" label="值" />
+						<el-table-column prop="key" label="key" />
+						<el-table-column prop="value" label="值" />
 						<el-table-column prop="usage" label="使用语法" width="150">
 							<template #default="scope">
 								<el-tag type="info">{{ scope.row.usage }}</el-tag>
@@ -227,10 +267,10 @@
 						</el-table-column>
 						<el-table-column prop="scope" label="范围" width="100">
 							<template #default="scope">
-								<el-tag size="small">{{ scope.row.scope }}</el-tag>
+								<el-tag size="small">{{ scope.row.scope === 'scope' ? '全局' : '' }}</el-tag>
 							</template>
 						</el-table-column>
-						<el-table-column label="操作" width="120" align="right">
+						<el-table-column label="操作" width="200" align="right">
 							<template #default="scope">
 								<el-button text size="small" @click="editVariable(scope.row)">编辑</el-button>
 								<el-button text size="small" type="danger" @click="deleteVariable(scope.row.id)"
@@ -267,7 +307,7 @@
 						<div style="font-size: 13px; color: #999; margin-bottom: 20px">
 							使用数据表来保存执行结果、在工作流之间共享数据以及跨进程体估指标。
 						</div>
-						<el-button @click="showTableDialog = true">创建数据表</el-button>
+						<el-button @click="dashboardStore.openTableDialog">创建数据表</el-button>
 					</div>
 					<div v-else>
 						<div
@@ -348,7 +388,7 @@
 									</div>
 
 									<div style="display: flex; gap: 8px; align-items: center">
-										<el-tag size="small" type="info">个人的</el-tag>
+										<!-- <el-tag size="small" type="info">个人的</el-tag> -->
 										<el-dropdown>
 											<span class="el-dropdown-link">•••</span>
 											<template #dropdown>
@@ -378,53 +418,59 @@
 					gap: 12px;
 				"
 			>
-				<span style="color: #666; font-size: 13px">总计 {{ getTabData.length }}</span>
 				<el-pagination
 					background
 					:page-size="pageSize"
 					:current-page="currentPage"
+					:page-sizes="[10, 20, 50]"
 					@update:current-page="currentPage = $event"
+					@update:page-size="pageSize = $event"
 					:total="getTabData.length"
-					layout="prev, pager, next"
+					layout="total, prev, pager, next, sizes"
 				/>
-				<el-select v-model.number="pageSize" size="small" style="width: 100px">
-					<el-option label="10/页" :value="10" />
-					<el-option label="20/页" :value="20" />
-					<el-option label="50/页" :value="50" />
-				</el-select>
 			</div>
 		</el-card>
 
 		<!-- 新变量对话框 -->
-		<el-dialog v-model="showVarDialog" title="新变量" width="500px" @close="resetVarForm">
-			<el-form :model="varForm">
-				<el-form-item label="钥匙" required>
-					<el-input v-model="varForm.key" placeholder="请输入姓名" />
+		<el-dialog
+			v-model="dashboardStore.showVarDialog"
+			:title="varDialogTitle"
+			width="500px"
+			@close="resetVarForm"
+		>
+			<el-form ref="varFormRef" :model="varForm" :rules="varFormRules" label-position="top">
+				<el-form-item label="Key" prop="key">
+					<el-input v-model="varForm.key" placeholder="请输入key" />
 				</el-form-item>
-				<el-form-item label="价值">
+				<el-form-item label="值" prop="value">
 					<el-input v-model="varForm.value" type="textarea" placeholder="请输入一个值" rows="4" />
 				</el-form-item>
-				<el-form-item label="范围" required>
+				<el-form-item label="范围" prop="scope">
 					<el-select v-model="varForm.scope" placeholder="选择">
-						<el-option label="全局的" value="全局的" />
+						<el-option label="全局" value="scope" />
 					</el-select>
 				</el-form-item>
 			</el-form>
 			<template #footer>
 				<div style="text-align: right">
-					<el-button @click="showVarDialog = false">取消</el-button>
+					<el-button @click="dashboardStore.closeVarDialog">取消</el-button>
 					<el-button type="primary" @click="submitVariable">提交</el-button>
 				</div>
 			</template>
 		</el-dialog>
 
 		<!-- 创建新数据表对话框 -->
-		<el-dialog v-model="showTableDialog" title="创建新数据表" width="500px" @close="resetTableForm">
-			<el-form :model="tableForm">
-				<el-form-item label="数据表名称" required>
+		<el-dialog
+			v-model="dashboardStore.showTableDialog"
+			title="创建新数据表"
+			width="500px"
+			@close="resetTableForm"
+		>
+			<el-form ref="tableFormRef" :model="tableForm" :rules="tableFormRules" label-position="top">
+				<el-form-item label="数据表名称" prop="name">
 					<el-input v-model="tableForm.name" placeholder="输入数据表名称" />
 				</el-form-item>
-				<el-form-item label="创建方式">
+				<el-form-item label="创建方式" prop="method">
 					<el-radio-group v-model="tableForm.method">
 						<el-radio value="from-scratch">从零开始</el-radio>
 						<el-radio value="import-csv">导入 CSV 文件</el-radio>
@@ -433,7 +479,7 @@
 			</el-form>
 			<template #footer>
 				<div style="text-align: right">
-					<el-button @click="showTableDialog = false">取消</el-button>
+					<el-button @click="dashboardStore.closeTableDialog">取消</el-button>
 					<el-button type="primary" @click="submitTable">创建</el-button>
 				</div>
 			</template>
@@ -444,10 +490,42 @@
 <script setup lang="ts">
 import { ref, computed } from 'vue'
 import { useRouter } from 'vue-router'
-import { Search } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search } from '@element-plus/icons-vue'
+import { useDashboardStore } from '@/stores/dashboard'
 
 const $router = useRouter()
+const dashboardStore = useDashboardStore()
+
+// 根据当前 tab 确定按钮文字和下拉菜单
+const buttonConfig = computed(() => {
+	const tab = dashboardStore.activeTab
+	const configs: Record<string, { text: string; items: string[] }> = {
+		flows: { text: '创建工作流程', items: ['创建凭证', '创建变量', '创建数据表'] },
+		certs: { text: '创建凭证', items: ['创建工作流程', '创建变量', '创建数据表'] },
+		execs: { text: '创建工作流程', items: ['创建凭证', '创建变量', '创建数据表'] },
+		vars: { text: '创建变量', items: ['创建工作流程', '创建凭证', '创建数据表'] },
+		tables: { text: '创建数据表', items: ['创建工作流程', '创建凭证', '创建变量'] }
+	}
+	return configs[tab] || { text: '创建工作流程', items: ['创建凭证', '创建变量', '创建数据表'] }
+})
+
+const handleMenuClick = (text: string) => {
+	const actionMap: Record<string, () => void> = {
+		创建工作流程: () => $router.push('/workflow/0'),
+		创建凭证: () => console.log('创建凭证'),
+		创建变量: () => {
+			dashboardStore.setActiveTab('vars')
+			dashboardStore.openVarDialog()
+		},
+		创建数据表: () => {
+			dashboardStore.setActiveTab('tables')
+			dashboardStore.openTableDialog()
+		}
+	}
+	const action = actionMap[text]
+	if (action) action()
+}
 
 const cards = [
 	{ title: '生产执行', value: 0 },
@@ -511,12 +589,15 @@ const executions = ref([
 	}
 ])
 
-const variables = ref([])
+const variables = ref<{ id: number; key: string; value: string; usage: string; scope: string }[]>(
+	[]
+)
 
-const tables = ref([])
+const tables = ref<
+	{ id: number; name: string; description: string; method: string; size: string; rows: string }[]
+>([])
 
 // 数据表相关
-const showTableDialog = ref(false)
 const tablePageInput = ref('')
 const tablePageSize = ref('50')
 const tableForm = ref({
@@ -531,68 +612,117 @@ const resetTableForm = () => {
 	}
 }
 
-const submitTable = () => {
-	if (!tableForm.value.name.trim()) {
-		ElMessage.error('请输入数据表名称')
-		return
-	}
-	const newTable = {
-		id: Math.max(...tables.value.map((t: any) => t.id), 0) + 1,
-		name: tableForm.value.name,
-		description: tableForm.value.method === 'from-scratch' ? '从零开始创建' : '从 CSV 导入',
-		method: tableForm.value.method
+const tableFormRef = ref()
+
+const tableFormRules = {
+	name: [
+		{ required: true, message: '请输入数据表名称', trigger: 'blur' },
+		{ min: 1, max: 100, message: '数据表名称长度为 1-100 个字符', trigger: 'blur' }
+	],
+	method: [{ required: true, message: '请选择创建方式', trigger: 'change' }]
+}
+
+const submitTable = async () => {
+	try {
+		await tableFormRef.value?.validate()
+		const newTable = {
+			id: Math.max(...tables.value.map((t: any) => t.id), 0) + 1,
+			name: tableForm.value.name,
+			description: tableForm.value.method === 'from-scratch' ? '从零开始创建' : '从 CSV 导入',
+			method: tableForm.value.method,
+			size: '0MB',
+			rows: '0'
+		}
+		tables.value.push(newTable)
+		dashboardStore.closeTableDialog()
+		resetTableForm()
+		ElMessage.success('数据表已创建')
+	} catch (error) {
+		// 验证失败,form 会自动显示错误信息
 	}
-	tables.value.push(newTable)
-	showTableDialog.value = false
-	resetTableForm()
-	ElMessage.success('数据表已创建')
 }
 
 const filter = ref('')
 const sort = ref('name')
-const activeTab = ref('flows')
 const pageSize = ref(10)
 
+// 使用 computed 来包装 dashboardStore.activeTab,以便简化模板中的引用
+const activeTab = computed(() => dashboardStore.activeTab)
+
 // 变量表单相关
-const showVarDialog = ref(false)
 const varFilter = ref('')
 const varSort = ref('name')
 const varPageSize = ref('25')
 const varForm = ref({
+	id: undefined as number | undefined,
 	key: '',
 	value: '',
-	scope: '全局的'
+	scope: 'scope'
 })
 
 const resetVarForm = () => {
 	varForm.value = {
+		id: undefined,
 		key: '',
 		value: '',
-		scope: '全局的'
+		scope: 'scope'
 	}
 }
 
-const submitVariable = () => {
-	if (!varForm.value.key.trim()) {
-		ElMessage.error('请输入钥匙')
-		return
-	}
-	const newVar = {
-		id: Math.max(...variables.value.map((v: any) => v.id), 0) + 1,
-		key: varForm.value.key,
-		value: varForm.value.value,
-		usage: `$vars.${varForm.value.key}`,
-		scope: varForm.value.scope
+const varFormRef = ref()
+
+const varFormRules = {
+	key: [
+		{ required: true, message: '请输入Key', trigger: 'blur' },
+		{ min: 1, max: 100, message: 'Key 长度为 1-100 个字符', trigger: 'blur' }
+	],
+	value: [],
+	scope: [{ required: true, message: '请选择范围', trigger: 'change' }]
+}
+
+const submitVariable = async () => {
+	try {
+		await varFormRef.value?.validate()
+		// 编辑已有变量
+		if (varForm.value.id) {
+			const idx = variables.value.findIndex((v: any) => v.id === varForm.value.id)
+			if (idx !== -1) {
+				variables.value[idx] = {
+					id: varForm.value.id,
+					key: varForm.value.key,
+					value: varForm.value.value,
+					usage: `$vars.${varForm.value.key}`,
+					scope: varForm.value.scope
+				}
+				dashboardStore.closeVarDialog()
+				resetVarForm()
+				ElMessage.success('变量已更新')
+				return
+			}
+		}
+
+		// 新增变量
+		const newVar = {
+			id: Math.max(...variables.value.map((v: any) => v.id), 0) + 1,
+			key: varForm.value.key,
+			value: varForm.value.value,
+			usage: `$vars.${varForm.value.key}`,
+			scope: varForm.value.scope
+		}
+		variables.value.push(newVar)
+		dashboardStore.closeVarDialog()
+		resetVarForm()
+		ElMessage.success('变量已添加')
+	} catch (error) {
+		// 验证失败,form 会自动显示错误信息
 	}
-	variables.value.push(newVar)
-	showVarDialog.value = false
-	resetVarForm()
-	ElMessage.success('变量已添加')
 }
 
+const varDialogTitle = computed(() => (varForm.value.id ? '编辑变量' : '新增'))
+
 const editVariable = (variable: any) => {
 	varForm.value = { ...variable, scope: variable.scope }
-	showVarDialog.value = true
+	dashboardStore.openVarDialog()
 }
 
 const deleteVariable = (id: number) => {

+ 158 - 264
apps/web/src/views/Editor.vue

@@ -1,286 +1,131 @@
 <template>
-	<div class="w-full h-full">
-		<Workflow
-			:workflow="workflow"
-			@click:node="handleNodeClick"
-			@create:node="handleNodeCreate"
-			@create:connection="onCreateConnection"
-			@drop="handleDrop"
-			@run="handleRunWorkflow"
-		/>
-		<RunWorkflow v-model:visible="runVisible" />
-		<Setter :data="{}" :nodeType="nodeType" v-model:visible="setterVisible" />
+	<div class="w-full h-full flex flex-col">
+		<div
+			class="h-60px shrink-0 border-b border-b-solid border-gray-200 flex items-center justify-between px-12px"
+		>
+			<div class="left flex items-center gap-4">
+				<el-breadcrumb separator="/">
+					<el-breadcrumb-item>Workspace</el-breadcrumb-item>
+					<el-breadcrumb-item>workflow_1</el-breadcrumb-item>
+				</el-breadcrumb>
+				<IconButton icon="iconoir:plus" type="primary" link>标签</IconButton>
+			</div>
+			<div class="right flex items-center gap-2">
+				<el-button type="default" size="small">发布</el-button>
+				<IconButton icon="lucide:history" type="default" link></IconButton>
+				<el-dropdown placement="bottom-end" popper-class="w-120px">
+					<IconButton icon="fluent-mdl2:more" type="default" link></IconButton>
+					<template #dropdown>
+						<el-dropdown-item>描述</el-dropdown-item>
+						<el-dropdown-item>复用</el-dropdown-item>
+						<el-dropdown-item>重命名</el-dropdown-item>
+						<el-dropdown-item divided>删除</el-dropdown-item>
+					</template>
+				</el-dropdown>
+			</div>
+		</div>
+		<el-splitter layout="vertical" class="flex-1">
+			<el-splitter-panel>
+				<Workflow
+					:workflow="workflow"
+					@click:node="handleNodeClick"
+					@create:node="handleNodeCreate"
+					@create:connection="onCreateConnection"
+					@drop="handleDrop"
+					@run="handleRunWorkflow"
+					@update:nodes:position="handleUpdateNodesPosition"
+					@update:node:attrs="handleUpdateNodeProps"
+					class="bg-#f5f5f5"
+				/>
+				<RunWorkflow v-model:visible="runVisible" />
+				<Setter
+					:id="nodeID"
+					:workflow="workflow"
+					@update:node:data="hangleUpdateNodeData"
+					v-model:visible="setterVisible"
+				/>
+			</el-splitter-panel>
+
+			<el-splitter-panel v-model:size.lazy="footerHeight" :min="32">
+				<EditorFooter @toggle="handleFooterToggle" />
+			</el-splitter-panel>
+		</el-splitter>
 	</div>
 </template>
 
 <script setup lang="ts">
+import { ref, inject, type CSSProperties, onBeforeUnmount } from 'vue'
 import { startNode, endNode, httpNode, conditionNode, databaseNode, codeNode } from '@repo/nodes'
 import { Workflow, type IWorkflow, type XYPosition, type Connection } from '@repo/workflow'
+import { v4 as uuid } from 'uuid'
+
 import Setter from '@/components/setter/index.vue'
 import RunWorkflow from '@/components/RunWorkflow/index.vue'
+import EditorFooter from '@/features/editorFooter/index.vue'
+
+import { IconButton } from '@repo/ui'
+
 import type { SourceType } from '@repo/nodes'
-import { ref } from 'vue'
 
-const workflow = ref<IWorkflow>({
-	id: '1',
-	nodes: [
-		startNode, // 初始化节点,
-		endNode, // 初始化节点,
-		// httpNode,
-		// conditionNode,
-		// databaseNode,
-		// codeNode
-		{
-			id: 'node-1',
-			type: 'canvas-node',
-			position: { x: 100, y: 100 },
-			data: {
-				version: ['1.0.0'],
-				displayName: '用户输入',
-				name: 'chart',
-				description: '通过用户输入开启流程处理',
-				icon: 'fluent:comment-multiple-28-regular',
-				iconColor: '#296dff',
-				inputs: [],
-				outputs: [
-					{
-						index: 0,
-						type: 'main'
-					}
-				]
-			}
-		},
-		{
-			id: 'node-2',
-			type: 'canvas-node',
-			position: { x: 400, y: 100 },
-			data: {
-				version: ['1.0.0'],
-				displayName: '条件判断',
-				name: 'if',
-				description: '通过条件判断拆分多个流程分支',
-				icon: 'roentgen:guidepost',
-				iconColor: '#108e49',
-				inputs: [
-					{
-						index: 0,
-						type: 'main'
-					}
-				],
-				outputs: [
-					{
-						index: 0,
-						type: 'main',
-						label: 'true'
-					},
-					{
-						index: 1,
-						type: 'main',
-						label: 'false'
-					}
-				],
-				outputNames: ['true', 'false']
-			}
-		},
-		{
-			id: 'node-3',
-			type: 'canvas-node',
-			width: 96,
-			height: 96,
-			position: { x: 600, y: 300 },
-			data: {
-				version: ['1.0.0'],
-				displayName: '条件判断',
-				name: 'if',
-				description: '通过条件判断拆分多个流程分支',
-				icon: 'roentgen:guidepost',
-				iconColor: '#108e49',
-				inputs: [
-					{
-						index: 0,
-						type: 'main'
-					}
-				],
-				outputs: [
-					{
-						index: 0,
-						type: 'main',
-						label: 'true'
-					},
-					{
-						index: 1,
-						type: 'main',
-						label: 'false'
-					}
-				],
-				outputNames: ['true', 'false']
-			}
-		},
-		{
-			id: 'node-note',
-			type: 'canvas-node',
-			position: { x: 600, y: 300 },
-			data: {
-				version: ['1.0.0'],
-				displayName: '条件判断',
-				name: 'if',
-				description: '通过条件判断拆分多个流程分支',
-				icon: 'roentgen:guidepost',
-				iconColor: '#108e49',
-				inputs: [],
-				outputs: [],
-				// 便签数据
-				renderType: 'stickyNote',
-				content:
-					'# 标题\n\n这是一些便签内容,可以使用 **Markdown** 语法进行格式化。\n\n- 列表项 1\n- 列表项 2\n\n[链接](https://example.com)',
-				width: 400,
-				height: 200,
-				color: '#d6f5e3'
-			}
-		}
-	],
-	edges: [
-		{
-			id: 'edge-1-2',
-			source: 'node-1',
-			target: 'node-2',
-			type: 'canvas-edge',
-			data: {
-				label: 'Edge 1-2'
-			}
-		}
-		// {
-		//     id: 'edge-1-2',
-		//     source: 'start-node',
-		//     target: 'http-node',
-		//     type: 'canvas-edge',
-		//     data: {
-		//         label: 'Edge 1-2'
-		//     }
-		// },
-		// {
-		//     id: 'edge-1-6',
-		//     source: 'http-node',
-		//     target: 'condition-node',
-		//     type: 'canvas-edge',
-		//     data: {
-		//         label: 'Edge 1-2'
-		//     }
-		// },
-		// {
-		//     id: 'edge-1-5',
-		//     source: 'condition-node',
-		//     target: 'data-node',
-		//     type: 'canvas-edge',
-		//     data: {
-		//         label: 'Edge 1-2'
-		//     }
-		// },
-		// {
-		//     id: 'edge-1-3',
-		//     source: 'database-node',
-		//     target: 'code-node',
-		//     type: 'canvas-edge',
-		//     data: {
-		//         label: 'Edge 1-2'
-		//     }
-		// },
+const layout = inject<{ setMainStyle: (style: CSSProperties) => void }>('layout')
 
-		// {
-		//     id: 'edge-1-4',
-		//     source: 'code-node',
-		//     target: 'end-node',
-		//     type: 'canvas-edge',
-		//     data: {
-		//         label: 'Edge 1-2'
-		//     }
-		// }
-	]
-	// 	id: '1',
-	// 	nodes: [
-	// 		{
-	// 			id: 'node-1',
-	// 			type: 'canvas-node',
-	// 			position: { x: 100, y: 100 },
-	// 			width: 96,
-	// 			height: 96,
-	// 			data: {
-	// 				version: ['1.0.0'],
-	// 				displayName: '用户输入',
-	// 				name: 'chart',
-	// 				description: '通过用户输入开启流程处理',
-	// 				icon: 'fluent:comment-multiple-28-regular',
-	// 				iconColor: '#296dff',
-	// 				inputs: [],
-	// 				outputs: [
-	// 					{
-	// 						index: 0,
-	// 						type: 'main'
-	// 					}
-	// 				]
-	// 			}
-	// 		},
-	// 		{
-	// 			id: 'node-2',
-	// 			type: 'canvas-node',
-	// 			width: 96,
-	// 			height: 96,
-	// 			position: { x: 400, y: 100 },
-	// 			data: {
-	// 				version: ['1.0.0'],
-	// 				displayName: '条件判断',
-	// 				name: 'if',
-	// 				description: '通过条件判断拆分多个流程分支',
-	// 				icon: 'roentgen:guidepost',
-	// 				iconColor: '#108e49',
-	// 				inputs: [
-	// 					{
-	// 						index: 0,
-	// 						type: 'main'
-	// 					}
-	// 				],
-	// 				outputs: [
-	// 					{
-	// 						index: 0,
-	// 						type: 'main',
-	// 						label: 'true'
-	// 					},
-	// 					{
-	// 						index: 1,
-	// 						type: 'main',
-	// 						label: 'false'
-	// 					}
-	// 				],
-	// 				outputNames: ['true', 'false']
-	// 			}
-	// 		}
-	// 	],
-	// 	edges: [
-	// 		{
-	// 			id: 'edge-1-2',
-	// 			source: 'node-1',
-	// 			target: 'node-2',
-	// 			type: 'canvas-edge',
-	// 			data: {
-	// 				label: 'Edge 1-2'
-	// 			}
-	// 		}
-	// 	]
+layout?.setMainStyle({
+	padding: '0px'
 })
-// 节点类型
-const nodeType = ref('')
+
+const footerHeight = ref(32)
+
+const workflow = ref<IWorkflow>({
+	id: uuid(),
+	nodes: [startNode, endNode],
+	edges: []
+})
+
+/**
+ * Editor
+ */
+const handleFooterToggle = (open: boolean) => {
+	footerHeight.value = open ? 200 : 32
+}
+
+/**
+ * Workflow
+ */
+const nodeID = ref('')
 const setterVisible = ref(false)
 const runVisible = ref(false)
 const handleRunWorkflow = () => {
 	runVisible.value = true
 	console.log('run workflow')
 }
-
-//  创建节点
-const handleNodeCreate = (value: SourceType) => {
+const handleNodeCreate = (value: SourceType | string) => {
 	console.log(value)
 
+	if (typeof value === 'string') {
+		if (value === 'stickyNote') {
+			workflow.value.nodes.push({
+				id: uuid(),
+				type: 'canvas-node',
+				zIndex: -1,
+				position: { x: 600, y: 300 },
+				data: {
+					version: ['1.0.0'],
+					inputs: [],
+					outputs: [],
+					renderType: 'stickyNote',
+					content: '注释内容,可以使用 **Markdown** 语法进行格式化, 双击进入编辑。',
+					width: 400,
+					height: 200,
+					color: '#fff5d6'
+				}
+			})
+		}
+		return
+	}
+
 	const nodeMap: Record<string, any> = {
+		start: startNode,
+		end: endNode,
 		http: httpNode,
 		condition: conditionNode,
 		code: codeNode,
@@ -290,14 +135,17 @@ const handleNodeCreate = (value: SourceType) => {
 
 	// 如果存在对应节点则添加
 	if (nodeToAdd) {
-		workflow.value.nodes.push(nodeToAdd)
+		workflow.value.nodes.push({
+			...nodeToAdd,
+			zIndex: 1,
+			id: uuid()
+		})
 	}
 	console.log(workflow.value.nodes, 'workflow.nodes')
 }
-// 点击节点
 const handleNodeClick = (id: string, position: XYPosition) => {
 	console.log('click node', id, position)
-	nodeType.value = id
+	nodeID.value = id
 	setterVisible.value = true
 }
 
@@ -305,6 +153,9 @@ const handleDrop = (position: XYPosition, event: DragEvent) => {
 	console.log('drag and drop at', position, event)
 }
 
+/**
+ * 创建连线
+ */
 const onCreateConnection = (connection: Connection) => {
 	console.log('create connection', connection)
 	const { source, target } = connection
@@ -319,4 +170,47 @@ const onCreateConnection = (connection: Connection) => {
 		})
 	}
 }
+
+/**
+ * 移动位置
+ */
+const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[]) => {
+	events?.forEach(({ id, position }) => {
+		const node = workflow.value.nodes.find((node) => node.id === id)
+		if (node) {
+			node.position = position
+		}
+	})
+}
+
+/**
+ * 修改节点数据
+ */
+const hangleUpdateNodeData = (id: string, data: any) => {
+	const node = workflow.value.nodes.find((node) => node.id === id)
+	if (node) {
+		node.data = {
+			...node.data,
+			...data
+		}
+	}
+}
+
+/**
+ * 修改节点属性
+ */
+const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
+	const node = workflow.value.nodes.find((node) => node.id === id)
+	if (node) {
+		if (node.data?.renderType === 'stickyNote') {
+			Object.assign(node.data, attrs)
+		} else {
+			Object.assign(node, attrs)
+		}
+	}
+}
+
+onBeforeUnmount(() => {
+	layout?.setMainStyle({})
+})
 </script>

+ 373 - 0
apps/web/src/views/Statistics.vue

@@ -0,0 +1,373 @@
+<template>
+	<!-- 顶部栏 -->
+	<div
+		style="
+			height: 64px;
+			display: flex;
+			align-items: center;
+			padding: 0 16px;
+			justify-content: space-between;
+			background: #fff;
+			border-bottom: 1px solid #f0f0f0;
+		"
+	>
+		<div style="display: flex; align-items: center; gap: 12px">
+			<div style="font-weight: 700; font-size: 18px">统计</div>
+			<div style="color: #888; font-size: 13px">所有项目</div>
+		</div>
+
+		<div style="display: flex; align-items: center; gap: 12px">
+			<el-date-picker
+				v-model="dateRange"
+				type="daterange"
+				range-separator="至"
+				start-placeholder="开始日期"
+				end-placeholder="结束日期"
+				format="YYYY年MM月DD日"
+			/>
+		</div>
+	</div>
+
+	<div style="padding: 16px">
+		<!-- 统计卡片 -->
+		<el-card shadow="never" style="padding: 18px; background-color: #fff">
+			<el-row :gutter="24" justify="start">
+				<el-col :span="4" v-for="(card, idx) in statsData" :key="idx">
+					<div
+						@click="selectCard(idx)"
+						:style="{
+							background:
+								selectedCardIdx === idx
+									? 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)'
+									: 'linear-gradient(135deg, #fff 0%, #fafafa 100%)',
+							padding: '20px 16px',
+							borderRadius: '8px',
+							boxShadow:
+								selectedCardIdx === idx
+									? '0 4px 12px rgba(0, 118, 255, 0.15)'
+									: '0 2px 8px rgba(0, 0, 0, 0.08)',
+							border: selectedCardIdx === idx ? '1px solid #0076ff' : '1px solid #f0f0f0',
+							textAlign: 'center',
+							cursor: 'pointer',
+							transition: 'all 0.3s ease'
+						}"
+					>
+						<div style="font-size: 12px; color: #999; margin-bottom: 8px">{{ card.title }}</div>
+						<div style="font-size: 32px; font-weight: 600; color: #262626">{{ card.value }}</div>
+						<div style="font-size: 12px; color: #bbb; margin-top: 8px">{{ card.subtitle }}</div>
+					</div>
+				</el-col>
+			</el-row>
+		</el-card>
+
+		<!-- 执行统计图表 -->
+		<el-card style="margin-top: 16px; padding: 18px">
+			<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px">
+				<div style="font-weight: 600">{{ currentChartTitle }}</div>
+				<div style="display: flex; gap: 8px">
+					<div style="display: flex; align-items: center; gap: 4px">
+						<div style="width: 12px; height: 12px; background: #13c2c2; border-radius: 2px"></div>
+						<span style="font-size: 12px; color: #666">Successful</span>
+					</div>
+					<div style="display: flex; align-items: center; gap: 4px">
+						<div style="width: 12px; height: 12px; background: #ff6b6b; border-radius: 2px"></div>
+						<span style="font-size: 12px; color: #666">Failed</span>
+					</div>
+				</div>
+			</div>
+			<ExecutionChart :data="currentChartData" />
+		</el-card>
+
+		<!-- 执行统计表格 -->
+		<el-card style="margin-top: 16px; padding: 18px">
+			<div style="font-weight: 600; margin-bottom: 16px; font-size: 14px">
+				{{ currentTableTitle }}
+			</div>
+			<el-table :data="filteredTableData" style="width: 100%" stripe border>
+				<el-table-column type="selection" width="50" />
+				<el-table-column prop="name" label="姓名" width="120" />
+				<el-table-column prop="totalExecutions" label="生产执行次数↓" width="140" />
+				<el-table-column prop="failedExecutions" label="生产环境执行失败数" width="160" />
+				<el-table-column prop="failureRate" label="故障率" width="100" />
+				<el-table-column prop="timeSaved" label="节省时间" width="100" />
+				<el-table-column prop="avgRuntime" label="运行时间(平均)" width="140" />
+				<el-table-column prop="projectName" label="项目名称" min-width="180" />
+			</el-table>
+
+			<!-- 分页器 -->
+			<div
+				style="
+					display: flex;
+					justify-content: flex-end;
+					align-items: center;
+					margin-top: 16px;
+					gap: 12px;
+				"
+			>
+				<el-pagination
+					background
+					:page-size="pageSize"
+					:current-page="currentPage"
+					:page-sizes="[10, 20, 50]"
+					@update:current-page="currentPage = $event"
+					@update:page-size="pageSize = $event"
+					:total="filteredTableData.length"
+					layout="total, prev, pager, next, sizes"
+				/>
+			</div>
+		</el-card>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import ExecutionChart from '@/components/Chart/ExecutionChart.vue'
+
+// 日期范围
+const dateRange = ref<[Date, Date] | null>([new Date('2026-01-20'), new Date('2026-01-27')])
+
+// 当前选中的卡片索引
+const selectedCardIdx = ref(0)
+
+// 统计数据
+const statsData = [
+	{ title: '生产执行', value: 12, subtitle: '过去7天' },
+	{ title: '生产环境执行失败', value: 3, subtitle: '过去7天' },
+	{ title: '故障率', value: '25%', subtitle: '过去7天' },
+	{ title: '节省时间', value: '8.2h', subtitle: '过去7天' },
+	{ title: '运行时间(平均)', value: '5.1s', subtitle: '过去7天' }
+]
+
+// 不同卡片对应的图表和表格数据
+const cardDataMap = [
+	{
+		// 生产执行
+		chartTitle: '生产执行 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [8, 12, 15, 24, 18, 10, 5],
+			failed: [1, 2, 1, 3, 2, 1, 0]
+		},
+		tableTitle: '生产执行明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目2',
+				totalExecutions: 18,
+				failedExecutions: 1,
+				failureRate: '5.6%',
+				timeSaved: '4.2h',
+				avgRuntime: '4.8s',
+				projectName: 'AI 分析项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.3s',
+				projectName: '图像识别项目'
+			}
+		]
+	},
+	{
+		// 生产环境执行失败
+		chartTitle: '生产环境执行失败 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [2, 3, 1, 2, 1, 0, 0],
+			failed: [1, 2, 1, 3, 2, 1, 0]
+		},
+		tableTitle: '生产环境失败执行明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 1,
+				failedExecutions: 1,
+				failureRate: '100%',
+				timeSaved: '0h',
+				avgRuntime: '2.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目2',
+				totalExecutions: 1,
+				failedExecutions: 1,
+				failureRate: '100%',
+				timeSaved: '0h',
+				avgRuntime: '1.8s',
+				projectName: 'AI 分析项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 1,
+				failedExecutions: 1,
+				failureRate: '100%',
+				timeSaved: '0h',
+				avgRuntime: '3.3s',
+				projectName: '图像识别项目'
+			}
+		]
+	},
+	{
+		// 故障率
+		chartTitle: '故障率 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [5, 8, 12, 18, 15, 8, 4],
+			failed: [1, 2, 1, 3, 2, 1, 0]
+		},
+		tableTitle: '故障率统计明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目2',
+				totalExecutions: 18,
+				failedExecutions: 1,
+				failureRate: '5.6%',
+				timeSaved: '4.2h',
+				avgRuntime: '4.8s',
+				projectName: 'AI 分析项目'
+			}
+		]
+	},
+	{
+		// 节省时间
+		chartTitle: '节省时间 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [12, 18, 20, 28, 22, 15, 8],
+			failed: [0, 1, 0, 2, 1, 0, 0]
+		},
+		tableTitle: '节省时间统计明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.3s',
+				projectName: '图像识别项目'
+			}
+		]
+	},
+	{
+		// 运行时间(平均)
+		chartTitle: '运行时间(平均) - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [6, 10, 14, 22, 18, 9, 4],
+			failed: [1, 1, 0, 2, 1, 1, 0]
+		},
+		tableTitle: '运行时间统计明细',
+		tableData: [
+			{
+				name: '项目2',
+				totalExecutions: 18,
+				failedExecutions: 1,
+				failureRate: '5.6%',
+				timeSaved: '4.2h',
+				avgRuntime: '4.8s',
+				projectName: 'AI 分析项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.3s',
+				projectName: '图像识别项目'
+			},
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			}
+		]
+	}
+]
+
+// 当前选中卡片的图表标题
+const currentChartTitle = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.chartTitle || '统计图表'
+})
+
+// 当前选中卡片的图表数据
+const currentChartData = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.chartData || cardDataMap[0].chartData
+})
+
+// 当前选中卡片的表格标题
+const currentTableTitle = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.tableTitle || '统计明细'
+})
+
+// 当前选中卡片的表格数据
+const filteredTableData = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.tableData || []
+})
+
+// 点击卡片
+const selectCard = (idx: number) => {
+	selectedCardIdx.value = idx
+	currentPage.value = 1
+}
+
+// 分页
+const pageSize = ref(10)
+const currentPage = ref(1)
+</script>
+
+<style scoped>
+:deep(.el-date-picker) {
+	width: 280px;
+}
+
+:deep(.el-card) {
+	border: 1px solid #f0f0f0;
+}
+
+:deep(.el-table) {
+	font-size: 13px;
+}
+
+:deep(.el-table__header-wrapper) {
+	background-color: #fafafa;
+}
+
+:deep(.el-pagination) {
+	margin-top: 16px;
+}
+</style>

+ 2 - 1
packages/nodes/materials/start.ts

@@ -19,4 +19,5 @@ export const startNode:IWorkflowNode = {
         inputs: [],
         outputs: []
     }
-}
+}
+

+ 83 - 63
packages/nodes/materials/toolbar.ts

@@ -6,69 +6,89 @@
  * @Describe: 工具栏配置
  */
 export interface MaterialToolType {
-    label: string;
-    id: string;
-    description: string;
-    source: Array<SourceType>;
-};
+	label: string
+	id: string
+	description: string
+	source: Array<SourceType>
+}
 
 export interface SourceType {
-    name: string;
-    type: string;
-    icon: string;
-    component: string;
-    id: string;
-    data?: any;
-    active: boolean;
-    isEdit: boolean;
-};
+	name: string
+	type: string
+	icon: string
+	component: string
+	id: string
+	data?: any
+	active: boolean
+	isEdit: boolean
+}
 
-
-export  const materialTools:MaterialToolType[] = [
-    {
-        label: '业务逻辑',
-        id: 'basic-nodes',
-        description: '业务节点',
-        source: [{
-                name: 'HTTP请求',
-                type: 'http',
-                icon: 'lucide:link',
-                component: 'Http',
-                id: 'http-node-id',
-                data: {},
-                active: false,
-                isEdit: false,
-            },
-            {
-                name: '条件分支',
-                type: 'condition',
-                icon: 'lucide:trending-up-down',
-                component: 'Condition',
-                id: 'condition-node-id',
-                data: {},
-                active: false,
-                isEdit: false,
-            },
-            {
-                name: '代码执行',
-                type: 'code',
-                icon: 'lucide:code',
-                component: 'Code',
-                id: 'code-node-id',
-                data: {},
-                active: false,
-                isEdit: false,
-            },
-            {
-                name: '数据查询',
-                type: 'database',
-                icon: 'lucide:database-zap',
-                component: 'Database',
-                id: 'data-query-node-id',
-                data: {},
-                active: false,
-                isEdit: false,
-            },
-        ],
-    }
-];
+export const materialTools: MaterialToolType[] = [
+	{
+		label: '业务逻辑',
+		id: 'basic-nodes',
+		description: '业务节点',
+		source: [
+			{
+				name: '开始',
+				type: 'start',
+				icon: 'lucide:play',
+				component: 'Start',
+				id: 'start-node',
+				data: {},
+				active: false,
+				isEdit: false
+			},
+			{
+				name: '结束',
+				type: 'end',
+				icon: 'lucide:unplug',
+				component: 'End',
+				id: 'end-node-id',
+				data: {},
+				active: false,
+				isEdit: false
+			},
+			{
+				name: 'HTTP请求',
+				type: 'http',
+				icon: 'lucide:link',
+				component: 'Http',
+				id: 'http-node-id',
+				data: {},
+				active: false,
+				isEdit: false
+			},
+			{
+				name: '条件分支',
+				type: 'condition',
+				icon: 'lucide:trending-up-down',
+				component: 'Condition',
+				id: 'condition-node-id',
+				data: {},
+				active: false,
+				isEdit: false
+			},
+			{
+				name: '代码执行',
+				type: 'code',
+				icon: 'lucide:code',
+				component: 'Code',
+				id: 'code-node-id',
+				data: {},
+				active: false,
+				isEdit: false
+			},
+			{
+				name: '数据查询',
+				type: 'database',
+				icon: 'lucide:database-zap',
+				component: 'Database',
+				id: 'data-query-node-id',
+				data: {},
+				active: false,
+				isEdit: false
+			}
+		]
+	}
+]

+ 17 - 5
packages/ui/components/icon-button/IconButton.vue

@@ -1,29 +1,41 @@
 <template>
-	<ElButton :type="type" :loading="loading" :disabled="disabled" :size="size" :class="{ square }">
-		<Icon :name="icon" :color="iconColor"></Icon>
+	<ElButton
+		:type="type"
+		:loading="loading"
+		:disabled="disabled"
+		:size="size"
+		:class="{ square }"
+		v-bind="$attrs"
+	>
+		<Icon :icon="icon" :color="iconColor" :width="iconSize" :height="iconSize"></Icon>
+		<slot>{{ label }}</slot>
 	</ElButton>
 </template>
 
 <script setup lang="ts">
 import { ElButton } from 'element-plus'
 import Icon from '../icon/Icon.vue'
+import { computed } from 'vue'
 
-withDefaults(
+const props = withDefaults(
 	defineProps<{
 		icon: string
 		iconColor?: string
-		size?: 'small' | 'medium' | 'large'
+		size?: 'small' | 'large' | 'default'
 		loading?: boolean
 		type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default'
 		disabled?: boolean
 		square?: boolean
+		label?: string
 	}>(),
 	{
-		size: 'medium',
+		size: 'default',
 		type: 'default',
 		loading: false
 	}
 )
+
+const iconSize = computed(() => (props.size === 'large' ? 20 : props.size === 'small' ? 12 : 16))
 </script>
 
 <style lang="less" scoped>

+ 1 - 0
packages/ui/components/sticky-note/StickyNote.vue

@@ -136,6 +136,7 @@ const onInputScroll = (event: WheelEvent) => {
 	position: absolute;
 	padding: 0.5rem 0.75rem 0;
 	overflow: hidden;
+	box-sizing: border-box;
 }
 </style>
 

+ 2 - 0
packages/workflow/src/Interface.ts

@@ -51,4 +51,6 @@ export type ConnectStartEvent = {
 	event?: MouseEvent | undefined
 } & OnConnectStartParams
 
+export type CanvasNodeMoveEvent = { id: string; position: IWorkflowNode['position'] }
+
 export { Position }

+ 32 - 6
packages/workflow/src/components/Canvas.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
-import type { IWorkflow, XYPosition, ConnectStartEvent } from '../Interface'
+import type { IWorkflow, XYPosition, ConnectStartEvent, CanvasNodeMoveEvent } from '../Interface'
 import type { SourceType } from '@repo/nodes'
-import type { NodeMouseEvent, Connection } from '@vue-flow/core'
+import type { NodeMouseEvent, Connection, NodeDragEvent } from '@vue-flow/core'
 
 import { ref } from 'vue'
 import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
@@ -24,7 +24,9 @@ defineOptions({
 })
 
 const emit = defineEmits<{
+	'update:node:size': [id: string, size: { width: number; height: number }]
 	'update:node:position': [id: string, position: XYPosition]
+	'update:nodes:position': [events: CanvasNodeMoveEvent[]]
 	'update:node:activated': [id: string, event?: MouseEvent]
 	'update:node:deactivated': [id: string]
 	'update:node:enabled': [id: string]
@@ -33,6 +35,7 @@ const emit = defineEmits<{
 	'update:node:parameters': [id: string, parameters: Record<string, unknown>]
 	'update:node:inputs': [id: string]
 	'update:node:outputs': [id: string]
+	'update:node:attrs': [id: string, attrs: Record<string, unknown>]
 	'update:logs-open': [open?: boolean]
 	'update:logs:input-open': [open?: boolean]
 	'update:logs:output-open': [open?: boolean]
@@ -80,6 +83,7 @@ const props = withDefaults(
 	}
 )
 
+const showMinimap = ref(false)
 const vueFlow = useVueFlow(props.id)
 
 const { viewport, viewportRef, project, zoomIn, zoomOut, fitView, zoomTo } = vueFlow
@@ -133,10 +137,30 @@ const onZoomToFit = () => {
 const onResetZoom = () => {
 	zoomTo(1)
 }
-const onAddNode = (value: SourceType) => {
+const onAddNode = (value: SourceType | string) => {
 	emit('create:node', value)
 }
 
+const onToggleMinimap = () => {
+	showMinimap.value = !showMinimap.value
+}
+
+function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
+	emit('update:nodes:position', events)
+}
+
+function onUpdateNodePosition(id: string, position: XYPosition) {
+	emit('update:node:position', id, position)
+}
+
+function onNodeDragStop(event: NodeDragEvent) {
+	onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position })))
+}
+
+function onUpdateNodeAttrs(id: string, attrs: Record<string, unknown>) {
+	emit('update:node:attrs', id, attrs)
+}
+
 /**
  * Connections / Edges
  */
@@ -184,7 +208,6 @@ function onClickConnectionAdd(connection: Connection) {
 const handleRun = () => {
 	emit('run')
 }
-console.log(props.nodes)
 </script>
 
 <template>
@@ -197,6 +220,7 @@ console.log(props.nodes)
 		snap-to-grid
 		:snap-grid="[16, 16]"
 		@node-click="onNodeClick"
+		@node-drag-stop="onNodeDragStop"
 		@drop="onDrop"
 		@connect="onConnect"
 		@connect-start="onConnectStart"
@@ -204,7 +228,7 @@ console.log(props.nodes)
 		v-bind="$attrs"
 	>
 		<template #node-canvas-node="nodeProps">
-			<CanvasNode v-bind="nodeProps" />
+			<CanvasNode v-bind="nodeProps" @move="onUpdateNodePosition" @update="onUpdateNodeAttrs" />
 		</template>
 
 		<template #node-start-node="nodeProps">
@@ -245,10 +269,11 @@ console.log(props.nodes)
 		</template>
 
 		<MiniMap
+			v-show="showMinimap"
 			:height="120"
 			:width="180"
 			:node-border-radius="16"
-			class="bg-white bottom-40px!"
+			class="bottom-40px! bg-#f5f5f5 border border-solid border-gray-300"
 			position="bottom-left"
 			pannable
 			zoomable
@@ -261,6 +286,7 @@ console.log(props.nodes)
 			@reset-zoom="onResetZoom"
 			@add-node="onAddNode"
 			@run="handleRun"
+			@toggle-minimap="onToggleMinimap"
 		/>
 
 		<slot name="canvas-background" v-bind="{ viewport }">

+ 13 - 2
packages/workflow/src/components/elements/CanvasControlBar.vue

@@ -11,7 +11,8 @@ const emit = defineEmits<{
 	'zoom-to-fit': []
 	'tidy-up': []
 	'toggle-zoom-mode': []
-	'add-node': [value: SourceType]
+	'add-node': [value: SourceType | string]
+	'toggle-minimap': []
 	run: []
 }>()
 
@@ -31,12 +32,16 @@ function onZoomToFit() {
 	emit('zoom-to-fit')
 }
 
-function onAddNode(value: SourceType) {
+function onAddNode(value: SourceType | string) {
 	emit('add-node', value)
 }
 function onRun() {
 	emit('run')
 }
+
+function onToggleMinimap() {
+	emit('toggle-minimap')
+}
 </script>
 
 <template>
@@ -54,6 +59,12 @@ function onRun() {
 			<ElButton @click="onResetZoom">
 				<Icon icon="bx:reset" height="16" width="16" />
 			</ElButton>
+			<ElButton @click="onToggleMinimap" square>
+				<Icon icon="lucide:map" height="16" width="16" />
+			</ElButton>
+			<ElButton @click="onAddNode('stickyNote')" square>
+				<Icon icon="lucide:file-plus-corner" height="16" width="16" />
+			</ElButton>
 			<AddNode @add-node="onAddNode" />
 			<ElButton @click="onRun" type="success">
 				<Icon icon="lucide:play" height="16" width="16" class="mr-1" /> 执行

+ 231 - 175
packages/workflow/src/components/elements/node-temp/CodeNode.vue

@@ -12,46 +12,46 @@ import { Icon } from '@repo/ui'
 import { computed } from 'vue'
 
 interface CodeConfig {
-    language: 'javascript' | 'python' | 'groovy' | 'java'
-    content: string
-    inputVars?: string[]
-    outputVar?: string
+	language: 'javascript' | 'python' | 'groovy' | 'java'
+	content: string
+	inputVars?: string[]
+	outputVar?: string
 }
 
 interface Environment {
-    timeout?: number
-    memory?: number
+	timeout?: number
+	memory?: number
 }
 
 interface Props {
-    data: {
-        label?: string
-        description?: string
-        code?: CodeConfig
-        environment?: Environment
-        [key: string]: any
-    }
-    selected?: boolean
+	data: {
+		label?: string
+		description?: string
+		code?: CodeConfig
+		environment?: Environment
+		[key: string]: any
+	}
+	selected?: boolean
 }
 
 const props = withDefaults(defineProps<Props>(), {
-    selected: false
+	selected: false
 })
 
 // 语言图标映射
 const languageIcons: Record<string, string> = {
-    javascript: 'lucide:file-code',
-    python: 'lucide:file-code-2',
-    groovy: 'lucide:coffee',
-    java: 'lucide:coffee'
+	javascript: 'lucide:file-code',
+	python: 'lucide:file-code-2',
+	groovy: 'lucide:coffee',
+	java: 'lucide:coffee'
 }
 
 // 语言颜色映射
-const languageColors: Record<string, { bg: string, text: string, badge: string }> = {
-    javascript: { bg: '#fef3c7', text: '#f59e0b', badge: '#fbbf24' },
-    python: { bg: '#dbeafe', text: '#3b82f6', badge: '#60a5fa' },
-    groovy: { bg: '#e0e7ff', text: '#6366f1', badge: '#818cf8' },
-    java: { bg: '#fecaca', text: '#dc2626', badge: '#f87171' }
+const languageColors: Record<string, { bg: string; text: string; badge: string }> = {
+	javascript: { bg: '#fef3c7', text: '#f59e0b', badge: '#fbbf24' },
+	python: { bg: '#dbeafe', text: '#3b82f6', badge: '#60a5fa' },
+	groovy: { bg: '#e0e7ff', text: '#6366f1', badge: '#818cf8' },
+	java: { bg: '#fecaca', text: '#dc2626', badge: '#f87171' }
 }
 
 const language = computed(() => props.data.code?.language || 'javascript')
@@ -60,180 +60,236 @@ const languageColor = computed(() => languageColors[language.value] || languageC
 
 // 获取代码预览(前3行)
 const codePreview = computed(() => {
-    const code = props.data.code?.content || '// 编写代码'
-    const lines = code.split('\n').slice(0, 3)
-    return lines.join('\n')
+	const code = props.data.code?.content || '// 编写代码'
+	const lines = code.split('\n').slice(0, 3)
+	return lines.join('\n')
 })
 
 console.log(props.data)
 // 代码行数
 const codeLines = computed(() => {
-    const code = props.data.code?.content || ''
-    return code.split('\n').length
+	const code = props.data.code?.content || ''
+	return code.split('\n').length
 })
 </script>
 
 <template>
-    <div class="relative min-w-[280px] transition-all duration-300 ease-out hover:-translate-y-0.5"
-        :class="{ 'scale-105': selected }">
-        <!-- 节点主体 -->
-        <div class="bg-gradient-to-br from-white to-purple-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
-            :class="selected ? 'border-purple-500 shadow-purple-200 shadow-lg' : 'border-purple-300 hover:shadow-lg hover:shadow-purple-100'">
-            <!-- 左侧装饰条 -->
-            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-purple-500 to-purple-400 rounded-l-xl">
-            </div>
-
-            <!-- 头部 -->
-            <div class="flex items-center gap-3 px-4 py-3 border-b border-purple-100">
-                <!-- 图标 -->
-                <div
-                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-purple-500 to-purple-400 rounded-lg shadow-md shadow-purple-200">
-                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
-                    <Icon icon="lucide:code-2" color="#ffffff" class="relative z-10" :size="20" />
-                </div>
-
-                <!-- 标题 -->
-                <div class="flex-1 min-w-0">
-                    <div class="text-sm font-semibold text-gray-800 truncate">
-                        {{ data.label || '代码执行' }}
-                    </div>
-                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
-                        {{ data.description }}
-                    </div>
-                </div>
-
-                <!-- 语言标签 -->
-                <div class="flex-shrink-0 px-2 py-1 rounded text-xs font-bold uppercase" :style="{
-                    backgroundColor: languageColor?.badge || '#fef3c7',
-                    color: 'white'
-                }">
-                    {{ language }}
-                </div>
-            </div>
-
-            <!-- 代码信息 -->
-            <div class="px-4 py-3 space-y-3">
-                <!-- 代码预览 -->
-                <div class="flex items-start gap-2">
-                    <Icon :icon="languageIcon" :color="languageColor?.text || '#6b7280'" :size="14"
-                        class="flex-shrink-0 mt-0.5" />
-                    <div class="flex-1 min-w-0">
-                        <div class="flex items-center justify-between mb-1">
-                            <span class="text-xs text-gray-500">代码片段</span>
-                            <span class="text-xs text-gray-400">{{ codeLines }} 行</span>
-                        </div>
-                        <div class="relative">
-                            <pre
-                                class="text-xs font-mono bg-gray-900 text-gray-100 px-3 py-2 rounded border border-gray-700 overflow-hidden max-h-16">{{ codePreview }}</pre>
-                            <div
-                                class="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-gray-900 to-transparent pointer-events-none">
-                            </div>
-                        </div>
-                    </div>
-                </div>
-
-                <!-- 输入变量 -->
-                <div v-if="data.code?.inputVars && data.code.inputVars.length > 0" class="flex items-start gap-2">
-                    <Icon icon="lucide:import" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
-                    <div class="flex-1 min-w-0">
-                        <div class="text-xs text-gray-500 mb-1">输入变量</div>
-                        <div class="flex flex-wrap gap-1">
-                            <span v-for="varName in data.code.inputVars" :key="varName"
-                                class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-200 font-mono">
-                                <Icon icon="lucide:arrow-down-to-line" :size="10" />
-                                {{ varName }}
-                            </span>
-                        </div>
-                    </div>
-                </div>
-
-                <!-- 输出变量 -->
-                <div v-if="data.code?.outputVar" class="flex items-start gap-2">
-                    <Icon icon="lucide:export" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
-                    <div class="flex-1 min-w-0">
-                        <div class="text-xs text-gray-500 mb-1">输出变量</div>
-                        <div
-                            class="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 text-xs rounded border border-green-200 font-mono">
-                            <Icon icon="lucide:arrow-up-from-line" :size="10" />
-                            {{ data.code.outputVar }}
-                        </div>
-                    </div>
-                </div>
-
-                <!-- 执行环境 -->
-                <div v-if="data.environment?.timeout || data.environment?.memory"
-                    class="flex items-center gap-3 pt-2 border-t border-purple-100">
-                    <div v-if="data.environment.timeout" class="flex items-center gap-1.5">
-                        <Icon icon="lucide:timer" color="#94a3b8" :size="12" />
-                        <span class="text-xs text-gray-600">
-                            <span class="text-gray-500">超时:</span>
-                            <span class="font-medium ml-1">{{ data.environment.timeout }}ms</span>
-                        </span>
-                    </div>
-                    <div v-if="data.environment.memory" class="flex items-center gap-1.5">
-                        <Icon icon="lucide:memory-stick" color="#94a3b8" :size="12" />
-                        <span class="text-xs text-gray-600">
-                            <span class="text-gray-500">内存:</span>
-                            <span class="font-medium ml-1">{{ data.environment.memory }}MB</span>
-                        </span>
-                    </div>
-                </div>
-            </div>
-
-            <!-- 底部状态栏 -->
-            <div class="flex items-center justify-between px-4 py-2 bg-purple-50/50 border-t border-purple-100">
-                <div class="flex items-center gap-2">
-                    <div class="flex items-center gap-1.5">
-                        <div class="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
-                        <span class="text-xs text-gray-500">就绪</span>
-                    </div>
-                    <!-- 运行状态 -->
-                    <div class="flex items-center gap-1 px-1.5 py-0.5 bg-purple-100 rounded">
-                        <Icon icon="lucide:zap" color="#9333ea" :size="10" />
-                        <span class="text-xs text-purple-700 font-medium">待执行</span>
-                    </div>
-                </div>
-                <div class="flex items-center gap-1">
-                    <Icon icon="lucide:play-circle" color="#94a3b8" :size="14"
-                        class="cursor-pointer hover:text-purple-500 transition-colors" title="运行代码" />
-                    <Icon icon="lucide:file-edit" color="#94a3b8" :size="14"
-                        class="cursor-pointer hover:text-purple-500 transition-colors" title="编辑代码" />
-                    <Icon icon="lucide:settings" color="#94a3b8" :size="14"
-                        class="cursor-pointer hover:text-purple-500 transition-colors" title="配置" />
-                </div>
-            </div>
-        </div>
-
-        <!-- 输入连接点 -->
-        <CanvasHandle handle-id="code-node-id" handle-type="target" :position="Position.Left" />
-        <!-- 输出连接点 -->
-        <CanvasHandle handle-id="code-node-id" handle-type="source" :position="Position.Right" />
-    </div>
+	<div
+		class="relative min-w-[280px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+		:class="{ 'scale-105': selected }"
+	>
+		<!-- 节点主体 -->
+		<div
+			class="bg-gradient-to-br from-white to-purple-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+			:class="
+				selected
+					? 'border-purple-500 shadow-purple-200 shadow-lg'
+					: 'border-purple-300 hover:shadow-lg hover:shadow-purple-100'
+			"
+		>
+			<!-- 左侧装饰条 -->
+			<div
+				class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-purple-500 to-purple-400 rounded-l-xl"
+			></div>
+
+			<!-- 头部 -->
+			<div class="flex items-center gap-3 px-4 py-3 border-b border-purple-100">
+				<!-- 图标 -->
+				<div
+					class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-purple-500 to-purple-400 rounded-lg shadow-md shadow-purple-200"
+				>
+					<div
+						class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
+					></div>
+					<Icon icon="lucide:code-2" color="#ffffff" class="relative z-10" :size="20" />
+				</div>
+
+				<!-- 标题 -->
+				<div class="flex-1 min-w-0">
+					<div class="text-sm font-semibold text-gray-800 truncate">
+						{{ data.label || '代码执行' }}
+					</div>
+					<div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+						{{ data.description }}
+					</div>
+				</div>
+
+				<!-- 语言标签 -->
+				<div
+					class="flex-shrink-0 px-2 py-1 rounded text-xs font-bold uppercase"
+					:style="{
+						backgroundColor: languageColor?.badge || '#fef3c7',
+						color: 'white'
+					}"
+				>
+					{{ language }}
+				</div>
+			</div>
+
+			<!-- 代码信息 -->
+			<div class="px-4 py-3 space-y-3">
+				<!-- 代码预览 -->
+				<div class="flex items-start gap-2">
+					<Icon
+						:icon="languageIcon"
+						:color="languageColor?.text || '#6b7280'"
+						:size="14"
+						class="flex-shrink-0 mt-0.5"
+					/>
+					<div class="flex-1 min-w-0">
+						<div class="flex items-center justify-between mb-1">
+							<span class="text-xs text-gray-500">代码片段</span>
+							<span class="text-xs text-gray-400">{{ codeLines }} 行</span>
+						</div>
+						<div class="relative">
+							<pre
+								class="text-xs font-mono bg-gray-900 text-gray-100 px-3 py-2 rounded border border-gray-700 overflow-hidden max-h-16"
+								>{{ codePreview }}</pre
+							>
+							<div
+								class="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-gray-900 to-transparent pointer-events-none"
+							></div>
+						</div>
+					</div>
+				</div>
+
+				<!-- 输入变量 -->
+				<div
+					v-if="data.code?.inputVars && data.code.inputVars.length > 0"
+					class="flex items-start gap-2"
+				>
+					<Icon icon="lucide:import" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+					<div class="flex-1 min-w-0">
+						<div class="text-xs text-gray-500 mb-1">输入变量</div>
+						<div class="flex flex-wrap gap-1">
+							<span
+								v-for="varName in data.code.inputVars"
+								:key="varName"
+								class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-200 font-mono"
+							>
+								<Icon icon="lucide:arrow-down-to-line" :size="10" />
+								{{ varName }}
+							</span>
+						</div>
+					</div>
+				</div>
+
+				<!-- 输出变量 -->
+				<div v-if="data.code?.outputVar" class="flex items-start gap-2">
+					<Icon icon="lucide:export" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+					<div class="flex-1 min-w-0">
+						<div class="text-xs text-gray-500 mb-1">输出变量</div>
+						<div
+							class="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 text-xs rounded border border-green-200 font-mono"
+						>
+							<Icon icon="lucide:arrow-up-from-line" :size="10" />
+							{{ data.code.outputVar }}
+						</div>
+					</div>
+				</div>
+
+				<!-- 执行环境 -->
+				<div
+					v-if="data.environment?.timeout || data.environment?.memory"
+					class="flex items-center gap-3 pt-2 border-t border-purple-100"
+				>
+					<div v-if="data.environment.timeout" class="flex items-center gap-1.5">
+						<Icon icon="lucide:timer" color="#94a3b8" :size="12" />
+						<span class="text-xs text-gray-600">
+							<span class="text-gray-500">超时:</span>
+							<span class="font-medium ml-1">{{ data.environment.timeout }}ms</span>
+						</span>
+					</div>
+					<div v-if="data.environment.memory" class="flex items-center gap-1.5">
+						<Icon icon="lucide:memory-stick" color="#94a3b8" :size="12" />
+						<span class="text-xs text-gray-600">
+							<span class="text-gray-500">内存:</span>
+							<span class="font-medium ml-1">{{ data.environment.memory }}MB</span>
+						</span>
+					</div>
+				</div>
+			</div>
+
+			<!-- 底部状态栏 -->
+			<div
+				class="flex items-center justify-between px-4 py-2 bg-purple-50/50 border-t border-purple-100"
+			>
+				<div class="flex items-center gap-2">
+					<div class="flex items-center gap-1.5">
+						<div class="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
+						<span class="text-xs text-gray-500">就绪</span>
+					</div>
+					<!-- 运行状态 -->
+					<div class="flex items-center gap-1 px-1.5 py-0.5 bg-purple-100 rounded">
+						<Icon icon="lucide:zap" color="#9333ea" :size="10" />
+						<span class="text-xs text-purple-700 font-medium">待执行</span>
+					</div>
+				</div>
+				<div class="flex items-center gap-1">
+					<Icon
+						icon="lucide:play-circle"
+						color="#94a3b8"
+						:size="14"
+						class="cursor-pointer hover:text-purple-500 transition-colors"
+						title="运行代码"
+					/>
+					<Icon
+						icon="lucide:file-edit"
+						color="#94a3b8"
+						:size="14"
+						class="cursor-pointer hover:text-purple-500 transition-colors"
+						title="编辑代码"
+					/>
+					<Icon
+						icon="lucide:settings"
+						color="#94a3b8"
+						:size="14"
+						class="cursor-pointer hover:text-purple-500 transition-colors"
+						title="配置"
+					/>
+				</div>
+			</div>
+		</div>
+
+		<!-- 输入连接点 -->
+		<CanvasHandle
+			handle-id="code-node-input"
+			type="target"
+			:connections-count="1"
+			:position="Position.Left"
+		/>
+		<!-- 输出连接点 -->
+		<CanvasHandle
+			handle-id="code-node-output"
+			type="source"
+			:connections-count="1"
+			:position="Position.Right"
+		/>
+	</div>
 </template>
 
 <style scoped>
 pre {
-    line-height: 1.4;
-    white-space: pre-wrap;
-    word-wrap: break-word;
+	line-height: 1.4;
+	white-space: pre-wrap;
+	word-wrap: break-word;
 }
 
-
 .overflow-y-auto::-webkit-scrollbar {
-    width: 4px;
+	width: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-track {
-    background: #f3e8ff;
-    border-radius: 4px;
+	background: #f3e8ff;
+	border-radius: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-thumb {
-    background: #9333ea;
-    border-radius: 4px;
+	background: #9333ea;
+	border-radius: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-thumb:hover {
-    background: #7e22ce;
+	background: #7e22ce;
 }
-</style>
+</style>

+ 175 - 130
packages/workflow/src/components/elements/node-temp/ConditionNode.vue

@@ -11,163 +11,208 @@ import CanvasHandle from '../handles/CanvasHandle.vue'
 import { Icon } from '@repo/ui'
 
 interface Condition {
-    id: string
-    name: string
-    expression: string
-    priority?: number
+	id: string
+	name: string
+	expression: string
+	priority?: number
 }
 
 interface Props {
-    data: {
-        label?: string
-        description?: string
-        conditions?: Condition[]
-        defaultBranch?: string
-        [key: string]: any
-    }
-    selected?: boolean
+	data: {
+		label?: string
+		description?: string
+		conditions?: Condition[]
+		defaultBranch?: string
+		[key: string]: any
+	}
+	selected?: boolean
 }
 
 const props = withDefaults(defineProps<Props>(), {
-    selected: false
+	selected: false
 })
 
 // 如果没有条件,至少显示默认分支
-const branches = props.data.conditions && props.data.conditions.length > 0
-    ? props.data.conditions
-    : [{ id: 'default', name: '默认分支', expression: 'true' }]
+const branches =
+	props.data.conditions && props.data.conditions.length > 0
+		? props.data.conditions
+		: [{ id: 'default', name: '默认分支', expression: 'true' }]
 
 // 计算每个分支 Handle 的位置
 const getBranchPosition = (index: number, total: number) => {
-    if (total === 1) return 50
-    const spacing = 70 / (total - 1) // 从 20% 到 90% 分布
-    return 15 + (index * spacing)
+	if (total === 1) return 50
+	const spacing = 70 / (total - 1) // 从 20% 到 90% 分布
+	return 15 + index * spacing
 }
 </script>
 
 <template>
-    <div class="relative min-w-[260px] transition-all duration-300 ease-out hover:-translate-y-0.5"
-        :class="{ 'scale-105': selected }">
-        <!-- 节点主体 -->
-        <div class="bg-gradient-to-br from-white to-orange-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
-            :class="selected ? 'border-orange-500 shadow-orange-200 shadow-lg' : 'border-orange-300 hover:shadow-lg hover:shadow-orange-100'">
-            <!-- 左侧装饰条 -->
-            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-orange-500 to-orange-400 rounded-l-xl">
-            </div>
-
-            <!-- 头部 -->
-            <div class="flex items-center gap-3 px-4 py-3 border-b border-orange-100">
-                <!-- 图标 - 菱形形状 -->
-                <div
-                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-orange-500 to-orange-400 rounded-lg shadow-md shadow-orange-200 rotate-45">
-                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
-                    <div class="-rotate-45">
-                        <Icon icon="lucide:git-branch" color="#ffffff" class="relative z-10" :size="20" />
-                    </div>
-                </div>
-
-                <!-- 标题 -->
-                <div class="flex-1 min-w-0">
-                    <div class="text-sm font-semibold text-gray-800 truncate">
-                        {{ data.label || '条件分支' }}
-                    </div>
-                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
-                        {{ data.description }}
-                    </div>
-                </div>
-
-                <!-- 分支数量标签 -->
-                <div
-                    class="flex-shrink-0 flex items-center gap-1 px-2 py-1 bg-orange-100 rounded text-xs font-medium text-orange-700">
-                    <Icon icon="lucide:split" color="#ea580c" :size="12" />
-                    <span>{{ branches.length }}</span>
-                </div>
-            </div>
-
-            <!-- 分支列表 -->
-            <div class="px-4 py-3 space-y-2 max-h-[200px] overflow-y-auto">
-                <div v-for="(condition, index) in branches" :key="condition.id"
-                    class="flex items-start gap-2 p-2 rounded-lg bg-orange-50/50 hover:bg-orange-100/50 transition-colors group">
-                    <!-- 分支序号 -->
-                    <div
-                        class="flex-shrink-0 flex items-center justify-center w-5 h-5 bg-orange-500 text-white text-xs font-bold rounded-full">
-                        {{ index + 1 }}
-                    </div>
-
-                    <!-- 分支信息 -->
-                    <div class="flex-1 min-w-0">
-                        <div class="flex items-center gap-1.5 mb-1">
-                            <span class="text-xs font-medium text-gray-700 truncate">
-                                {{ condition.name }}
-                            </span>
-                            <Icon v-if="condition.priority" icon="lucide:star" color="#f97316" :size="12"
-                                class="flex-shrink-0" />
-                        </div>
-                        <div class="text-xs text-gray-500 font-mono bg-white px-2 py-1 rounded truncate">
-                            {{ condition.expression }}
-                        </div>
-                    </div>
-
-                    <!-- 分支指示箭头 -->
-                    <div class="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
-                        <Icon icon="lucide:arrow-right" color="#f97316" :size="14" />
-                    </div>
-                </div>
-
-                <!-- 默认分支提示 -->
-                <div v-if="data.defaultBranch"
-                    class="flex items-center gap-2 p-2 rounded-lg bg-gray-50 border border-dashed border-gray-300">
-                    <Icon icon="lucide:shield-check" color="#94a3b8" :size="14" />
-                    <span class="text-xs text-gray-500">
-                        默认: <span class="font-medium text-gray-700">{{ data.defaultBranch }}</span>
-                    </span>
-                </div>
-            </div>
-
-            <!-- 底部状态栏 -->
-            <div class="flex items-center justify-between px-4 py-2 bg-orange-50/50 border-t border-orange-100">
-                <div class="flex items-center gap-1.5">
-                    <Icon icon="lucide:zap" color="#f97316" :size="12" />
-                    <span class="text-xs text-gray-500">条件判断</span>
-                </div>
-                <Icon icon="lucide:settings" color="#94a3b8" :size="14"
-                    class="cursor-pointer hover:text-orange-500 transition-colors" />
-            </div>
-        </div>
-
-        <!-- 输入连接点 -->
-        <CanvasHandle handle-id="condition-node-id" handle-type="target" :position="Position.Left" />
-
-        <!-- 输出连接点 - 为每个分支创建一个 -->
-        <CanvasHandle v-for="(condition, index) in branches" :key="condition.id" :id="`branch-${condition.id}`"
-            handle-id="condition-node-id" handle-type="source" :position="Position.Right" :style="{
-                top: `${getBranchPosition(index, branches.length)}%`,
-            }">
-            <!-- 多个分支标签提示 -->
-            <div
-                class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-orange-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none">
-                {{ condition.name }}
-            </div>
-        </CanvasHandle>
-    </div>
+	<div
+		class="relative min-w-[260px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+		:class="{ 'scale-105': selected }"
+	>
+		<!-- 节点主体 -->
+		<div
+			class="bg-gradient-to-br from-white to-orange-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+			:class="
+				selected
+					? 'border-orange-500 shadow-orange-200 shadow-lg'
+					: 'border-orange-300 hover:shadow-lg hover:shadow-orange-100'
+			"
+		>
+			<!-- 左侧装饰条 -->
+			<div
+				class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-orange-500 to-orange-400 rounded-l-xl"
+			></div>
+
+			<!-- 头部 -->
+			<div class="flex items-center gap-3 px-4 py-3 border-b border-orange-100">
+				<!-- 图标 - 菱形形状 -->
+				<div
+					class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-orange-500 to-orange-400 rounded-lg shadow-md shadow-orange-200 rotate-45"
+				>
+					<div
+						class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
+					></div>
+					<div class="-rotate-45">
+						<Icon icon="lucide:git-branch" color="#ffffff" class="relative z-10" :size="20" />
+					</div>
+				</div>
+
+				<!-- 标题 -->
+				<div class="flex-1 min-w-0">
+					<div class="text-sm font-semibold text-gray-800 truncate">
+						{{ data.label || '条件分支' }}
+					</div>
+					<div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+						{{ data.description }}
+					</div>
+				</div>
+
+				<!-- 分支数量标签 -->
+				<div
+					class="flex-shrink-0 flex items-center gap-1 px-2 py-1 bg-orange-100 rounded text-xs font-medium text-orange-700"
+				>
+					<Icon icon="lucide:split" color="#ea580c" :size="12" />
+					<span>{{ branches.length }}</span>
+				</div>
+			</div>
+
+			<!-- 分支列表 -->
+			<div class="px-4 py-3 space-y-2 max-h-[200px] overflow-y-auto">
+				<div
+					v-for="(condition, index) in branches"
+					:key="condition.id"
+					class="flex items-start gap-2 p-2 rounded-lg bg-orange-50/50 hover:bg-orange-100/50 transition-colors group"
+				>
+					<!-- 分支序号 -->
+					<div
+						class="flex-shrink-0 flex items-center justify-center w-5 h-5 bg-orange-500 text-white text-xs font-bold rounded-full"
+					>
+						{{ index + 1 }}
+					</div>
+
+					<!-- 分支信息 -->
+					<div class="flex-1 min-w-0">
+						<div class="flex items-center gap-1.5 mb-1">
+							<span class="text-xs font-medium text-gray-700 truncate">
+								{{ condition.name }}
+							</span>
+							<Icon
+								v-if="condition.priority"
+								icon="lucide:star"
+								color="#f97316"
+								:size="12"
+								class="flex-shrink-0"
+							/>
+						</div>
+						<div class="text-xs text-gray-500 font-mono bg-white px-2 py-1 rounded truncate">
+							{{ condition.expression }}
+						</div>
+					</div>
+
+					<!-- 分支指示箭头 -->
+					<div class="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
+						<Icon icon="lucide:arrow-right" color="#f97316" :size="14" />
+					</div>
+				</div>
+
+				<!-- 默认分支提示 -->
+				<div
+					v-if="data.defaultBranch"
+					class="flex items-center gap-2 p-2 rounded-lg bg-gray-50 border border-dashed border-gray-300"
+				>
+					<Icon icon="lucide:shield-check" color="#94a3b8" :size="14" />
+					<span class="text-xs text-gray-500">
+						默认: <span class="font-medium text-gray-700">{{ data.defaultBranch }}</span>
+					</span>
+				</div>
+			</div>
+
+			<!-- 底部状态栏 -->
+			<div
+				class="flex items-center justify-between px-4 py-2 bg-orange-50/50 border-t border-orange-100"
+			>
+				<div class="flex items-center gap-1.5">
+					<Icon icon="lucide:zap" color="#f97316" :size="12" />
+					<span class="text-xs text-gray-500">条件判断</span>
+				</div>
+				<Icon
+					icon="lucide:settings"
+					color="#94a3b8"
+					:size="14"
+					class="cursor-pointer hover:text-orange-500 transition-colors"
+				/>
+			</div>
+		</div>
+
+		<!-- 输入连接点 -->
+		<CanvasHandle
+			handle-id="condition-node-input"
+			type="target"
+			:connections-count="1"
+			:position="Position.Left"
+		/>
+
+		<!-- 输出连接点 - 为每个分支创建一个 -->
+		<CanvasHandle
+			v-for="(condition, index) in branches"
+			:key="condition.id"
+			:id="`branch-${condition.id}`"
+			:handle-id="`condition-node-id-${index}`"
+			:connections-count="branches.length"
+			type="source"
+			:position="Position.Right"
+			:style="{
+				top: `${getBranchPosition(index, branches.length)}%`
+			}"
+		>
+			<!-- 多个分支标签提示 -->
+			<div
+				class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-orange-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none"
+			>
+				{{ condition.name }}
+			</div>
+		</CanvasHandle>
+	</div>
 </template>
 
 <style scoped>
 .overflow-y-auto::-webkit-scrollbar {
-    width: 4px;
+	width: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-track {
-    background: #fed7aa;
-    border-radius: 4px;
+	background: #fed7aa;
+	border-radius: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-thumb {
-    background: #f97316;
-    border-radius: 4px;
+	background: #f97316;
+	border-radius: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-thumb:hover {
-    background: #ea580c;
+	background: #ea580c;
 }
-</style>
+</style>

+ 227 - 179
packages/workflow/src/components/elements/node-temp/DataBaseNode.vue

@@ -12,52 +12,52 @@ import { Icon } from '@repo/ui'
 import { computed } from 'vue'
 
 interface Datasource {
-    type: 'mysql' | 'postgresql' | 'mongodb' | 'redis' | 'api'
-    connectionId: string
-    connectionName?: string
+	type: 'mysql' | 'postgresql' | 'mongodb' | 'redis' | 'api'
+	connectionId: string
+	connectionName?: string
 }
 
 interface Query {
-    type: 'sql' | 'nosql' | 'rest'
-    content: string
-    params?: Record<string, any>
+	type: 'sql' | 'nosql' | 'rest'
+	content: string
+	params?: Record<string, any>
 }
 
 interface Props {
-    data: {
-        label?: string
-        description?: string
-        datasource?: Datasource
-        query?: Query
-        result?: {
-            limit?: number
-            mapping?: Record<string, string>
-        }
-        [key: string]: any
-    }
-    selected?: boolean
+	data: {
+		label?: string
+		description?: string
+		datasource?: Datasource
+		query?: Query
+		result?: {
+			limit?: number
+			mapping?: Record<string, string>
+		}
+		[key: string]: any
+	}
+	selected?: boolean
 }
 
 const props = withDefaults(defineProps<Props>(), {
-    selected: false
+	selected: false
 })
 
 // 数据源类型图标映射
 const datasourceIcons: Record<string, string> = {
-    mysql: 'lucide:database',
-    postgresql: 'lucide:database',
-    mongodb: 'lucide:database',
-    redis: 'lucide:hard-drive',
-    api: 'lucide:cloud'
+	mysql: 'lucide:database',
+	postgresql: 'lucide:database',
+	mongodb: 'lucide:database',
+	redis: 'lucide:hard-drive',
+	api: 'lucide:cloud'
 }
 
 // 数据源类型颜色映射
 const datasourceColors: Record<string, string> = {
-    mysql: '#13c2c2',
-    postgresql: '#13c2c2',
-    mongodb: '#52c41a',
-    redis: '#ff4d4f',
-    api: '#1890ff'
+	mysql: '#13c2c2',
+	postgresql: '#13c2c2',
+	mongodb: '#52c41a',
+	redis: '#ff4d4f',
+	api: '#1890ff'
 }
 
 const datasourceType = computed(() => props.data.datasource?.type || 'mysql')
@@ -66,178 +66,226 @@ const datasourceColor = computed(() => datasourceColors[datasourceType.value] ||
 
 // 查询类型标签
 const queryTypeLabel = computed(() => {
-    const type = props.data.query?.type
-    return type?.toUpperCase() || 'SQL'
+	const type = props.data.query?.type
+	return type?.toUpperCase() || 'SQL'
 })
 </script>
 
 <template>
-    <div class="relative min-w-[280px] transition-all duration-300 ease-out hover:-translate-y-0.5"
-        :class="{ 'scale-105': selected }">
-        <!-- 节点主体 -->
-        <div class="bg-gradient-to-br from-white to-cyan-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
-            :class="selected ? 'border-cyan-500 shadow-cyan-200 shadow-lg' : 'border-cyan-300 hover:shadow-lg hover:shadow-cyan-100'">
-            <!-- 左侧装饰条 -->
-            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-cyan-500 to-cyan-400 rounded-l-xl">
-            </div>
-
-            <!-- 头部 -->
-            <div class="flex items-center gap-3 px-4 py-3 border-b border-cyan-100">
-                <!-- 图标 -->
-                <div
-                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-cyan-500 to-cyan-400 rounded-lg shadow-md shadow-cyan-200">
-                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
-                    <Icon :icon="datasourceIcon" color="#ffffff" class="relative z-10" :size="20" />
-                </div>
-
-                <!-- 标题 -->
-                <div class="flex-1 min-w-0">
-                    <div class="text-sm font-semibold text-gray-800 truncate">
-                        {{ data.label || '数据查询' }}
-                    </div>
-                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
-                        {{ data.description }}
-                    </div>
-                </div>
-
-                <!-- 查询类型标签 -->
-                <div class="flex-shrink-0 px-2 py-1 bg-cyan-100 rounded text-xs font-bold text-cyan-700">
-                    {{ queryTypeLabel }}
-                </div>
-            </div>
-
-            <!-- 数据源信息 -->
-            <div class="px-4 py-3 space-y-3">
-                <!-- 数据源 -->
-                <div class="flex items-start gap-2">
-                    <Icon icon="lucide:server" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
-                    <div class="flex-1 min-w-0">
-                        <div class="text-xs text-gray-500 mb-1">数据源</div>
-                        <div class="flex items-center gap-2">
-                            <div class="w-2 h-2 rounded-full" :style="{ backgroundColor: datasourceColor }"></div>
-                            <span class="text-xs font-medium text-gray-700 uppercase">
-                                {{ datasourceType }}
-                            </span>
-                            <span v-if="data.datasource?.connectionName" class="text-xs text-gray-500">
-                                - {{ data.datasource.connectionName }}
-                            </span>
-                        </div>
-                    </div>
-                </div>
-
-                <!-- 查询语句 -->
-                <div class="flex items-start gap-2">
-                    <Icon icon="lucide:code-2" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
-                    <div class="flex-1 min-w-0">
-                        <div class="text-xs text-gray-500 mb-1">查询语句</div>
-                        <div
-                            class="text-xs text-gray-700 font-mono bg-gray-50 px-2 py-2 rounded border border-gray-200 max-h-20 overflow-y-auto">
-                            {{ data.query?.content || 'SELECT * FROM table_name' }}
-                        </div>
-                    </div>
-                </div>
-
-                <!-- 查询参数 -->
-                <div v-if="data.query?.params && Object.keys(data.query.params).length > 0"
-                    class="flex items-start gap-2">
-                    <Icon icon="lucide:list-filter" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
-                    <div class="flex-1 min-w-0">
-                        <div class="text-xs text-gray-500 mb-1">查询参数</div>
-                        <div class="flex flex-wrap gap-1">
-                            <span v-for="(value, key) in data.query.params" :key="key"
-                                class="inline-flex items-center gap-1 px-2 py-0.5 bg-cyan-50 text-cyan-700 text-xs rounded border border-cyan-200">
-                                <span class="font-medium">{{ key }}:</span>
-                                <span class="font-mono">{{ value }}</span>
-                            </span>
-                        </div>
-                    </div>
-                </div>
-
-                <!-- 结果配置 -->
-                <div v-if="data.result?.limit" class="flex items-center gap-2">
-                    <Icon icon="lucide:layers" color="#94a3b8" :size="14" class="flex-shrink-0" />
-                    <div class="text-xs text-gray-600">
-                        <span class="text-gray-500">返回条数:</span>
-                        <span class="font-medium ml-1">{{ data.result.limit }}</span>
-                    </div>
-                </div>
-            </div>
-
-            <!-- 底部状态栏 -->
-            <div class="flex items-center justify-between px-4 py-2 bg-cyan-50/50 border-t border-cyan-100">
-                <div class="flex items-center gap-2">
-                    <div class="flex items-center gap-1.5">
-                        <div class="w-1.5 h-1.5 bg-cyan-500 rounded-full animate-pulse"></div>
-                        <span class="text-xs text-gray-500">就绪</span>
-                    </div>
-                    <!-- 连接状态 -->
-                    <div class="flex items-center gap-1 px-1.5 py-0.5 bg-green-100 rounded">
-                        <Icon icon="lucide:check-circle-2" color="#52c41a" :size="10" />
-                        <span class="text-xs text-green-700 font-medium">已连接</span>
-                    </div>
-                </div>
-                <div class="flex items-center gap-1">
-                    <Icon icon="lucide:play" color="#94a3b8" :size="14"
-                        class="cursor-pointer hover:text-cyan-500 transition-colors" title="测试查询" />
-                    <Icon icon="lucide:settings" color="#94a3b8" :size="14"
-                        class="cursor-pointer hover:text-cyan-500 transition-colors" title="配置" />
-                </div>
-            </div>
-        </div>
-
-        <!-- 输入连接点 -->
-        <CanvasHandle handle-id="data-node-id" handle-type="target" :position="Position.Left" />
-
-        <!-- 输出连接点 - 成功 -->
-        <CanvasHandle handle-id="success" handle-type="source" :position="Position.Right" :style="{ top: '40%' }">
-            <div
-                class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-green-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none">
-                成功
-            </div>
-        </CanvasHandle>
-
-        <!-- 输出连接点 - 失败 -->
-        <CanvasHandle handle-id="error" handle-type="source" :position="Position.Right" :style="{ top: '60%' }">
-            <div
-                class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-red-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none">
-                失败
-            </div>
-        </CanvasHandle>
-    </div>
+	<div
+		class="relative min-w-[280px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+		:class="{ 'scale-105': selected }"
+	>
+		<!-- 节点主体 -->
+		<div
+			class="bg-gradient-to-br from-white to-cyan-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+			:class="
+				selected
+					? 'border-cyan-500 shadow-cyan-200 shadow-lg'
+					: 'border-cyan-300 hover:shadow-lg hover:shadow-cyan-100'
+			"
+		>
+			<!-- 左侧装饰条 -->
+			<div
+				class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-cyan-500 to-cyan-400 rounded-l-xl"
+			></div>
+
+			<!-- 头部 -->
+			<div class="flex items-center gap-3 px-4 py-3 border-b border-cyan-100">
+				<!-- 图标 -->
+				<div
+					class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-cyan-500 to-cyan-400 rounded-lg shadow-md shadow-cyan-200"
+				>
+					<div
+						class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
+					></div>
+					<Icon :icon="datasourceIcon" color="#ffffff" class="relative z-10" :size="20" />
+				</div>
+
+				<!-- 标题 -->
+				<div class="flex-1 min-w-0">
+					<div class="text-sm font-semibold text-gray-800 truncate">
+						{{ data.label || '数据查询' }}
+					</div>
+					<div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+						{{ data.description }}
+					</div>
+				</div>
+
+				<!-- 查询类型标签 -->
+				<div class="flex-shrink-0 px-2 py-1 bg-cyan-100 rounded text-xs font-bold text-cyan-700">
+					{{ queryTypeLabel }}
+				</div>
+			</div>
+
+			<!-- 数据源信息 -->
+			<div class="px-4 py-3 space-y-3">
+				<!-- 数据源 -->
+				<div class="flex items-start gap-2">
+					<Icon icon="lucide:server" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+					<div class="flex-1 min-w-0">
+						<div class="text-xs text-gray-500 mb-1">数据源</div>
+						<div class="flex items-center gap-2">
+							<div class="w-2 h-2 rounded-full" :style="{ backgroundColor: datasourceColor }"></div>
+							<span class="text-xs font-medium text-gray-700 uppercase">
+								{{ datasourceType }}
+							</span>
+							<span v-if="data.datasource?.connectionName" class="text-xs text-gray-500">
+								- {{ data.datasource.connectionName }}
+							</span>
+						</div>
+					</div>
+				</div>
+
+				<!-- 查询语句 -->
+				<div class="flex items-start gap-2">
+					<Icon icon="lucide:code-2" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+					<div class="flex-1 min-w-0">
+						<div class="text-xs text-gray-500 mb-1">查询语句</div>
+						<div
+							class="text-xs text-gray-700 font-mono bg-gray-50 px-2 py-2 rounded border border-gray-200 max-h-20 overflow-y-auto"
+						>
+							{{ data.query?.content || 'SELECT * FROM table_name' }}
+						</div>
+					</div>
+				</div>
+
+				<!-- 查询参数 -->
+				<div
+					v-if="data.query?.params && Object.keys(data.query.params).length > 0"
+					class="flex items-start gap-2"
+				>
+					<Icon icon="lucide:list-filter" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+					<div class="flex-1 min-w-0">
+						<div class="text-xs text-gray-500 mb-1">查询参数</div>
+						<div class="flex flex-wrap gap-1">
+							<span
+								v-for="(value, key) in data.query.params"
+								:key="key"
+								class="inline-flex items-center gap-1 px-2 py-0.5 bg-cyan-50 text-cyan-700 text-xs rounded border border-cyan-200"
+							>
+								<span class="font-medium">{{ key }}:</span>
+								<span class="font-mono">{{ value }}</span>
+							</span>
+						</div>
+					</div>
+				</div>
+
+				<!-- 结果配置 -->
+				<div v-if="data.result?.limit" class="flex items-center gap-2">
+					<Icon icon="lucide:layers" color="#94a3b8" :size="14" class="flex-shrink-0" />
+					<div class="text-xs text-gray-600">
+						<span class="text-gray-500">返回条数:</span>
+						<span class="font-medium ml-1">{{ data.result.limit }}</span>
+					</div>
+				</div>
+			</div>
+
+			<!-- 底部状态栏 -->
+			<div
+				class="flex items-center justify-between px-4 py-2 bg-cyan-50/50 border-t border-cyan-100"
+			>
+				<div class="flex items-center gap-2">
+					<div class="flex items-center gap-1.5">
+						<div class="w-1.5 h-1.5 bg-cyan-500 rounded-full animate-pulse"></div>
+						<span class="text-xs text-gray-500">就绪</span>
+					</div>
+					<!-- 连接状态 -->
+					<div class="flex items-center gap-1 px-1.5 py-0.5 bg-green-100 rounded">
+						<Icon icon="lucide:check-circle-2" color="#52c41a" :size="10" />
+						<span class="text-xs text-green-700 font-medium">已连接</span>
+					</div>
+				</div>
+				<div class="flex items-center gap-1">
+					<Icon
+						icon="lucide:play"
+						color="#94a3b8"
+						:size="14"
+						class="cursor-pointer hover:text-cyan-500 transition-colors"
+						title="测试查询"
+					/>
+					<Icon
+						icon="lucide:settings"
+						color="#94a3b8"
+						:size="14"
+						class="cursor-pointer hover:text-cyan-500 transition-colors"
+						title="配置"
+					/>
+				</div>
+			</div>
+		</div>
+
+		<!-- 输入连接点 -->
+		<CanvasHandle
+			handle-id="data-node-input"
+			type="target"
+			:connections-count="1"
+			:position="Position.Left"
+		/>
+
+		<!-- 输出连接点 - 成功 -->
+		<CanvasHandle
+			handle-id="code-node-output1"
+			type="target"
+			:connections-count="2"
+			:position="Position.Right"
+			:style="{ top: '40%' }"
+		>
+			<div
+				class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-green-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none"
+			>
+				成功
+			</div>
+		</CanvasHandle>
+
+		<!-- 输出连接点 - 失败 -->
+		<CanvasHandle
+			handle-id="code-node-output2"
+			type="target"
+			:connections-count="2"
+			:position="Position.Right"
+			:style="{ top: '60%' }"
+		>
+			<div
+				class="absolute left-5 top-1/2 -translate-y-1/2 px-2 py-0.5 bg-red-500 text-white text-xs rounded whitespace-nowrap opacity-0 hover:opacity-100 transition-opacity pointer-events-none"
+			>
+				失败
+			</div>
+		</CanvasHandle>
+	</div>
 </template>
 
 <style scoped>
 .overflow-y-auto::-webkit-scrollbar {
-    width: 4px;
+	width: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-track {
-    background: #cffafe;
-    border-radius: 4px;
+	background: #cffafe;
+	border-radius: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-thumb {
-    background: #06b6d4;
-    border-radius: 4px;
+	background: #06b6d4;
+	border-radius: 4px;
 }
 
 .overflow-y-auto::-webkit-scrollbar-thumb:hover {
-    background: #0891b2;
+	background: #0891b2;
 }
 
 @keyframes pulse {
+	0%,
+	100% {
+		opacity: 1;
+	}
 
-    0%,
-    100% {
-        opacity: 1;
-    }
-
-    50% {
-        opacity: 0.5;
-    }
+	50% {
+		opacity: 0.5;
+	}
 }
 
 .animate-pulse {
-    animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+	animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 }
-</style>
+</style>

+ 60 - 46
packages/workflow/src/components/elements/node-temp/EndNode.vue

@@ -12,64 +12,78 @@ import type { NodeProps } from '@vue-flow/core'
 import { Icon } from '@repo/ui'
 
 const props = withDefaults(defineProps<NodeProps>(), {
-    selected: true
+	selected: true
 })
-
 </script>
 <template>
-    <div class="relative min-w-[200px] transition-all duration-300 ease-out hover:-translate-y-0.5"
-        :class="{ 'scale-105': selected }">
-        <!-- 节点主体 -->
-        <div class="flex items-center gap-3 px-4 py-3 bg-gradient-to-br from-white to-green-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
-            :class="selected ? 'border-green-500 shadow-green-200 shadow-lg' : 'border-green-300 hover:shadow-lg hover:shadow-green-100'">
-            <!-- 左侧装饰条 -->
-            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-green-500 to-green-400 rounded-l-xl">
-            </div>
+	<div
+		class="relative min-w-[200px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+		:class="{ 'scale-105': selected }"
+	>
+		<!-- 节点主体 -->
+		<div
+			class="flex items-center gap-3 px-4 py-3 bg-gradient-to-br from-white to-green-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+			:class="
+				selected
+					? 'border-green-500 shadow-green-200 shadow-lg'
+					: 'border-green-300 hover:shadow-lg hover:shadow-green-100'
+			"
+		>
+			<!-- 左侧装饰条 -->
+			<div
+				class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-green-500 to-green-400 rounded-l-xl"
+			></div>
 
-            <!-- 图标区域 -->
-            <div
-                class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-green-500 to-green-400 rounded-lg shadow-md shadow-green-200">
-                <!-- 光泽效果 -->
-                <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+			<!-- 图标区域 -->
+			<div
+				class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-green-500 to-green-400 rounded-lg shadow-md shadow-green-200"
+			>
+				<!-- 光泽效果 -->
+				<div
+					class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
+				></div>
 
-                <!-- 图标 -->
-                <Icon icon="lucide:unplug" color="#ffffff" class="relative z-10" :size="20" />
-            </div>
+				<!-- 图标 -->
+				<Icon icon="lucide:unplug" color="#ffffff" class="relative z-10" :size="20" />
+			</div>
 
-            <!-- 内容区域 -->
-            <div class="flex-1 min-w-0">
-                <div class="text-sm font-semibold text-gray-800 truncate">
-                    {{ data.label || '结束' }}
-                </div>
-                <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
-                    {{ data.description }}
-                </div>
-            </div>
-        </div>
+			<!-- 内容区域 -->
+			<div class="flex-1 min-w-0">
+				<div class="text-sm font-semibold text-gray-800 truncate">
+					{{ data.label || '结束' }}
+				</div>
+				<div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+					{{ data.description }}
+				</div>
+			</div>
+		</div>
 
-        <!-- 输出连接点 -->
-        <CanvasHandle handle-id="1" handle-type="source" :position="Position.Right" />
-    </div>
+		<!-- 输入连接点 -->
+		<CanvasHandle
+			handle-id="code-node-input"
+			type="target"
+			:connections-count="1"
+			:position="Position.Left"
+		/>
+	</div>
 </template>
 <style lang="less" scoped>
 @keyframes pulse {
+	0%,
+	100% {
+		opacity: 1;
+	}
 
-    0%,
-    100% {
-        opacity: 1;
-    }
-
-    50% {
-        opacity: 0.5;
-    }
+	50% {
+		opacity: 0.5;
+	}
 }
 
 @keyframes ping {
-
-    75%,
-    100% {
-        transform: scale(2);
-        opacity: 0;
-    }
+	75%,
+	100% {
+		transform: scale(2);
+		opacity: 0;
+	}
 }
-</style>
+</style>

+ 114 - 84
packages/workflow/src/components/elements/node-temp/HttpNode1.vue

@@ -11,33 +11,33 @@ import CanvasHandle from '../handles/CanvasHandle.vue'
 import { Icon } from '@repo/ui'
 
 interface HttpConfig {
-    method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
-    url?: string
-    timeout?: number
+	method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
+	url?: string
+	timeout?: number
 }
 
 interface Props {
-    data: {
-        label?: string
-        description?: string
-        config?: HttpConfig
-        [key: string]: any
-    }
-    selected?: boolean
+	data: {
+		label?: string
+		description?: string
+		config?: HttpConfig
+		[key: string]: any
+	}
+	selected?: boolean
 }
 
 const props = withDefaults(defineProps<Props>(), {
-    selected: false
+	selected: false
 })
 
 console.log(props.data.id, '1212121')
 // 请求方法对应的颜色
 const methodColors: Record<string, string> = {
-    GET: '#1890ff',
-    POST: '#52c41a',
-    PUT: '#faad14',
-    DELETE: '#ff4d4f',
-    PATCH: '#722ed1'
+	GET: '#1890ff',
+	POST: '#52c41a',
+	PUT: '#faad14',
+	DELETE: '#ff4d4f',
+	PATCH: '#722ed1'
 }
 
 const currentMethod = props.data.config?.method || 'GET'
@@ -45,81 +45,111 @@ const methodColor = methodColors[currentMethod]
 </script>
 
 <template>
-    <div class="relative min-w-[240px] transition-all duration-300 ease-out hover:-translate-y-0.5"
-        :class="{ 'scale-105': selected }">
-        <!-- 节点主体 -->
-        <div class="bg-gradient-to-br from-white to-blue-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
-            :class="selected ? 'border-blue-500 shadow-blue-200 shadow-lg' : 'border-blue-300 hover:shadow-lg hover:shadow-blue-100'">
-            <!-- 左侧装饰条 -->
-            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-blue-500 to-blue-400 rounded-l-xl">
-            </div>
+	<div
+		class="relative min-w-[240px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+		:class="{ 'scale-105': selected }"
+	>
+		<!-- 节点主体 -->
+		<div
+			class="bg-gradient-to-br from-white to-blue-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+			:class="
+				selected
+					? 'border-blue-500 shadow-blue-200 shadow-lg'
+					: 'border-blue-300 hover:shadow-lg hover:shadow-blue-100'
+			"
+		>
+			<!-- 左侧装饰条 -->
+			<div
+				class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-blue-500 to-blue-400 rounded-l-xl"
+			></div>
 
-            <!-- 头部 -->
-            <div class="flex items-center gap-3 px-4 py-3 border-b border-blue-100">
-                <!-- 图标 -->
-                <div
-                    class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-400 rounded-lg shadow-md shadow-blue-200">
-                    <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
-                    <Icon icon="lucide:cloud" color="#ffffff" class="relative z-10" :size="20" />
-                </div>
+			<!-- 头部 -->
+			<div class="flex items-center gap-3 px-4 py-3 border-b border-blue-100">
+				<!-- 图标 -->
+				<div
+					class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-400 rounded-lg shadow-md shadow-blue-200"
+				>
+					<div
+						class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
+					></div>
+					<Icon icon="lucide:cloud" color="#ffffff" class="relative z-10" :size="20" />
+				</div>
 
-                <!-- 标题 -->
-                <div class="flex-1 min-w-0">
-                    <div class="text-sm font-semibold text-gray-800 truncate">
-                        {{ data.label || 'HTTP 请求' }}
-                    </div>
-                    <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
-                        {{ data.description }}
-                    </div>
-                </div>
+				<!-- 标题 -->
+				<div class="flex-1 min-w-0">
+					<div class="text-sm font-semibold text-gray-800 truncate">
+						{{ data.label || 'HTTP 请求' }}
+					</div>
+					<div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+						{{ data.description }}
+					</div>
+				</div>
 
-                <!-- 请求方法标签 -->
-                <div class="flex-shrink-0 px-2 py-1 rounded text-xs font-bold text-white"
-                    :style="{ backgroundColor: methodColor }">
-                    {{ currentMethod }}
-                </div>
-            </div>
+				<!-- 请求方法标签 -->
+				<div
+					class="flex-shrink-0 px-2 py-1 rounded text-xs font-bold text-white"
+					:style="{ backgroundColor: methodColor }"
+				>
+					{{ currentMethod }}
+				</div>
+			</div>
 
-            <!-- 配置信息 -->
-            <div class="px-4 py-3 space-y-2">
-                <!-- URL -->
-                <div class="flex items-start gap-2">
-                    <Icon icon="lucide:link" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
-                    <div class="flex-1 min-w-0">
-                        <div class="text-xs text-gray-500 mb-0.5">请求地址</div>
-                        <div class="text-xs text-gray-700 font-mono bg-gray-50 px-2 py-1 rounded truncate">
-                            {{ data.config?.url || '未配置' }}
-                        </div>
-                    </div>
-                </div>
+			<!-- 配置信息 -->
+			<div class="px-4 py-3 space-y-2">
+				<!-- URL -->
+				<div class="flex items-start gap-2">
+					<Icon icon="lucide:link" color="#94a3b8" :size="14" class="flex-shrink-0 mt-0.5" />
+					<div class="flex-1 min-w-0">
+						<div class="text-xs text-gray-500 mb-0.5">请求地址</div>
+						<div class="text-xs text-gray-700 font-mono bg-gray-50 px-2 py-1 rounded truncate">
+							{{ data.config?.url || '未配置' }}
+						</div>
+					</div>
+				</div>
 
-                <!-- 超时时间 -->
-                <div v-if="data.config?.timeout" class="flex items-center gap-2">
-                    <Icon icon="lucide:clock" color="#94a3b8" :size="14" class="flex-shrink-0" />
-                    <div class="text-xs text-gray-600">
-                        <span class="text-gray-500">超时:</span>
-                        <span class="font-medium ml-1">{{ data.config.timeout }}ms</span>
-                    </div>
-                </div>
-            </div>
+				<!-- 超时时间 -->
+				<div v-if="data.config?.timeout" class="flex items-center gap-2">
+					<Icon icon="lucide:clock" color="#94a3b8" :size="14" class="flex-shrink-0" />
+					<div class="text-xs text-gray-600">
+						<span class="text-gray-500">超时:</span>
+						<span class="font-medium ml-1">{{ data.config.timeout }}ms</span>
+					</div>
+				</div>
+			</div>
 
-            <!-- 底部状态栏 -->
-            <div class="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-t border-blue-100">
-                <div class="flex items-center gap-1.5">
-                    <div class="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
-                    <span class="text-xs text-gray-500">就绪</span>
-                </div>
-                <Icon icon="lucide:settings" color="#94a3b8" :size="14"
-                    class="cursor-pointer hover:text-blue-500 transition-colors" />
-            </div>
-        </div>
+			<!-- 底部状态栏 -->
+			<div
+				class="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-t border-blue-100"
+			>
+				<div class="flex items-center gap-1.5">
+					<div class="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
+					<span class="text-xs text-gray-500">就绪</span>
+				</div>
+				<Icon
+					icon="lucide:settings"
+					color="#94a3b8"
+					:size="14"
+					class="cursor-pointer hover:text-blue-500 transition-colors"
+				/>
+			</div>
+		</div>
 
-        <!-- 输入连接点 -->
-        <CanvasHandle :handle-id="props.data.id" handle-type="target" :position="Position.Left" />
+		<!-- 输入连接点 -->
+		<CanvasHandle
+			handle-id="http-node-input"
+			type="target"
+			:connections-count="1"
+			:position="Position.Left"
+		/>
 
-        <!-- 输出连接点 -->
-        <CanvasHandle :handle-id="props.data.id" handle-type="source" :position="Position.Right" />
-    </div>
+		<!-- 输出连接点 -->
+		<CanvasHandle
+			handle-id="http-node-output"
+			type="source"
+			:connections-count="1"
+			:position="Position.Right"
+		/>
+	</div>
 </template>
 
-<style scoped lang="less"></style>
+<style scoped lang="less"></style>

+ 60 - 47
packages/workflow/src/components/elements/node-temp/StartNode.vue

@@ -12,65 +12,78 @@ import type { NodeProps } from '@vue-flow/core'
 import { Icon } from '@repo/ui'
 
 const props = withDefaults(defineProps<NodeProps>(), {
-    selected: false
+	selected: false
 })
-
 </script>
 <template>
-    <div class="relative min-w-[200px] transition-all duration-300 ease-out hover:-translate-y-0.5"
-        :class="{ 'scale-105': selected }">
-        <!-- 节点主体 -->
-        <div class="flex items-center gap-3 px-4 py-3 bg-gradient-to-br from-white to-green-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
-            :class="selected ? 'border-green-500 shadow-green-200 shadow-lg' : 'border-green-300 hover:shadow-lg hover:shadow-green-100'">
-            <!-- 左侧装饰条 -->
-            <div class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-green-500 to-green-400 rounded-l-xl">
-            </div>
-
-            <!-- 图标区域 -->
-            <div
-                class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-green-500 to-green-400 rounded-lg shadow-md shadow-green-200">
-                <!-- 光泽效果 -->
-                <div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"></div>
+	<div
+		class="relative min-w-[200px] transition-all duration-300 ease-out hover:-translate-y-0.5"
+		:class="{ 'scale-105': selected }"
+	>
+		<!-- 节点主体 -->
+		<div
+			class="flex items-center gap-3 px-4 py-3 bg-gradient-to-br from-white to-green-50 border-2 rounded-xl shadow-md transition-all duration-300 relative overflow-hidden"
+			:class="
+				selected
+					? 'border-green-500 shadow-green-200 shadow-lg'
+					: 'border-green-300 hover:shadow-lg hover:shadow-green-100'
+			"
+		>
+			<!-- 左侧装饰条 -->
+			<div
+				class="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-green-500 to-green-400 rounded-l-xl"
+			></div>
 
-                <!-- 播放图标 -->
-                <Icon icon="lucide:play" color="#ffffff" class="relative z-10" :size="20" />
-            </div>
+			<!-- 图标区域 -->
+			<div
+				class="relative flex-shrink-0 flex items-center justify-center w-10 h-10 bg-gradient-to-br from-green-500 to-green-400 rounded-lg shadow-md shadow-green-200"
+			>
+				<!-- 光泽效果 -->
+				<div
+					class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent rounded-lg"
+				></div>
 
-            <!-- 内容区域 -->
-            <div class="flex-1 min-w-0">
-                <div class="text-sm font-semibold text-gray-800 truncate">
-                    {{ data.label || '开始' }}
-                </div>
-                <div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
-                    {{ data.description }}
-                </div>
-            </div>
+				<!-- 播放图标 -->
+				<Icon icon="lucide:play" color="#ffffff" class="relative z-10" :size="20" />
+			</div>
 
-        </div>
+			<!-- 内容区域 -->
+			<div class="flex-1 min-w-0">
+				<div class="text-sm font-semibold text-gray-800 truncate">
+					{{ data.label || '开始' }}
+				</div>
+				<div v-if="data.description" class="text-xs text-gray-500 mt-0.5 truncate">
+					{{ data.description }}
+				</div>
+			</div>
+		</div>
 
-        <!-- 输出连接点 -->
-        <CanvasHandle handle-id="1" handle-type="source" :position="Position.Right" />
-    </div>
+		<!-- 输出连接点 -->
+		<CanvasHandle
+			handle-id="start-node-output"
+			type="source"
+			:connections-count="1"
+			:position="Position.Right"
+		/>
+	</div>
 </template>
 <style lang="less" scoped>
 @keyframes pulse {
+	0%,
+	100% {
+		opacity: 1;
+	}
 
-    0%,
-    100% {
-        opacity: 1;
-    }
-
-    50% {
-        opacity: 0.5;
-    }
+	50% {
+		opacity: 0.5;
+	}
 }
 
 @keyframes ping {
-
-    75%,
-    100% {
-        transform: scale(2);
-        opacity: 0;
-    }
+	75%,
+	100% {
+		transform: scale(2);
+		opacity: 0;
+	}
 }
-</style>
+</style>

+ 10 - 1
packages/workflow/src/components/elements/nodes/CanvasNode.vue

@@ -18,6 +18,11 @@ type Props = NodeProps<IWorkflowNode['data']> & {
 
 const props = defineProps<Props>()
 
+const emit = defineEmits<{
+	update: [id: string, parameters: Record<string, unknown>]
+	move: [id: string, position: { x: number; y: number }]
+}>()
+
 /**
  * 处理节点
  */
@@ -72,6 +77,10 @@ const outputs = computed(() =>
 	)
 )
 
+const onUpdate = (prop: Record<string, unknown>) => {
+	emit('update', props.id, prop)
+}
+
 provide('canvas-node-data', {
 	props,
 	inputs,
@@ -81,7 +90,7 @@ provide('canvas-node-data', {
 
 <template>
 	<div class="relative">
-		<NodeRenderer />
+		<NodeRenderer v-bind="$attrs" @update="onUpdate" />
 
 		<template v-for="target in inputs" :key="'handle-inputs-port' + target.index">
 			<CanvasHandle v-bind="target" type="target" />

+ 23 - 5
packages/workflow/src/components/elements/nodes/render-types/NodeStickyNote.vue

@@ -7,6 +7,13 @@ import type { OnResize } from '@vue-flow/node-resizer'
 import type { NodeProps, XYPosition } from '@vue-flow/core'
 import type { IWorkflowNode } from '../../../../Interface'
 
+// make sure to include the necessary styles!
+import '@vue-flow/node-resizer/dist/style.css'
+
+defineOptions({
+	inheritAttrs: false
+})
+
 const node = inject<{
 	props?: NodeProps<
 		IWorkflowNode['data'] & {
@@ -45,10 +52,18 @@ const modelValue = computed({
 
 const editMode = ref(false)
 
-const handleSetEditMode = (_link: HTMLElement, e: MouseEvent) => {
-	e.stopPropagation()
+const nodeClass = computed(() => {
+	let classes: string[] = []
+	if (node?.props?.selected) {
+		classes.push('ring-6px', 'ring-#e0e2e7')
+	}
+
+	return classes
+})
+
+const handleSetEditMode = (edit: boolean) => {
 	if (!isReadOnly.value) {
-		editMode.value = true
+		editMode.value = edit
 	}
 }
 
@@ -75,11 +90,13 @@ function onResize(event: OnResize) {
 		:min-width="150"
 		:height="data?.height"
 		:width="data?.width"
-		:is-visible="!isReadOnly"
+		:is-visible="!isReadOnly && node?.props?.selected"
+		handleClassName="bg-transparent! border-transparent!"
+		lineClassName="border-transparent!"
 		@resize="onResize"
 	/>
 
-	<div class="sticky-note__node relative">
+	<div class="sticky-note__node relative rounded-4px" :class="nodeClass">
 		<StickyNote
 			v-model="modelValue"
 			:minHeight="100"
@@ -90,6 +107,7 @@ function onResize(event: OnResize) {
 			:readOnly="node?.props?.readOnly"
 			:editMode="editMode"
 			@dblclick.stop="handleSetEditMode"
+			@edit="handleSetEditMode"
 		/>
 	</div>
 </template>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1300 - 113
pnpm-lock.yaml