|
|
@@ -0,0 +1,1774 @@
|
|
|
+# Data Factory API 前端开发说明文档
|
|
|
+
|
|
|
+## 概述
|
|
|
+
|
|
|
+Data Factory API 提供了与 n8n 工作流引擎的集成接口,支持工作流的查询、状态监控、执行记录查看和工作流触发等功能。
|
|
|
+
|
|
|
+**版本**: v1.0
|
|
|
+**基础URL**: `/api/datafactory`
|
|
|
+**内容类型**: `application/json`
|
|
|
+**字符编码**: UTF-8
|
|
|
+
|
|
|
+## 通用响应格式
|
|
|
+
|
|
|
+所有API接口都遵循统一的响应格式:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200, // HTTP状态码
|
|
|
+ "message": "操作成功", // 操作结果消息
|
|
|
+ "data": {} // 返回的具体数据
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 状态码说明
|
|
|
+
|
|
|
+| 状态码 | 含义 | 说明 |
|
|
|
+|--------|------|------|
|
|
|
+| 200 | 成功 | 请求成功执行 |
|
|
|
+| 400 | 请求错误 | 请求参数错误或缺失 |
|
|
|
+| 401 | 认证失败 | n8n API Key 无效 |
|
|
|
+| 403 | 权限不足 | 无权访问该资源 |
|
|
|
+| 404 | 未找到 | 请求的资源不存在 |
|
|
|
+| 500 | 服务器错误 | 服务器内部错误 |
|
|
|
+| 503 | 服务不可用 | n8n 服务连接失败 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## API 接口详情
|
|
|
+
|
|
|
+### 1. 获取工作流列表
|
|
|
+
|
|
|
+**接口描述**: 获取 n8n 系统中的工作流列表,支持分页、搜索和状态过滤。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `GET`
|
|
|
+- **请求路径**: `/workflows`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 请求参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
|
|
+|--------|------|------|--------|------|
|
|
|
+| page | int | 否 | 1 | 页码,从1开始 |
|
|
|
+| page_size | int | 否 | 20 | 每页数量 |
|
|
|
+| active | string | 否 | - | 过滤活跃状态:`true`/`false` |
|
|
|
+| search | string | 否 | - | 按名称搜索关键词 |
|
|
|
+| tags | string | 否 | - | 按标签过滤,多个标签用逗号分隔 |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "获取工作流列表成功",
|
|
|
+ "data": {
|
|
|
+ "items": [
|
|
|
+ {
|
|
|
+ "id": "1PCBqwesRXiFcfJ1",
|
|
|
+ "name": "Simple RAG",
|
|
|
+ "active": false,
|
|
|
+ "tags": ["AI", "RAG"],
|
|
|
+ "created_at": "2025-12-01 10:00:00",
|
|
|
+ "updated_at": "2025-12-03 03:14:49"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "name": "My workflow 2",
|
|
|
+ "active": true,
|
|
|
+ "tags": [],
|
|
|
+ "created_at": "2025-10-28 11:00:00",
|
|
|
+ "updated_at": "2025-10-28 12:06:12"
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "total": 3,
|
|
|
+ "page": 1,
|
|
|
+ "page_size": 20,
|
|
|
+ "total_pages": 1
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 响应字段说明
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| items | array | 工作流列表 |
|
|
|
+| items[].id | string | 工作流唯一标识 |
|
|
|
+| items[].name | string | 工作流名称 |
|
|
|
+| items[].active | boolean | 是否激活状态 |
|
|
|
+| items[].tags | array | 标签列表 |
|
|
|
+| items[].created_at | string | 创建时间 |
|
|
|
+| items[].updated_at | string | 更新时间 |
|
|
|
+| total | int | 总记录数 |
|
|
|
+| page | int | 当前页码 |
|
|
|
+| page_size | int | 每页数量 |
|
|
|
+| total_pages | int | 总页数 |
|
|
|
+
|
|
|
+#### Vue 代码示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="workflow-list">
|
|
|
+ <!-- 搜索栏 -->
|
|
|
+ <div class="search-bar">
|
|
|
+ <el-input
|
|
|
+ v-model="searchKeyword"
|
|
|
+ placeholder="搜索工作流名称"
|
|
|
+ clearable
|
|
|
+ @clear="fetchWorkflows"
|
|
|
+ @keyup.enter="fetchWorkflows"
|
|
|
+ >
|
|
|
+ <template #append>
|
|
|
+ <el-button @click="fetchWorkflows">
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+
|
|
|
+ <el-select v-model="activeFilter" placeholder="状态筛选" clearable @change="fetchWorkflows">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
+ <el-option label="活跃" value="true" />
|
|
|
+ <el-option label="停用" value="false" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 工作流表格 -->
|
|
|
+ <el-table :data="workflows" v-loading="loading" stripe>
|
|
|
+ <el-table-column prop="id" label="ID" width="180" />
|
|
|
+ <el-table-column prop="name" label="工作流名称" />
|
|
|
+ <el-table-column prop="active" label="状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.active ? 'success' : 'info'">
|
|
|
+ {{ row.active ? '活跃' : '停用' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="tags" label="标签" width="200">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-for="tag in row.tags" :key="tag" size="small" class="tag-item">
|
|
|
+ {{ tag }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="updated_at" label="更新时间" width="180" />
|
|
|
+ <el-table-column label="操作" width="200" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link @click="viewDetail(row.id)">详情</el-button>
|
|
|
+ <el-button type="primary" link @click="viewStatus(row.id)">状态</el-button>
|
|
|
+ <el-button type="primary" link @click="viewExecutions(row.id)">执行记录</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.page"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ :total="pagination.total"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="fetchWorkflows"
|
|
|
+ @current-change="fetchWorkflows"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted } from 'vue'
|
|
|
+import { Search } from '@element-plus/icons-vue'
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const workflows = ref([])
|
|
|
+const searchKeyword = ref('')
|
|
|
+const activeFilter = ref('')
|
|
|
+
|
|
|
+const pagination = reactive({
|
|
|
+ page: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ total: 0
|
|
|
+})
|
|
|
+
|
|
|
+const fetchWorkflows = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ page: pagination.page,
|
|
|
+ page_size: pagination.pageSize
|
|
|
+ }
|
|
|
+
|
|
|
+ if (searchKeyword.value) {
|
|
|
+ params.search = searchKeyword.value
|
|
|
+ }
|
|
|
+ if (activeFilter.value) {
|
|
|
+ params.active = activeFilter.value
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await axios.get('/api/datafactory/workflows', { params })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ workflows.value = response.data.data.items
|
|
|
+ pagination.total = response.data.data.total
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('获取工作流列表失败')
|
|
|
+ console.error(error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const viewDetail = (id) => {
|
|
|
+ // 跳转到详情页
|
|
|
+ router.push(`/datafactory/workflow/${id}`)
|
|
|
+}
|
|
|
+
|
|
|
+const viewStatus = (id) => {
|
|
|
+ // 跳转到状态页
|
|
|
+ router.push(`/datafactory/workflow/${id}/status`)
|
|
|
+}
|
|
|
+
|
|
|
+const viewExecutions = (id) => {
|
|
|
+ // 跳转到执行记录页
|
|
|
+ router.push(`/datafactory/workflow/${id}/executions`)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchWorkflows()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.workflow-list {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-bar {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-bar .el-input {
|
|
|
+ width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-item {
|
|
|
+ margin-right: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination {
|
|
|
+ margin-top: 20px;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2. 获取工作流详情
|
|
|
+
|
|
|
+**接口描述**: 根据工作流ID获取详细信息,包括节点列表和配置。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `GET`
|
|
|
+- **请求路径**: `/workflows/{workflow_id}`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 路径参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| workflow_id | string | 是 | 工作流ID |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "获取工作流详情成功",
|
|
|
+ "data": {
|
|
|
+ "id": "1PCBqwesRXiFcfJ1",
|
|
|
+ "name": "Simple RAG",
|
|
|
+ "active": false,
|
|
|
+ "tags": ["AI", "RAG"],
|
|
|
+ "created_at": "2025-12-01 10:00:00",
|
|
|
+ "updated_at": "2025-12-03 03:14:49",
|
|
|
+ "nodes_count": 5,
|
|
|
+ "nodes": [
|
|
|
+ {
|
|
|
+ "id": "node-1",
|
|
|
+ "name": "Start",
|
|
|
+ "type": "n8n-nodes-base.manualTrigger",
|
|
|
+ "type_version": 1,
|
|
|
+ "position": [250, 300],
|
|
|
+ "disabled": false
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "node-2",
|
|
|
+ "name": "HTTP Request",
|
|
|
+ "type": "n8n-nodes-base.httpRequest",
|
|
|
+ "type_version": 4,
|
|
|
+ "position": [450, 300],
|
|
|
+ "disabled": false
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "settings": {
|
|
|
+ "executionOrder": "v1"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 响应字段说明
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| id | string | 工作流ID |
|
|
|
+| name | string | 工作流名称 |
|
|
|
+| active | boolean | 是否激活 |
|
|
|
+| tags | array | 标签列表 |
|
|
|
+| created_at | string | 创建时间 |
|
|
|
+| updated_at | string | 更新时间 |
|
|
|
+| nodes_count | int | 节点数量 |
|
|
|
+| nodes | array | 节点列表 |
|
|
|
+| nodes[].id | string | 节点ID |
|
|
|
+| nodes[].name | string | 节点名称 |
|
|
|
+| nodes[].type | string | 节点类型 |
|
|
|
+| nodes[].type_version | int | 节点版本 |
|
|
|
+| nodes[].position | array | 节点位置 [x, y] |
|
|
|
+| nodes[].disabled | boolean | 是否禁用 |
|
|
|
+| settings | object | 工作流设置 |
|
|
|
+
|
|
|
+#### Vue 代码示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="workflow-detail" v-loading="loading">
|
|
|
+ <el-page-header @back="goBack" :title="workflow.name">
|
|
|
+ <template #content>
|
|
|
+ <div class="header-content">
|
|
|
+ <el-tag :type="workflow.active ? 'success' : 'info'" size="large">
|
|
|
+ {{ workflow.active ? '活跃' : '停用' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template #extra>
|
|
|
+ <el-button-group>
|
|
|
+ <el-button
|
|
|
+ v-if="!workflow.active"
|
|
|
+ type="success"
|
|
|
+ @click="activateWorkflow"
|
|
|
+ >
|
|
|
+ 激活
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-else
|
|
|
+ type="warning"
|
|
|
+ @click="deactivateWorkflow"
|
|
|
+ >
|
|
|
+ 停用
|
|
|
+ </el-button>
|
|
|
+ </el-button-group>
|
|
|
+ </template>
|
|
|
+ </el-page-header>
|
|
|
+
|
|
|
+ <el-descriptions :column="2" border class="detail-info">
|
|
|
+ <el-descriptions-item label="工作流ID">{{ workflow.id }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="节点数量">{{ workflow.nodes_count }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ workflow.created_at }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="更新时间">{{ workflow.updated_at }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="标签" :span="2">
|
|
|
+ <el-tag v-for="tag in workflow.tags" :key="tag" class="tag-item">
|
|
|
+ {{ tag }}
|
|
|
+ </el-tag>
|
|
|
+ <span v-if="!workflow.tags?.length">无</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <el-card class="nodes-card">
|
|
|
+ <template #header>
|
|
|
+ <span>节点列表</span>
|
|
|
+ </template>
|
|
|
+ <el-table :data="workflow.nodes" stripe>
|
|
|
+ <el-table-column prop="name" label="节点名称" />
|
|
|
+ <el-table-column prop="type" label="节点类型" />
|
|
|
+ <el-table-column prop="type_version" label="版本" width="80" />
|
|
|
+ <el-table-column prop="disabled" label="状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.disabled ? 'danger' : 'success'" size="small">
|
|
|
+ {{ row.disabled ? '禁用' : '启用' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import { useRoute, useRouter } from 'vue-router'
|
|
|
+import axios from 'axios'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const router = useRouter()
|
|
|
+const loading = ref(false)
|
|
|
+const workflow = ref({})
|
|
|
+
|
|
|
+const workflowId = route.params.id
|
|
|
+
|
|
|
+const fetchWorkflowDetail = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/datafactory/workflows/${workflowId}`)
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ workflow.value = response.data.data
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('获取工作流详情失败')
|
|
|
+ console.error(error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const activateWorkflow = async () => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('确定要激活该工作流吗?', '提示', {
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ const response = await axios.post(`/api/datafactory/workflows/${workflowId}/activate`)
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ ElMessage.success('工作流已激活')
|
|
|
+ fetchWorkflowDetail()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ ElMessage.error('激活失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const deactivateWorkflow = async () => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('确定要停用该工作流吗?', '提示', {
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ const response = await axios.post(`/api/datafactory/workflows/${workflowId}/deactivate`)
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ ElMessage.success('工作流已停用')
|
|
|
+ fetchWorkflowDetail()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ ElMessage.error('停用失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const goBack = () => {
|
|
|
+ router.back()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchWorkflowDetail()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.workflow-detail {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.header-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-info {
|
|
|
+ margin: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.nodes-card {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-item {
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3. 获取工作流状态
|
|
|
+
|
|
|
+**接口描述**: 获取工作流的运行状态和最近执行情况统计。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `GET`
|
|
|
+- **请求路径**: `/workflows/{workflow_id}/status`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 路径参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| workflow_id | string | 是 | 工作流ID |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "获取工作流状态成功",
|
|
|
+ "data": {
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "name": "My workflow 2",
|
|
|
+ "active": true,
|
|
|
+ "status": "active",
|
|
|
+ "status_label": "运行中",
|
|
|
+ "recent_executions": {
|
|
|
+ "total": 5,
|
|
|
+ "success": 4,
|
|
|
+ "error": 1
|
|
|
+ },
|
|
|
+ "last_execution": {
|
|
|
+ "id": "12345",
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "workflow_name": "My workflow 2",
|
|
|
+ "status": "success",
|
|
|
+ "status_label": "成功",
|
|
|
+ "mode": "trigger",
|
|
|
+ "started_at": "2025-12-24 10:30:00",
|
|
|
+ "finished_at": "2025-12-24 10:30:05",
|
|
|
+ "retry_of": null,
|
|
|
+ "retry_success_id": null
|
|
|
+ },
|
|
|
+ "updated_at": "2025-10-28 12:06:12"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 响应字段说明
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| workflow_id | string | 工作流ID |
|
|
|
+| name | string | 工作流名称 |
|
|
|
+| active | boolean | 是否激活 |
|
|
|
+| status | string | 状态码:active/inactive |
|
|
|
+| status_label | string | 状态显示文本 |
|
|
|
+| recent_executions | object | 最近执行统计 |
|
|
|
+| recent_executions.total | int | 最近执行总数 |
|
|
|
+| recent_executions.success | int | 成功次数 |
|
|
|
+| recent_executions.error | int | 失败次数 |
|
|
|
+| last_execution | object | 最后一次执行信息(可能为null) |
|
|
|
+| updated_at | string | 更新时间 |
|
|
|
+
|
|
|
+#### Vue 代码示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="workflow-status" v-loading="loading">
|
|
|
+ <el-card class="status-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>{{ status.name }}</span>
|
|
|
+ <el-tag :type="status.active ? 'success' : 'info'" size="large">
|
|
|
+ {{ status.status_label }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-statistic title="最近执行总数" :value="status.recent_executions?.total || 0" />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-statistic title="成功次数" :value="status.recent_executions?.success || 0">
|
|
|
+ <template #suffix>
|
|
|
+ <el-icon color="#67C23A"><SuccessFilled /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-statistic>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-statistic title="失败次数" :value="status.recent_executions?.error || 0">
|
|
|
+ <template #suffix>
|
|
|
+ <el-icon color="#F56C6C"><CircleCloseFilled /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-statistic>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card class="last-execution-card" v-if="status.last_execution">
|
|
|
+ <template #header>
|
|
|
+ <span>最后一次执行</span>
|
|
|
+ </template>
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
+ <el-descriptions-item label="执行ID">
|
|
|
+ {{ status.last_execution.id }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
+ <el-tag :type="getStatusType(status.last_execution.status)">
|
|
|
+ {{ status.last_execution.status_label }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="开始时间">
|
|
|
+ {{ status.last_execution.started_at }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="结束时间">
|
|
|
+ {{ status.last_execution.finished_at }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="执行模式">
|
|
|
+ {{ status.last_execution.mode }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-empty v-else description="暂无执行记录" />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
+import { SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const loading = ref(false)
|
|
|
+const status = ref({})
|
|
|
+let refreshTimer = null
|
|
|
+
|
|
|
+const workflowId = route.params.id
|
|
|
+
|
|
|
+const fetchStatus = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/datafactory/workflows/${workflowId}/status`)
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ status.value = response.data.data
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取状态失败', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getStatusType = (statusCode) => {
|
|
|
+ const types = {
|
|
|
+ success: 'success',
|
|
|
+ error: 'danger',
|
|
|
+ waiting: 'warning',
|
|
|
+ running: 'primary'
|
|
|
+ }
|
|
|
+ return types[statusCode] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchStatus()
|
|
|
+ // 每30秒自动刷新状态
|
|
|
+ refreshTimer = setInterval(fetchStatus, 30000)
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (refreshTimer) {
|
|
|
+ clearInterval(refreshTimer)
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.workflow-status {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.last-execution-card {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 4. 激活工作流
|
|
|
+
|
|
|
+**接口描述**: 激活指定的工作流。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `POST`
|
|
|
+- **请求路径**: `/workflows/{workflow_id}/activate`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 路径参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| workflow_id | string | 是 | 工作流ID |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "工作流激活成功",
|
|
|
+ "data": {
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "active": true,
|
|
|
+ "message": "工作流已激活"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5. 停用工作流
|
|
|
+
|
|
|
+**接口描述**: 停用指定的工作流。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `POST`
|
|
|
+- **请求路径**: `/workflows/{workflow_id}/deactivate`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 路径参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| workflow_id | string | 是 | 工作流ID |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "工作流停用成功",
|
|
|
+ "data": {
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "active": false,
|
|
|
+ "message": "工作流已停用"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 6. 获取工作流执行记录列表
|
|
|
+
|
|
|
+**接口描述**: 获取指定工作流的执行记录列表。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `GET`
|
|
|
+- **请求路径**: `/workflows/{workflow_id}/executions`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 路径参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| workflow_id | string | 是 | 工作流ID |
|
|
|
+
|
|
|
+#### 请求参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
|
|
+|--------|------|------|--------|------|
|
|
|
+| page | int | 否 | 1 | 页码 |
|
|
|
+| page_size | int | 否 | 20 | 每页数量 |
|
|
|
+| status | string | 否 | - | 状态过滤:`success`/`error`/`waiting` |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "获取执行记录列表成功",
|
|
|
+ "data": {
|
|
|
+ "items": [
|
|
|
+ {
|
|
|
+ "id": "12345",
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "workflow_name": "My workflow 2",
|
|
|
+ "status": "success",
|
|
|
+ "status_label": "成功",
|
|
|
+ "mode": "trigger",
|
|
|
+ "started_at": "2025-12-24 10:30:00",
|
|
|
+ "finished_at": "2025-12-24 10:30:05",
|
|
|
+ "retry_of": null,
|
|
|
+ "retry_success_id": null
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": "12344",
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "workflow_name": "My workflow 2",
|
|
|
+ "status": "error",
|
|
|
+ "status_label": "失败",
|
|
|
+ "mode": "manual",
|
|
|
+ "started_at": "2025-12-24 09:15:00",
|
|
|
+ "finished_at": "2025-12-24 09:15:03",
|
|
|
+ "retry_of": null,
|
|
|
+ "retry_success_id": null
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "total": 25,
|
|
|
+ "page": 1,
|
|
|
+ "page_size": 20,
|
|
|
+ "total_pages": 2
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 响应字段说明
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| items[].id | string | 执行记录ID |
|
|
|
+| items[].workflow_id | string | 工作流ID |
|
|
|
+| items[].workflow_name | string | 工作流名称 |
|
|
|
+| items[].status | string | 状态码:success/error/waiting/running |
|
|
|
+| items[].status_label | string | 状态显示文本 |
|
|
|
+| items[].mode | string | 执行模式:trigger/manual/webhook等 |
|
|
|
+| items[].started_at | string | 开始时间 |
|
|
|
+| items[].finished_at | string | 结束时间 |
|
|
|
+| items[].retry_of | string | 重试来源执行ID(可能为null) |
|
|
|
+| items[].retry_success_id | string | 重试成功执行ID(可能为null) |
|
|
|
+
|
|
|
+#### Vue 代码示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="execution-list">
|
|
|
+ <!-- 筛选栏 -->
|
|
|
+ <div class="filter-bar">
|
|
|
+ <el-select v-model="statusFilter" placeholder="状态筛选" clearable @change="fetchExecutions">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
+ <el-option label="成功" value="success" />
|
|
|
+ <el-option label="失败" value="error" />
|
|
|
+ <el-option label="等待中" value="waiting" />
|
|
|
+ </el-select>
|
|
|
+
|
|
|
+ <el-button type="primary" @click="fetchExecutions">
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
+ 刷新
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 执行记录表格 -->
|
|
|
+ <el-table :data="executions" v-loading="loading" stripe>
|
|
|
+ <el-table-column prop="id" label="执行ID" width="120" />
|
|
|
+ <el-table-column prop="status" label="状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="getStatusType(row.status)">
|
|
|
+ {{ row.status_label }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="mode" label="执行模式" width="100" />
|
|
|
+ <el-table-column prop="started_at" label="开始时间" width="180" />
|
|
|
+ <el-table-column prop="finished_at" label="结束时间" width="180" />
|
|
|
+ <el-table-column label="耗时" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ calculateDuration(row.started_at, row.finished_at) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="100" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link @click="viewExecutionDetail(row.id)">
|
|
|
+ 详情
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.page"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ :total="pagination.total"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next"
|
|
|
+ @size-change="fetchExecutions"
|
|
|
+ @current-change="fetchExecutions"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted } from 'vue'
|
|
|
+import { useRoute, useRouter } from 'vue-router'
|
|
|
+import { Refresh } from '@element-plus/icons-vue'
|
|
|
+import axios from 'axios'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const router = useRouter()
|
|
|
+const loading = ref(false)
|
|
|
+const executions = ref([])
|
|
|
+const statusFilter = ref('')
|
|
|
+
|
|
|
+const workflowId = route.params.id
|
|
|
+
|
|
|
+const pagination = reactive({
|
|
|
+ page: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ total: 0
|
|
|
+})
|
|
|
+
|
|
|
+const fetchExecutions = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ page: pagination.page,
|
|
|
+ page_size: pagination.pageSize
|
|
|
+ }
|
|
|
+
|
|
|
+ if (statusFilter.value) {
|
|
|
+ params.status = statusFilter.value
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await axios.get(
|
|
|
+ `/api/datafactory/workflows/${workflowId}/executions`,
|
|
|
+ { params }
|
|
|
+ )
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ executions.value = response.data.data.items
|
|
|
+ pagination.total = response.data.data.total
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取执行记录失败', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getStatusType = (status) => {
|
|
|
+ const types = {
|
|
|
+ success: 'success',
|
|
|
+ error: 'danger',
|
|
|
+ waiting: 'warning',
|
|
|
+ running: 'primary'
|
|
|
+ }
|
|
|
+ return types[status] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+const calculateDuration = (start, end) => {
|
|
|
+ if (!start || !end) return '-'
|
|
|
+ const duration = dayjs(end).diff(dayjs(start), 'second')
|
|
|
+ if (duration < 60) return `${duration}秒`
|
|
|
+ if (duration < 3600) return `${Math.floor(duration / 60)}分${duration % 60}秒`
|
|
|
+ return `${Math.floor(duration / 3600)}时${Math.floor((duration % 3600) / 60)}分`
|
|
|
+}
|
|
|
+
|
|
|
+const viewExecutionDetail = (executionId) => {
|
|
|
+ router.push(`/datafactory/executions/${executionId}`)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchExecutions()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.execution-list {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-bar {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination {
|
|
|
+ margin-top: 20px;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 7. 获取所有执行记录列表
|
|
|
+
|
|
|
+**接口描述**: 获取所有工作流的执行记录列表(不限定工作流)。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `GET`
|
|
|
+- **请求路径**: `/executions`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 请求参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
|
|
+|--------|------|------|--------|------|
|
|
|
+| page | int | 否 | 1 | 页码 |
|
|
|
+| page_size | int | 否 | 20 | 每页数量 |
|
|
|
+| workflow_id | string | 否 | - | 按工作流ID过滤 |
|
|
|
+| status | string | 否 | - | 状态过滤:`success`/`error`/`waiting` |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+响应格式与 `/workflows/{workflow_id}/executions` 相同。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 8. 获取执行详情
|
|
|
+
|
|
|
+**接口描述**: 获取单次执行的详细信息,包括各节点的执行结果。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `GET`
|
|
|
+- **请求路径**: `/executions/{execution_id}`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 路径参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| execution_id | string | 是 | 执行ID |
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "获取执行详情成功",
|
|
|
+ "data": {
|
|
|
+ "id": "12345",
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "workflow_name": "My workflow 2",
|
|
|
+ "status": "success",
|
|
|
+ "status_label": "成功",
|
|
|
+ "mode": "trigger",
|
|
|
+ "started_at": "2025-12-24 10:30:00",
|
|
|
+ "finished_at": "2025-12-24 10:30:05",
|
|
|
+ "retry_of": null,
|
|
|
+ "retry_success_id": null,
|
|
|
+ "node_results": [
|
|
|
+ {
|
|
|
+ "node_name": "Start",
|
|
|
+ "start_time": "2025-12-24 10:30:00",
|
|
|
+ "execution_time": 10,
|
|
|
+ "source": [],
|
|
|
+ "data": {
|
|
|
+ "main": [[{"json": {"started": true}}]]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "node_name": "HTTP Request",
|
|
|
+ "start_time": "2025-12-24 10:30:01",
|
|
|
+ "execution_time": 3500,
|
|
|
+ "source": [{"previousNode": "Start"}],
|
|
|
+ "data": {
|
|
|
+ "main": [[{"json": {"response": "OK"}}]]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "error": null
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 响应字段说明
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| node_results | array | 各节点执行结果列表 |
|
|
|
+| node_results[].node_name | string | 节点名称 |
|
|
|
+| node_results[].start_time | string | 节点开始执行时间 |
|
|
|
+| node_results[].execution_time | int | 执行耗时(毫秒) |
|
|
|
+| node_results[].source | array | 数据来源节点 |
|
|
|
+| node_results[].data | object | 节点输出数据 |
|
|
|
+| error | object | 错误信息(成功时为null) |
|
|
|
+
|
|
|
+#### Vue 代码示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="execution-detail" v-loading="loading">
|
|
|
+ <el-page-header @back="goBack" title="执行详情">
|
|
|
+ <template #content>
|
|
|
+ <el-tag :type="getStatusType(execution.status)" size="large">
|
|
|
+ {{ execution.status_label }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-page-header>
|
|
|
+
|
|
|
+ <el-descriptions :column="2" border class="execution-info">
|
|
|
+ <el-descriptions-item label="执行ID">{{ execution.id }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="工作流">{{ execution.workflow_name }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="执行模式">{{ execution.mode }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
+ <el-tag :type="getStatusType(execution.status)">
|
|
|
+ {{ execution.status_label }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="开始时间">{{ execution.started_at }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="结束时间">{{ execution.finished_at }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <!-- 错误信息 -->
|
|
|
+ <el-alert
|
|
|
+ v-if="execution.error"
|
|
|
+ :title="execution.error.message || '执行出错'"
|
|
|
+ type="error"
|
|
|
+ :description="JSON.stringify(execution.error, null, 2)"
|
|
|
+ show-icon
|
|
|
+ class="error-alert"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 节点执行结果 -->
|
|
|
+ <el-card class="node-results-card">
|
|
|
+ <template #header>
|
|
|
+ <span>节点执行结果</span>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-timeline>
|
|
|
+ <el-timeline-item
|
|
|
+ v-for="(node, index) in execution.node_results"
|
|
|
+ :key="index"
|
|
|
+ :type="getNodeStatusType(node)"
|
|
|
+ :timestamp="node.start_time"
|
|
|
+ >
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <div class="node-header">
|
|
|
+ <span class="node-name">{{ node.node_name }}</span>
|
|
|
+ <el-tag size="small">{{ node.execution_time }}ms</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="node-data">
|
|
|
+ <el-collapse>
|
|
|
+ <el-collapse-item title="输出数据">
|
|
|
+ <pre>{{ JSON.stringify(node.data, null, 2) }}</pre>
|
|
|
+ </el-collapse-item>
|
|
|
+ </el-collapse>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import { useRoute, useRouter } from 'vue-router'
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const router = useRouter()
|
|
|
+const loading = ref(false)
|
|
|
+const execution = ref({})
|
|
|
+
|
|
|
+const executionId = route.params.id
|
|
|
+
|
|
|
+const fetchExecutionDetail = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`/api/datafactory/executions/${executionId}`)
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ execution.value = response.data.data
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取执行详情失败', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getStatusType = (status) => {
|
|
|
+ const types = {
|
|
|
+ success: 'success',
|
|
|
+ error: 'danger',
|
|
|
+ waiting: 'warning',
|
|
|
+ running: 'primary'
|
|
|
+ }
|
|
|
+ return types[status] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+const getNodeStatusType = (node) => {
|
|
|
+ // 根据节点执行情况判断状态
|
|
|
+ if (node.error) return 'danger'
|
|
|
+ return 'success'
|
|
|
+}
|
|
|
+
|
|
|
+const goBack = () => {
|
|
|
+ router.back()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchExecutionDetail()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.execution-detail {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.execution-info {
|
|
|
+ margin: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.error-alert {
|
|
|
+ margin: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.node-results-card {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.node-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.node-name {
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.node-data pre {
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow-x: auto;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 9. 触发工作流执行
|
|
|
+
|
|
|
+**接口描述**: 通过 Webhook 触发工作流执行。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `POST`
|
|
|
+- **请求路径**: `/workflows/{workflow_id}/execute`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 路径参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| workflow_id | string | 是 | 工作流ID |
|
|
|
+
|
|
|
+#### 请求参数
|
|
|
+
|
|
|
+| 参数名 | 类型 | 必填 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| webhook_path | string | 是 | Webhook 路径(工作流中配置的路径) |
|
|
|
+| data | object | 否 | 传递给工作流的数据 |
|
|
|
+
|
|
|
+#### 请求体示例
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "webhook_path": "my-webhook-path",
|
|
|
+ "data": {
|
|
|
+ "user_id": 12345,
|
|
|
+ "action": "process",
|
|
|
+ "params": {
|
|
|
+ "key1": "value1",
|
|
|
+ "key2": "value2"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+**成功响应**:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "工作流触发成功",
|
|
|
+ "data": {
|
|
|
+ "success": true,
|
|
|
+ "message": "工作流已通过 Webhook 触发",
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX",
|
|
|
+ "response": {
|
|
|
+ "result": "OK"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**缺少 webhook_path 响应**:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 400,
|
|
|
+ "message": "请提供 Webhook 路径以触发工作流",
|
|
|
+ "data": {
|
|
|
+ "success": false,
|
|
|
+ "message": "请提供 Webhook 路径以触发工作流",
|
|
|
+ "workflow_id": "9w5VhCRlRrjFqDpX"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 代码示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="trigger-workflow">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <span>触发工作流执行</span>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
|
|
+ <el-form-item label="Webhook路径" prop="webhook_path">
|
|
|
+ <el-input v-model="form.webhook_path" placeholder="请输入Webhook路径" />
|
|
|
+ <div class="form-tip">工作流中 Webhook 节点配置的路径</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="传递数据">
|
|
|
+ <el-input
|
|
|
+ v-model="form.dataJson"
|
|
|
+ type="textarea"
|
|
|
+ :rows="8"
|
|
|
+ placeholder="请输入JSON格式的数据(可选)"
|
|
|
+ />
|
|
|
+ <div class="form-tip">JSON格式,例如:{"key": "value"}</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="triggerWorkflow" :loading="loading">
|
|
|
+ 触发执行
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="resetForm">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 执行结果 -->
|
|
|
+ <el-card v-if="result" class="result-card">
|
|
|
+ <template #header>
|
|
|
+ <span>执行结果</span>
|
|
|
+ </template>
|
|
|
+ <el-result
|
|
|
+ :icon="result.success ? 'success' : 'error'"
|
|
|
+ :title="result.success ? '触发成功' : '触发失败'"
|
|
|
+ :sub-title="result.message"
|
|
|
+ >
|
|
|
+ <template #extra>
|
|
|
+ <pre v-if="result.response">{{ JSON.stringify(result.response, null, 2) }}</pre>
|
|
|
+ </template>
|
|
|
+ </el-result>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive } from 'vue'
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
+import axios from 'axios'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const formRef = ref()
|
|
|
+const loading = ref(false)
|
|
|
+const result = ref(null)
|
|
|
+
|
|
|
+const workflowId = route.params.id
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ webhook_path: '',
|
|
|
+ dataJson: ''
|
|
|
+})
|
|
|
+
|
|
|
+const rules = {
|
|
|
+ webhook_path: [
|
|
|
+ { required: true, message: '请输入Webhook路径', trigger: 'blur' }
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+const triggerWorkflow = async () => {
|
|
|
+ try {
|
|
|
+ await formRef.value.validate()
|
|
|
+ } catch {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析JSON数据
|
|
|
+ let data = {}
|
|
|
+ if (form.dataJson) {
|
|
|
+ try {
|
|
|
+ data = JSON.parse(form.dataJson)
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error('数据格式错误,请输入有效的JSON')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true
|
|
|
+ result.value = null
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await axios.post(`/api/datafactory/workflows/${workflowId}/execute`, {
|
|
|
+ webhook_path: form.webhook_path,
|
|
|
+ data: data
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ result.value = response.data.data
|
|
|
+ ElMessage.success('工作流触发成功')
|
|
|
+ } else {
|
|
|
+ result.value = {
|
|
|
+ success: false,
|
|
|
+ message: response.data.message
|
|
|
+ }
|
|
|
+ ElMessage.error(response.data.message)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ result.value = {
|
|
|
+ success: false,
|
|
|
+ message: error.message || '触发失败'
|
|
|
+ }
|
|
|
+ ElMessage.error('触发工作流失败')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const resetForm = () => {
|
|
|
+ formRef.value?.resetFields()
|
|
|
+ result.value = null
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.trigger-workflow {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.result-card {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.result-card pre {
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow-x: auto;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 10. 健康检查
|
|
|
+
|
|
|
+**接口描述**: 检查 n8n 服务的连接状态。
|
|
|
+
|
|
|
+#### 请求信息
|
|
|
+- **HTTP方法**: `GET`
|
|
|
+- **请求路径**: `/health`
|
|
|
+- **请求头**: `Content-Type: application/json`
|
|
|
+
|
|
|
+#### 响应数据
|
|
|
+
|
|
|
+**连接正常**:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "message": "n8n 服务连接正常",
|
|
|
+ "data": {
|
|
|
+ "status": "healthy",
|
|
|
+ "connected": true,
|
|
|
+ "api_url": "https://n8n.citupro.com"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**连接失败**:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 503,
|
|
|
+ "message": "n8n 服务连接失败: n8n API 认证失败,请检查 API Key 配置",
|
|
|
+ "data": {
|
|
|
+ "status": "unhealthy",
|
|
|
+ "connected": false,
|
|
|
+ "error": "n8n API 认证失败,请检查 API Key 配置",
|
|
|
+ "api_url": "https://n8n.citupro.com"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Vue 代码示例
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="health-check">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>n8n 服务状态</span>
|
|
|
+ <el-button type="primary" size="small" @click="checkHealth" :loading="loading">
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
+ 检查
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-result
|
|
|
+ :icon="health.connected ? 'success' : 'error'"
|
|
|
+ :title="health.connected ? '服务正常' : '服务异常'"
|
|
|
+ >
|
|
|
+ <template #sub-title>
|
|
|
+ <p>API地址: {{ health.api_url }}</p>
|
|
|
+ <p v-if="health.error" class="error-text">错误: {{ health.error }}</p>
|
|
|
+ </template>
|
|
|
+ </el-result>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import { Refresh } from '@element-plus/icons-vue'
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const health = ref({
|
|
|
+ connected: false,
|
|
|
+ api_url: ''
|
|
|
+})
|
|
|
+
|
|
|
+const checkHealth = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const response = await axios.get('/api/datafactory/health')
|
|
|
+ health.value = response.data.data
|
|
|
+ } catch (error) {
|
|
|
+ health.value = {
|
|
|
+ connected: false,
|
|
|
+ error: '无法连接到服务'
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ checkHealth()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.health-check {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.error-text {
|
|
|
+ color: #F56C6C;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## API 服务封装
|
|
|
+
|
|
|
+建议将 API 调用封装成独立的服务模块,便于统一管理和维护。
|
|
|
+
|
|
|
+### api/datafactory.js
|
|
|
+
|
|
|
+```javascript
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+const BASE_URL = '/api/datafactory'
|
|
|
+
|
|
|
+export const datafactoryApi = {
|
|
|
+ // 工作流相关
|
|
|
+ getWorkflows(params) {
|
|
|
+ return axios.get(`${BASE_URL}/workflows`, { params })
|
|
|
+ },
|
|
|
+
|
|
|
+ getWorkflow(workflowId) {
|
|
|
+ return axios.get(`${BASE_URL}/workflows/${workflowId}`)
|
|
|
+ },
|
|
|
+
|
|
|
+ getWorkflowStatus(workflowId) {
|
|
|
+ return axios.get(`${BASE_URL}/workflows/${workflowId}/status`)
|
|
|
+ },
|
|
|
+
|
|
|
+ activateWorkflow(workflowId) {
|
|
|
+ return axios.post(`${BASE_URL}/workflows/${workflowId}/activate`)
|
|
|
+ },
|
|
|
+
|
|
|
+ deactivateWorkflow(workflowId) {
|
|
|
+ return axios.post(`${BASE_URL}/workflows/${workflowId}/deactivate`)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 执行记录相关
|
|
|
+ getWorkflowExecutions(workflowId, params) {
|
|
|
+ return axios.get(`${BASE_URL}/workflows/${workflowId}/executions`, { params })
|
|
|
+ },
|
|
|
+
|
|
|
+ getAllExecutions(params) {
|
|
|
+ return axios.get(`${BASE_URL}/executions`, { params })
|
|
|
+ },
|
|
|
+
|
|
|
+ getExecution(executionId) {
|
|
|
+ return axios.get(`${BASE_URL}/executions/${executionId}`)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 触发执行
|
|
|
+ triggerWorkflow(workflowId, data) {
|
|
|
+ return axios.post(`${BASE_URL}/workflows/${workflowId}/execute`, data)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 健康检查
|
|
|
+ healthCheck() {
|
|
|
+ return axios.get(`${BASE_URL}/health`)
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 使用示例
|
|
|
+
|
|
|
+```javascript
|
|
|
+import { datafactoryApi } from '@/api/datafactory'
|
|
|
+
|
|
|
+// 获取工作流列表
|
|
|
+const response = await datafactoryApi.getWorkflows({
|
|
|
+ page: 1,
|
|
|
+ page_size: 20,
|
|
|
+ active: 'true'
|
|
|
+})
|
|
|
+
|
|
|
+// 触发工作流
|
|
|
+const result = await datafactoryApi.triggerWorkflow('workflow-id', {
|
|
|
+ webhook_path: 'my-webhook',
|
|
|
+ data: { key: 'value' }
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 路由配置示例
|
|
|
+
|
|
|
+### router/datafactory.js
|
|
|
+
|
|
|
+```javascript
|
|
|
+export default [
|
|
|
+ {
|
|
|
+ path: '/datafactory',
|
|
|
+ name: 'DataFactory',
|
|
|
+ component: () => import('@/views/datafactory/index.vue'),
|
|
|
+ meta: { title: '数据工厂' },
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ path: '',
|
|
|
+ name: 'WorkflowList',
|
|
|
+ component: () => import('@/views/datafactory/WorkflowList.vue'),
|
|
|
+ meta: { title: '工作流列表' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: 'workflow/:id',
|
|
|
+ name: 'WorkflowDetail',
|
|
|
+ component: () => import('@/views/datafactory/WorkflowDetail.vue'),
|
|
|
+ meta: { title: '工作流详情' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: 'workflow/:id/status',
|
|
|
+ name: 'WorkflowStatus',
|
|
|
+ component: () => import('@/views/datafactory/WorkflowStatus.vue'),
|
|
|
+ meta: { title: '工作流状态' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: 'workflow/:id/executions',
|
|
|
+ name: 'WorkflowExecutions',
|
|
|
+ component: () => import('@/views/datafactory/ExecutionList.vue'),
|
|
|
+ meta: { title: '执行记录' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: 'workflow/:id/trigger',
|
|
|
+ name: 'TriggerWorkflow',
|
|
|
+ component: () => import('@/views/datafactory/TriggerWorkflow.vue'),
|
|
|
+ meta: { title: '触发执行' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: 'executions/:id',
|
|
|
+ name: 'ExecutionDetail',
|
|
|
+ component: () => import('@/views/datafactory/ExecutionDetail.vue'),
|
|
|
+ meta: { title: '执行详情' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 注意事项
|
|
|
+
|
|
|
+1. **认证配置**: 后端已配置 n8n API Key,前端无需额外处理认证
|
|
|
+2. **跨域问题**: 后端已配置 CORS,支持跨域请求
|
|
|
+3. **错误处理**: 建议在 axios 拦截器中统一处理错误响应
|
|
|
+4. **状态刷新**: 工作流状态页面建议设置定时刷新(如30秒)
|
|
|
+5. **Webhook路径**: 触发工作流时需要提供正确的 Webhook 路径,该路径在 n8n 工作流的 Webhook 节点中配置
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 更新日志
|
|
|
+
|
|
|
+- **v1.0** (2025-12-24): 初始版本,包含工作流查询、状态监控、执行记录和触发执行功能
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 技术支持
|
|
|
+
|
|
|
+如有问题请联系后端开发团队或查看详细的 API 文档。
|
|
|
+
|
|
|
+**文档版本**: v1.0
|
|
|
+**最后更新**: 2025-12-24
|
|
|
+
|