Kaynağa Gözat

Merge branch 'main' of https://git.shalu.com/Shalu/shalu-agent-workflow

jiaxing.liao 4 gün önce
ebeveyn
işleme
80a6c29057

+ 3 - 0
apps/web/src/assets/icons/efficiency.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
+</svg>

+ 4 - 0
apps/web/src/assets/icons/growth.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <circle cx="12" cy="12" r="10"/>
+  <path d="M12 6v6l4 2"/>
+</svg>

+ 6 - 0
apps/web/src/assets/icons/legal.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
+  <polyline points="14 2 14 8 20 8"/>
+  <line x1="12" y1="11" x2="12" y2="17"/>
+  <line x1="9" y1="14" x2="15" y2="14"/>
+</svg>

+ 4 - 0
apps/web/src/assets/icons/lock.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <rect width="18" height="11" x="3" y="11" rx="2" ry="2"/>
+  <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
+</svg>

+ 6 - 0
apps/web/src/assets/icons/service.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
+  <circle cx="9" cy="7" r="4"/>
+  <path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
+  <path d="M16 3.13a4 4 0 0 1 0 7.75"/>
+</svg>

+ 4 - 0
apps/web/src/assets/icons/smartphone.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+  <rect width="14" height="20" x="5" y="2" rx="2" ry="2"/>
+  <path d="M12 18h.01"/>
+</svg>

+ 21 - 4
apps/web/src/components/Sidebar/index.vue

@@ -19,7 +19,6 @@
 						<el-dropdown-menu>
 							<el-dropdown-item @click="createWorkflow">工作流程</el-dropdown-item>
 							<el-dropdown-item @click="createCertificate">凭证</el-dropdown-item>
-							<el-dropdown-item @click="createTable">数据表</el-dropdown-item>
 						</el-dropdown-menu>
 					</template>
 				</el-dropdown>
@@ -59,6 +58,22 @@
 				<span v-if="!collapsed" class="label">概览</span>
 			</el-menu-item>
 
+			<el-menu-item index="/orchestration">
+				<el-tooltip v-if="collapsed" content="流程设计" placement="right">
+					<span><SvgIcon name="workflow" /></span>
+				</el-tooltip>
+				<SvgIcon v-else name="workflow" />
+				<span v-if="!collapsed" class="label">流程设计</span>
+			</el-menu-item>
+
+			<el-menu-item index="/execution">
+				<el-tooltip v-if="collapsed" content="执行" placement="right">
+					<span><SvgIcon name="play" /></span>
+				</el-tooltip>
+				<SvgIcon v-else name="play" />
+				<span v-if="!collapsed" class="label">执行</span>
+			</el-menu-item>
+
 			<el-menu-item index="/chat">
 				<el-tooltip v-if="collapsed" content="聊天" placement="right">
 					<span><SvgIcon name="chatMessage" /></span>
@@ -141,6 +156,7 @@
 <script setup lang="ts">
 import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
 import SearchDialog from '../SearchDialog/index.vue'
 import { v4 } from 'uuid'
 import TemplateModal from '../TemplateModal/index.vue'
@@ -166,9 +182,10 @@ const createWorkflow = () => {
 	router.push(`/workflow/${id}`)
 }
 
-const createCertificate = () => {}
-
-const createTable = () => {}
+const createCertificate = () => {
+	// TODO: 实现凭证创建功能
+	ElMessage.info('凭证功能开发中')
+}
 
 const handleTemplateClick = () => {
 	showTemplateModal.value = true

+ 4 - 4
apps/web/src/components/TemplateModal/index.vue

@@ -13,7 +13,7 @@
 							class="use-btn"
 							@click="selectTemplate(template)"
 						>
-							使用模板
+							查看模板
 						</el-button>
 					</div>
 				</el-col>
@@ -178,12 +178,12 @@ const selectTemplate = (template: Template) => {
 		margin-bottom: 24px;
 
 		&:hover {
-			border-color: #0076ff;
-			box-shadow: 0 4px 12px rgba(0, 118, 255, 0.15);
+			border-color: var(--el-color-primary);
+			box-shadow: 0 4px 12px var(--el-color-primary-light-7);
 			transform: translateY(-2px);
 
 			.template-title {
-				color: #0076ff;
+				color: var(--el-color-primary);
 			}
 		}
 

+ 14 - 1
apps/web/src/router/index.ts

@@ -12,6 +12,8 @@ const About = () => import('@/views/About.vue')
 const UserCenter = () => import('@/views/UserCenter.vue')
 const LogStream = () => import('@/views/LogStream.vue')
 const ModelLog = () => import('@/views/ModelLog.vue')
+const WorkflowOrchestration = () => import('@/views/WorkflowOrchestration.vue')
+const WorkflowExecution = () => import('@/views/WorkflowExecution.vue')
 
 const routes = [
 	{
@@ -21,13 +23,24 @@ const routes = [
 			{
 				path: '',
 				name: 'Dashboard',
-				component: Dashboard
+				component: Dashboard,
+				alias: 'overview'
 			},
 			{
 				path: 'statistics',
 				name: 'Statistics',
 				component: Statistics
 			},
+			{
+				path: 'orchestration',
+				name: 'WorkflowOrchestration',
+				component: WorkflowOrchestration
+			},
+			{
+				path: 'execution',
+				name: 'WorkflowExecution',
+				component: WorkflowExecution
+			},
 			{
 				path: 'chat',
 				name: 'Chat',

+ 4 - 4
apps/web/src/style.css

@@ -18,11 +18,11 @@
 
 a {
 	font-weight: 500;
-	color: #646cff;
+	color: var(--el-color-primary);
 	text-decoration: inherit;
 }
 a:hover {
-	color: #535bf2;
+	color: var(--el-color-primary);
 }
 
 body {
@@ -49,7 +49,7 @@ button {
 	transition: border-color 0.25s;
 }
 button:hover {
-	border-color: #646cff;
+	border-color: var(--el-color-primary);
 }
 button:focus,
 button:focus-visible {
@@ -73,7 +73,7 @@ button:focus-visible {
 		background-color: #ffffff;
 	}
 	a:hover {
-		color: #747bff;
+		color: var(--el-color-primary);
 	}
 	button {
 		background-color: #f9f9f9;

+ 2 - 6
apps/web/src/views/About.vue

@@ -60,7 +60,7 @@
 	.hero-section {
 		text-align: center;
 		padding: 60px 0;
-		background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+		background: linear-gradient(135deg, var(--el-color-primary) 0%, #764ba2 100%);
 		border-radius: 20px;
 		margin-bottom: 40px;
 		color: #fff;
@@ -143,11 +143,7 @@
 
 				.feature-icon {
 					font-size: 40px;
-					color: #667eea;
-					margin-bottom: 16px;
-				}
-
-				h3 {
+				color: var(--el-color-primary);
 					font-size: 18px;
 					font-weight: 600;
 					color: #333;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 631 - 709
apps/web/src/views/Dashboard.vue


+ 6 - 6
apps/web/src/views/Docs.vue

@@ -195,9 +195,9 @@ const docDescription = computed(() => {
 						}
 
 						&.active {
-							color: #667eea;
+							color: var(--el-color-primary);
 							background: #f0f3ff;
-							border-right: 3px solid #667eea;
+							border-right: 3px solid var(--el-color-primary);
 							font-weight: 500;
 						}
 					}
@@ -268,7 +268,7 @@ const docDescription = computed(() => {
 								content: '✓';
 								position: absolute;
 								left: 0;
-								color: #667eea;
+								color: var(--el-color-primary);
 								font-weight: 700;
 							}
 
@@ -317,13 +317,13 @@ const docDescription = computed(() => {
 
 							&:hover {
 								background: #fff;
-								border-color: #667eea;
-								box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
+								border-color: var(--el-color-primary);
+								box-shadow: 0 4px 12px var(--el-color-primary-light-9);
 							}
 
 							svg {
 								font-size: 24px;
-								color: #667eea;
+								color: var(--el-color-primary);
 							}
 
 							span {

+ 1 - 1
apps/web/src/views/LogStream.vue

@@ -213,7 +213,7 @@ const refreshLogs = () => {
 
 				.log-workflow {
 					font-size: 13px;
-					color: #667eea;
+					color: var(--el-color-primary);
 					font-weight: 500;
 				}
 			}

+ 1 - 1
apps/web/src/views/ModelLog.vue

@@ -260,7 +260,7 @@ const viewDetails = (log: any) => {
 				width: 56px;
 				height: 56px;
 				border-radius: 12px;
-				background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+				background: linear-gradient(135deg, var(--el-color-primary) 0%, #764ba2 100%);
 				display: flex;
 				align-items: center;
 				justify-content: center;

+ 5 - 6
apps/web/src/views/QuickStart.vue

@@ -108,7 +108,7 @@
 				max-width: 800px;
 				margin: 0 auto;
 				aspect-ratio: 16/9;
-				background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+				background: linear-gradient(135deg, var(--el-color-primary) 0%, #764ba2 100%);
 				border-radius: 12px;
 				display: flex;
 				flex-direction: column;
@@ -160,7 +160,7 @@
 					.step-number {
 						width: 40px;
 						height: 40px;
-						background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+						background: linear-gradient(135deg, var(--el-color-primary) 0%, #764ba2 100%);
 						color: #fff;
 						border-radius: 50%;
 						display: flex;
@@ -212,15 +212,14 @@
 					transition: all 0.3s ease;
 
 					&:hover {
-						border-color: #667eea;
-						box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
+						border-color: var(--el-color-primary);
+						box-shadow: 0 4px 12px var(--el-color-primary-light-7);
 						transform: translateY(-4px);
 					}
 
 					svg {
 						font-size: 48px;
-						color: #667eea;
-						margin-bottom: 16px;
+						color: var(--el-color-primary);
 					}
 
 					h3 {

+ 3 - 3
apps/web/src/views/Statistics.vue

@@ -38,15 +38,15 @@
 						:style="{
 							background:
 								selectedCardIdx === idx
-									? 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)'
+									? 'linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-color-primary-light-8) 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 4px 12px var(--el-color-primary-light-7)'
 									: '0 2px 8px rgba(0, 0, 0, 0.08)',
-							border: selectedCardIdx === idx ? '1px solid #0076ff' : '1px solid #f0f0f0',
+							border: selectedCardIdx === idx ? '1px solid var(--el-color-primary)' : '1px solid #f0f0f0',
 							textAlign: 'center',
 							cursor: 'pointer',
 							transition: 'all 0.3s ease'

+ 7 - 7
apps/web/src/views/UserCenter.vue

@@ -8,7 +8,7 @@
 			<div class="profile-card">
 				<div class="avatar-section">
 					<div class="avatar">
-						<SvgIcon name="user-round" size="60"/>
+						<SvgIcon name="user-round" size="60" />
 					</div>
 					<el-button type="primary" size="small">更换头像</el-button>
 				</div>
@@ -70,7 +70,7 @@
 				<div class="security-items">
 					<div class="security-item">
 						<div class="security-left">
-							<SvgIcon name="lock" />
+							<SvgIcon name="lock" size="30" />
 							<div class="security-text">
 								<div class="security-title">登录密码</div>
 								<div class="security-desc">定期更换密码可以提高账号安全性</div>
@@ -80,7 +80,7 @@
 					</div>
 					<div class="security-item">
 						<div class="security-left">
-							<SvgIcon name="shield" />
+							<SvgIcon name="shield" size="30" />
 							<div class="security-text">
 								<div class="security-title">双因素认证</div>
 								<div class="security-desc">未开启</div>
@@ -90,7 +90,7 @@
 					</div>
 					<div class="security-item">
 						<div class="security-left">
-							<SvgIcon name="smartphone" />
+							<SvgIcon name="smartphone" size="30" />
 							<div class="security-text">
 								<div class="security-title">绑定手机</div>
 								<div class="security-desc">131****1111</div>
@@ -151,7 +151,7 @@ const userInfo = ref({
 					width: 120px;
 					height: 120px;
 					border-radius: 50%;
-					background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+					background: linear-gradient(135deg, var(--el-color-primary) 0%, #764ba2 100%);
 					display: flex;
 					align-items: center;
 					justify-content: center;
@@ -191,7 +191,7 @@ const userInfo = ref({
 					width: 60px;
 					height: 60px;
 					border-radius: 12px;
-					background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+					background: linear-gradient(135deg, var(--el-color-primary) 0%, #764ba2 100%);
 					display: flex;
 					align-items: center;
 					justify-content: center;
@@ -249,7 +249,7 @@ const userInfo = ref({
 
 						svg {
 							font-size: 32px;
-							color: #667eea;
+							color: var(--el-color-primary);
 						}
 
 						.security-text {

+ 341 - 0
apps/web/src/views/WorkflowExecution.vue

@@ -0,0 +1,341 @@
+<template>
+	<div class="execution-container">
+		<div class="header">
+			<div>
+				<h1>执行</h1>
+				<p class="subtitle">聚合监控执行状态、耗时与失败原因</p>
+			</div>
+			<el-button type="primary" @click="refresh">
+				<el-icon><RefreshRight /></el-icon>
+				刷新
+			</el-button>
+		</div>
+
+		<div class="stats-grid">
+			<div class="stat-card">
+				<div class="stat-icon">
+					<SvgIcon name="play" size="20" />
+				</div>
+				<div>
+					<div class="stat-value">186</div>
+					<div class="stat-label">今日执行</div>
+				</div>
+			</div>
+			<div class="stat-card">
+				<div class="stat-icon success">
+					<SvgIcon name="check-circle" size="20" />
+				</div>
+				<div>
+					<div class="stat-value">96.8%</div>
+					<div class="stat-label">成功率</div>
+				</div>
+			</div>
+			<div class="stat-card">
+				<div class="stat-icon warning">
+					<SvgIcon name="clock" size="20" />
+				</div>
+				<div>
+					<div class="stat-value">2.4s</div>
+					<div class="stat-label">平均耗时</div>
+				</div>
+			</div>
+			<div class="stat-card">
+				<div class="stat-icon danger">
+					<SvgIcon name="x-circle" size="20" />
+				</div>
+				<div>
+					<div class="stat-value">6</div>
+					<div class="stat-label">失败数</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="filters">
+			<el-input v-model="keyword" placeholder="搜索工作流或执行 ID" clearable class="filter-item">
+				<template #prefix>
+					<el-icon><Search /></el-icon>
+				</template>
+			</el-input>
+			<el-select v-model="status" placeholder="状态" class="filter-item" clearable>
+				<el-option label="全部" value="" />
+				<el-option label="成功" value="success" />
+				<el-option label="失败" value="failed" />
+				<el-option label="运行中" value="running" />
+			</el-select>
+			<el-date-picker
+				v-model="dateRange"
+				type="daterange"
+				range-separator="至"
+				start-placeholder="开始"
+				end-placeholder="结束"
+				class="filter-item"
+			/>
+			<el-button @click="resetFilters">重置</el-button>
+		</div>
+
+		<div class="panel">
+			<el-table :data="filteredExecutions" stripe style="width: 100%">
+				<el-table-column prop="workflow" label="工作流" />
+				<el-table-column prop="executionId" label="执行 ID" />
+				<el-table-column prop="startedAt" label="开始时间" />
+				<el-table-column prop="duration" label="耗时" />
+				<el-table-column prop="trigger" label="触发方式" />
+				<el-table-column label="状态">
+					<template #default="scope">
+						<el-tag :type="statusType(scope.row.status)">
+							{{ statusText(scope.row.status) }}
+						</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column label="操作">
+					<template #default>
+						<el-button text size="small">详情</el-button>
+						<el-button text size="small">重试</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+		</div>
+
+		<div class="side-panels">
+			<div class="panel">
+				<div class="panel-title">执行概览</div>
+				<div class="summary-item" v-for="item in summary" :key="item.label">
+					<span class="label">{{ item.label }}</span>
+					<span class="value">{{ item.value }}</span>
+				</div>
+			</div>
+			<div class="panel">
+				<div class="panel-title">告警与建议</div>
+				<ul class="tips">
+					<li>失败率高的工作流建议开启重试与超时策略</li>
+					<li>高耗时节点建议拆分为异步执行</li>
+					<li>关键流程可接入通知节点提高可观测性</li>
+				</ul>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import { RefreshRight, Search } from '@element-plus/icons-vue'
+
+const keyword = ref('')
+const status = ref('')
+const dateRange = ref<[Date, Date] | null>(null)
+
+const executions = ref([
+	{
+		workflow: '客户支持自动分派',
+		executionId: 'EXE-20260129001',
+		startedAt: '2026-01-29 09:12:03',
+		duration: '2.1s',
+		trigger: '定时',
+		status: 'success'
+	},
+	{
+		workflow: '内容生成与审核',
+		executionId: 'EXE-20260129002',
+		startedAt: '2026-01-29 09:15:45',
+		duration: '4.3s',
+		trigger: '手动',
+		status: 'running'
+	},
+	{
+		workflow: 'RAG 知识库同步',
+		executionId: 'EXE-20260129003',
+		startedAt: '2026-01-29 08:58:12',
+		duration: '3.9s',
+		trigger: 'Webhook',
+		status: 'failed'
+	},
+	{
+		workflow: '财务报表汇总',
+		executionId: 'EXE-20260129004',
+		startedAt: '2026-01-29 08:40:30',
+		duration: '1.6s',
+		trigger: '定时',
+		status: 'success'
+	}
+])
+
+const summary = ref([
+	{ label: '正在运行', value: '3' },
+	{ label: '队列中', value: '8' },
+	{ label: '近 24h 执行量', value: '412' },
+	{ label: 'P95 耗时', value: '5.9s' }
+])
+
+const filteredExecutions = computed(() => {
+	const q = keyword.value.trim().toLowerCase()
+	return executions.value.filter((item) => {
+		const matchKeyword =
+			!q || item.workflow.toLowerCase().includes(q) || item.executionId.toLowerCase().includes(q)
+		const matchStatus = !status.value || item.status === status.value
+		return matchKeyword && matchStatus
+	})
+})
+
+const statusType = (value: string) => {
+	if (value === 'success') return 'success'
+	if (value === 'failed') return 'danger'
+	if (value === 'running') return 'warning'
+	return 'info'
+}
+
+const statusText = (value: string) => {
+	if (value === 'success') return '成功'
+	if (value === 'failed') return '失败'
+	if (value === 'running') return '运行中'
+	return '未知'
+}
+
+const refresh = () => {
+	console.log('refresh executions')
+}
+
+const resetFilters = () => {
+	keyword.value = ''
+	status.value = ''
+	dateRange.value = null
+}
+</script>
+
+<style lang="less" scoped>
+.execution-container {
+	max-width: 1200px;
+	margin: 0 auto;
+	padding: 24px;
+
+	.header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		margin-bottom: 24px;
+
+		h1 {
+			font-size: 28px;
+			margin: 0;
+			color: #222;
+		}
+
+		.subtitle {
+			margin: 6px 0 0;
+			color: #666;
+			font-size: 14px;
+		}
+	}
+
+	.stats-grid {
+		display: grid;
+		grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+		gap: 16px;
+		margin-bottom: 20px;
+
+		.stat-card {
+			background: #fff;
+			border-radius: 12px;
+			padding: 18px;
+			box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+			display: flex;
+			gap: 12px;
+			align-items: center;
+
+			.stat-icon {
+				width: 40px;
+				height: 40px;
+				border-radius: 10px;
+				background: #eef2ff;
+				color: #4f46e5;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+
+				&.success {
+					background: #ecfdf3;
+					color: #10b981;
+				}
+
+				&.warning {
+					background: #fff7ed;
+					color: #f97316;
+				}
+
+				&.danger {
+					background: #fef2f2;
+					color: #ef4444;
+				}
+			}
+
+			.stat-value {
+				font-size: 22px;
+				font-weight: 700;
+				color: #1f2937;
+			}
+
+			.stat-label {
+				font-size: 13px;
+				color: #6b7280;
+			}
+		}
+	}
+
+	.filters {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 12px;
+		margin-bottom: 16px;
+		align-items: center;
+
+		.filter-item {
+			width: 220px;
+		}
+	}
+
+	.panel {
+		background: #fff;
+		border-radius: 12px;
+		padding: 20px;
+		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+	}
+
+	.side-panels {
+		display: grid;
+		grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+		gap: 16px;
+		margin-top: 16px;
+
+		.panel-title {
+			font-size: 16px;
+			font-weight: 600;
+			margin-bottom: 12px;
+			color: #1f2937;
+		}
+
+		.summary-item {
+			display: flex;
+			justify-content: space-between;
+			padding: 10px 0;
+			border-bottom: 1px solid #f1f5f9;
+
+			.label {
+				color: #6b7280;
+				font-size: 13px;
+			}
+
+			.value {
+				font-weight: 600;
+				color: #111827;
+			}
+		}
+
+		.tips {
+			margin: 0;
+			padding-left: 16px;
+			color: #6b7280;
+			line-height: 1.6;
+			font-size: 13px;
+		}
+	}
+}
+</style>

+ 541 - 0
apps/web/src/views/WorkflowOrchestration.vue

@@ -0,0 +1,541 @@
+<template>
+	<div class="orchestration-container">
+		<div class="header">
+			<div>
+				<h1>流程设计</h1>
+				<p class="subtitle">把一堆"要做的事",按顺序、条件、规则,自动地串起来执行</p>
+			</div>
+			<div class="header-actions">
+				<el-button type="primary" @click="createWorkflow">
+					<SvgIcon name="workflow" size="16" />
+					创建工作流
+				</el-button>
+				<el-button @click="openTemplateModal">
+					<SvgIcon name="box" size="16" />
+					导入模板
+				</el-button>
+			</div>
+		</div>
+
+		<!-- 编排核心概念 -->
+		<div class="core-concepts">
+			<div class="concept-card sequence">
+				<div class="concept-icon">🔗</div>
+				<div class="concept-info">
+					<div class="concept-title">按顺序</div>
+					<div class="concept-desc">节点A → 节点B → 节点C,串联执行</div>
+				</div>
+				<div class="concept-example">开始 → HTTP请求 → 数据处理 → 发送通知</div>
+			</div>
+			<div class="concept-card condition">
+				<div class="concept-icon">🔀</div>
+				<div class="concept-info">
+					<div class="concept-title">按条件</div>
+					<div class="concept-desc">判断结果,走不同的分支路径</div>
+				</div>
+				<div class="concept-example">if (状态=成功) → 路径A else → 路径B</div>
+			</div>
+			<div class="concept-card rule">
+				<div class="concept-icon">⚙️</div>
+				<div class="concept-info">
+					<div class="concept-title">按规则</div>
+					<div class="concept-desc">触发器、定时任务、事件驱动自动执行</div>
+				</div>
+				<div class="concept-example">每天9点 | Webhook触发 | 数据变化时</div>
+			</div>
+		</div>
+
+		<div class="stats-grid">
+			<div class="stat-card">
+				<div class="stat-icon">
+					<SvgIcon name="workflow" size="22" />
+				</div>
+				<div class="stat-info">
+					<div class="stat-value">28</div>
+					<div class="stat-label">工作流总数</div>
+				</div>
+			</div>
+			<div class="stat-card">
+				<div class="stat-icon green">
+					<SvgIcon name="clock" size="22" />
+				</div>
+				<div class="stat-info">
+					<div class="stat-value">6</div>
+					<div class="stat-label">近 7 天更新</div>
+				</div>
+			</div>
+			<div class="stat-card">
+				<div class="stat-icon purple">
+					<SvgIcon name="box" size="22" />
+				</div>
+				<div class="stat-info">
+					<div class="stat-value">12</div>
+					<div class="stat-label">模板可用</div>
+				</div>
+			</div>
+			<div class="stat-card">
+				<div class="stat-icon orange">
+					<SvgIcon name="sparkle" size="22" />
+				</div>
+				<div class="stat-info">
+					<div class="stat-value">18</div>
+					<div class="stat-label">节点类型</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="content-grid">
+			<div class="panel">
+				<div class="panel-title">最近编排</div>
+				<div class="workflow-list">
+					<div class="workflow-item" v-for="item in recentWorkflows" :key="item.id">
+						<div class="workflow-main">
+							<div class="workflow-title">{{ item.name }}</div>
+							<div class="workflow-meta">{{ item.updatedAt }} · {{ item.owner }}</div>
+						</div>
+						<div class="workflow-tags">
+							<el-tag size="small" type="info">{{ item.tag }}</el-tag>
+							<el-button text size="small" @click="openWorkflow(item.id)">打开</el-button>
+						</div>
+					</div>
+				</div>
+			</div>
+
+			<div class="panel">
+				<div class="panel-title">可用节点类型</div>
+				<div class="node-types">
+					<div class="node-type-group">
+						<div class="node-type-label">流程控制</div>
+						<div class="node-type-tags">
+							<el-tag size="small" type="info">开始节点</el-tag>
+							<el-tag size="small" type="warning">条件分支</el-tag>
+							<el-tag size="small" type="danger">循环节点</el-tag>
+							<el-tag size="small" type="success">结束节点</el-tag>
+						</div>
+					</div>
+					<div class="node-type-group">
+						<div class="node-type-label">数据操作</div>
+						<div class="node-type-tags">
+							<el-tag size="small">HTTP请求</el-tag>
+							<el-tag size="small">数据库查询</el-tag>
+							<el-tag size="small">代码执行</el-tag>
+							<el-tag size="small">数据转换</el-tag>
+						</div>
+					</div>
+					<div class="node-type-group">
+						<div class="node-type-label">触发规则</div>
+						<div class="node-type-tags">
+							<el-tag size="small" type="info">定时触发</el-tag>
+							<el-tag size="small" type="info">Webhook</el-tag>
+							<el-tag size="small" type="info">事件监听</el-tag>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="panel full">
+			<div class="panel-title">推荐模板</div>
+			<div class="template-grid">
+				<div class="template-card" v-for="item in templates" :key="item.id">
+					<div class="template-title">{{ item.name }}</div>
+					<div class="template-desc">{{ item.desc }}</div>
+					<div class="template-footer">
+						<el-tag size="small">{{ item.category }}</el-tag>
+						<el-button text size="small" @click="useTemplate(item.id)">查看模板</el-button>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<!-- 模板弹窗 -->
+		<TemplateModal
+			:visible="templateModalVisible"
+			@close="closeTemplateModal"
+			@select="selectTemplate"
+		/>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import SvgIcon from '@/components/SvgIcon/index.vue'
+import TemplateModal from '@/components/TemplateModal/index.vue'
+import { v4 } from 'uuid'
+
+const router = useRouter()
+
+const templateModalVisible = ref(false)
+
+const recentWorkflows = ref([
+	{ id: '1', name: '客户支持自动分派', updatedAt: '今天 10:12', owner: '张伟', tag: '客服' },
+	{ id: '2', name: '内容生成与审核', updatedAt: '昨天 18:20', owner: '李娜', tag: '内容' },
+	{ id: '3', name: 'RAG 知识库同步', updatedAt: '1 月 27 日', owner: '王强', tag: '知识库' },
+	{ id: '4', name: '财务报表汇总', updatedAt: '1 月 26 日', owner: '赵敏', tag: '财务' }
+])
+
+const templates = ref([
+	{
+		id: 't1',
+		name: '客户意图识别',
+		desc: '接收消息 → AI分析意图 → 条件判断 → 自动分派客服/机器人',
+		category: '客服'
+	},
+	{
+		id: 't2',
+		name: '日报自动汇总',
+		desc: '定时触发 → 查询数据表 → 汇总计算 → 生成报告 → 通知团队',
+		category: '运营'
+	},
+	{
+		id: 't3',
+		name: '合同审查助手',
+		desc: '上传合同 → OCR识别 → AI审查 → 风险分类 → 生成报告',
+		category: '法务'
+	},
+	{
+		id: 't4',
+		name: '线索打标',
+		desc: 'Webhook接收 → 数据清洗 → 规则匹配 → 打标签 → 写入CRM',
+		category: '增长'
+	}
+])
+
+const createWorkflow = () => {
+	const id = v4()
+	router.push(`/workflow/${id}`)
+}
+
+const openTemplateModal = () => {
+	templateModalVisible.value = true
+}
+
+const closeTemplateModal = () => {
+	templateModalVisible.value = false
+}
+
+const selectTemplate = () => {
+	templateModalVisible.value = false
+	// 根据模板ID创建新工作流
+	const id = v4()
+	router.push(`/templates/${id}`)
+}
+
+const openWorkflow = (id: string) => {
+	router.push(`/workflow/${id}`)
+}
+
+const useTemplate = (id: string) => {
+	router.push(`/templates/${id}`)
+}
+</script>
+
+<style lang="less" scoped>
+.orchestration-container {
+	max-width: 1200px;
+	margin: 0 auto;
+	padding: 24px;
+
+	.header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		margin-bottom: 24px;
+
+		h1 {
+			font-size: 28px;
+			margin: 0;
+			color: #222;
+		}
+
+		.subtitle {
+			margin: 6px 0 0;
+			color: #666;
+			font-size: 14px;
+		}
+
+		.header-actions {
+			display: flex;
+			gap: 12px;
+
+			:deep(.el-button) {
+				&:not(:last-child) {
+					margin-right: 0;
+				}
+			}
+		}
+	}
+
+	.core-concepts {
+		display: grid;
+		grid-template-columns: repeat(3, 1fr);
+		gap: 16px;
+		margin-bottom: 24px;
+
+		@media (max-width: 900px) {
+			grid-template-columns: 1fr;
+		}
+
+		.concept-card {
+			background: #fff;
+			border-radius: 12px;
+			padding: 20px;
+			box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+			border-left: 4px solid #3b82f6;
+
+			&.sequence {
+				border-left-color: #3b82f6;
+			}
+
+			&.condition {
+				border-left-color: #f59e0b;
+			}
+
+			&.rule {
+				border-left-color: #10b981;
+			}
+
+			.concept-icon {
+				font-size: 32px;
+				margin-bottom: 12px;
+			}
+
+			.concept-info {
+				margin-bottom: 12px;
+
+				.concept-title {
+					font-size: 18px;
+					font-weight: 700;
+					color: #1f2937;
+					margin-bottom: 6px;
+				}
+
+				.concept-desc {
+					font-size: 13px;
+					color: #6b7280;
+					line-height: 1.6;
+				}
+			}
+
+			.concept-example {
+				font-size: 12px;
+				padding: 10px 12px;
+				background: #f9fafb;
+				border-radius: 8px;
+				color: #374151;
+				font-family: 'Consolas', 'Monaco', monospace;
+			}
+		}
+	}
+
+	.stats-grid {
+		display: grid;
+		grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+		gap: 16px;
+		margin-bottom: 24px;
+
+		.stat-card {
+			background: #fff;
+			border-radius: 12px;
+			padding: 18px;
+			display: flex;
+			align-items: center;
+			gap: 12px;
+			box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+
+			.stat-icon {
+				width: 40px;
+				height: 40px;
+				border-radius: 10px;
+				background: #eef2ff;
+				color: #4f46e5;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+
+				&.green {
+					background: #ecfdf3;
+					color: #10b981;
+				}
+
+				&.purple {
+					background: #f5f3ff;
+					color: #7c3aed;
+				}
+
+				&.orange {
+					background: #fff7ed;
+					color: #f97316;
+				}
+			}
+
+			.stat-info {
+				.stat-value {
+					font-size: 22px;
+					font-weight: 700;
+					color: #1f2937;
+				}
+
+				.stat-label {
+					font-size: 13px;
+					color: #6b7280;
+				}
+			}
+		}
+	}
+
+	.content-grid {
+		display: grid;
+		grid-template-columns: 2fr 1fr;
+		gap: 16px;
+		margin-bottom: 16px;
+
+		@media (max-width: 900px) {
+			grid-template-columns: 1fr;
+		}
+	}
+
+	.panel {
+		background: #fff;
+		border-radius: 12px;
+		padding: 20px;
+		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+
+		&.full {
+			margin-top: 8px;
+		}
+
+		.panel-title {
+			font-size: 16px;
+			font-weight: 600;
+			margin-bottom: 16px;
+			color: #1f2937;
+		}
+	}
+
+	.workflow-list {
+		display: flex;
+		flex-direction: column;
+		gap: 12px;
+
+		.workflow-item {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			padding: 12px;
+			border-radius: 10px;
+			border: 1px solid #f0f0f0;
+
+			.workflow-title {
+				font-weight: 600;
+				color: #111827;
+			}
+
+			.workflow-meta {
+				font-size: 12px;
+				color: #6b7280;
+				margin-top: 4px;
+			}
+
+			.workflow-tags {
+				display: flex;
+				gap: 8px;
+				align-items: center;
+			}
+		}
+	}
+
+	.feature-list {
+		display: flex;
+		flex-direction: column;
+		gap: 16px;
+
+		.feature-item {
+			display: flex;
+			gap: 12px;
+			align-items: flex-start;
+
+			.feature-icon {
+				width: 36px;
+				height: 36px;
+				border-radius: 10px;
+				background: #f3f4f6;
+				color: #374151;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+			}
+
+			.feature-title {
+				font-weight: 600;
+				color: #111827;
+			}
+
+			.feature-desc {
+				font-size: 13px;
+				color: #6b7280;
+				margin-top: 4px;
+			}
+		}
+	}
+
+	.node-types {
+		display: flex;
+		flex-direction: column;
+		gap: 16px;
+
+		.node-type-group {
+			.node-type-label {
+				font-size: 13px;
+				color: #6b7280;
+				margin-bottom: 8px;
+				font-weight: 600;
+			}
+
+			.node-type-tags {
+				display: flex;
+				flex-wrap: wrap;
+				gap: 8px;
+			}
+		}
+	}
+
+	.template-grid {
+		display: grid;
+		grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+		gap: 16px;
+
+		.template-card {
+			border: 1px solid #f0f0f0;
+			border-radius: 12px;
+			padding: 16px;
+			background: #fcfcff;
+
+			.template-title {
+				font-weight: 600;
+				color: #111827;
+				margin-bottom: 8px;
+			}
+
+			.template-desc {
+				font-size: 12px;
+				color: #6b7280;
+				line-height: 1.6;
+				min-height: 54px;
+				font-family: 'Consolas', 'Monaco', monospace;
+				background: #f9fafb;
+				padding: 8px;
+				border-radius: 6px;
+				margin-bottom: 10px;
+			}
+
+			.template-footer {
+				display: flex;
+				justify-content: space-between;
+				align-items: center;
+			}
+		}
+	}
+}
+</style>