Jelajahi Sumber

数据工厂-工作流管理

Xiao_123 1 hari lalu
induk
melakukan
92c87e2ac1

+ 52 - 0
src/api/dataFactory.js

@@ -498,3 +498,55 @@ export function getProductionLineTasksLogs (params) {
   return http.post('/dags/exec-results/task-logs', params)
 }
 /** ================================= 生产线 监控 END ==================================================== */
+
+/** ================================= n8n 工作流管理 START ==================================================== */
+// 获取工作流列表
+export function getWorkflows (params) {
+  return http.get('/datafactory/workflows', params)
+}
+
+// 获取工作流详情
+export function getWorkflow (workflowId) {
+  return http.get(`/datafactory/workflows/${workflowId}`)
+}
+
+// 获取工作流状态
+export function getWorkflowStatus (workflowId) {
+  return http.get(`/datafactory/workflows/${workflowId}/status`)
+}
+
+// 激活工作流
+export function activateWorkflow (workflowId) {
+  return http.post(`/datafactory/workflows/${workflowId}/activate`)
+}
+
+// 停用工作流
+export function deactivateWorkflow (workflowId) {
+  return http.post(`/datafactory/workflows/${workflowId}/deactivate`)
+}
+
+// 获取工作流执行记录列表
+export function getWorkflowExecutions (workflowId, params) {
+  return http.get(`/datafactory/workflows/${workflowId}/executions`, params)
+}
+
+// 获取所有执行记录列表
+export function getAllExecutions (params) {
+  return http.get('/datafactory/executions', params)
+}
+
+// 获取执行详情
+export function getExecution (executionId) {
+  return http.get(`/datafactory/executions/${executionId}`)
+}
+
+// 触发工作流执行
+export function triggerWorkflow (workflowId, data) {
+  return http.post(`/datafactory/workflows/${workflowId}/execute`, data)
+}
+
+// 健康检查
+export function healthCheck () {
+  return http.get('/datafactory/health')
+}
+/** ================================= n8n 工作流管理 END ==================================================== */

+ 43 - 0
src/router/routes.js

@@ -1801,6 +1801,49 @@ export default {
           metastr: '{"keepAlive":false,"allowClick":false,"enName":"Assembly Line","editModules":false,"title":"生产线管理","fullScreen":false,"target":true}',
           open: null,
           target: true
+        },
+        {
+          meun: '',
+          code: '',
+          hidden: 0,
+          rootId: 972,
+          icon: '',
+          remark: '',
+          type: 1,
+          title: '工作流管理',
+          local: '',
+          path: '/dataFactory/workflow',
+          urls: '',
+          children: [],
+          enName: 'n8n Workflow',
+          id: 975,
+          redirect: '',
+          level: 2,
+          openPath: '',
+          active: '',
+          label: '工作流管理',
+          sort: 7,
+          parentId: 972,
+          effectiveStatus: true,
+          parentName: 'dataFactory',
+          component: 'dataFactory/workflow',
+          meta: {
+            keepAlive: false,
+            allowClick: false,
+            roles: [],
+            enName: 'Workflow',
+            icon: '',
+            editModules: false,
+            title: '工作流管理',
+            fullScreen: false,
+            target: false,
+            effectiveStatus: true
+          },
+          name: 'workflowIndex',
+          style: '',
+          alwaysShow: 0,
+          metastr: '{"keepAlive":false,"allowClick":false,"enName":"Workflow","editModules":false,"title":"工作流管理","fullScreen":false,"target":false}',
+          open: null
         }
         // {
         //   meun: '',

+ 340 - 0
src/views/dataFactory/workflow/ExecutionRecord.vue

@@ -0,0 +1,340 @@
+<template>
+  <div class="white" :class="{'mt-3': !workflowId}">
+    <!-- 筛选栏 -->
+    <FilterList v-if="!workflowId" :option="filterOption" @search="handleSearch" />
+
+    <!-- 执行记录表格 -->
+    <TableList
+      :loading="loading"
+      :headers="headers"
+      :items="executions"
+      :elevation="workflowId ? 0 : 5"
+      :total="pagination.total"
+      :page-info="pageInfo"
+      :disable-sort="true"
+      :is-tools="false"
+      :show-select="false"
+      @pageHandleChange="pageHandleChange"
+    >
+      <template #status="{ item }">
+        <v-chip
+          :color="getStatusColor(item.status)"
+          small
+          dark
+        >
+          {{ item.status_label }}
+        </v-chip>
+      </template>
+      <template #duration="{ item }">
+        {{ calculateDuration(item.started_at, item.finished_at) }}
+      </template>
+      <template #actions="{ item }">
+        <v-btn text color="primary" @click="viewExecutionDetail(item.id)">详情</v-btn>
+      </template>
+    </TableList>
+
+    <!-- 执行详情抽屉 -->
+    <v-navigation-drawer
+      v-model="drawer"
+      fixed
+      right
+      temporary
+      width="600"
+      style="z-index: var(--zIndex-drawers);"
+    >
+      <div class="execution-detail-drawer" v-loading="detailLoading">
+        <div class="drawer-header pa-3 d-flex align-center" :style="{'margin-top': workflowId ? '0' : '70px'}">
+          <span class="text-h6">执行详情</span>
+          <v-spacer />
+          <v-btn icon class="ml-2" @click="drawer = false">
+            <v-icon>mdi-close</v-icon>
+          </v-btn>
+        </div>
+        <v-divider></v-divider>
+        <div class="drawer-content pa-3" style="overflow-y: auto; height: calc(100vh - 80px);">
+          <m-card :no-title="true" :no-elevation="false" :px5="false">
+            <v-row>
+              <v-col cols="12">
+                <v-simple-table>
+                  <tbody>
+                    <tr>
+                      <td class="font-weight-bold" style="width: 120px;">执行ID</td>
+                      <td>{{ executionDetail.id }}</td>
+                    </tr>
+                    <tr>
+                      <td class="font-weight-bold">工作流</td>
+                      <td>{{ executionDetail.workflow_name }}</td>
+                    </tr>
+                    <tr>
+                      <td class="font-weight-bold">执行模式</td>
+                      <td>{{ executionDetail.mode }}</td>
+                    </tr>
+                    <tr>
+                      <td class="font-weight-bold">状态</td>
+                      <td>
+                        <v-chip
+                          :color="getStatusColor(executionDetail.status)"
+                          small
+                          dark
+                        >
+                          {{ executionDetail.status_label }}
+                        </v-chip>
+                      </td>
+                    </tr>
+                    <tr>
+                      <td class="font-weight-bold">开始时间</td>
+                      <td>{{ executionDetail.started_at }}</td>
+                    </tr>
+                    <tr>
+                      <td class="font-weight-bold">结束时间</td>
+                      <td>{{ executionDetail.finished_at }}</td>
+                    </tr>
+                  </tbody>
+                </v-simple-table>
+              </v-col>
+            </v-row>
+
+            <!-- 错误信息 -->
+            <v-alert
+              v-if="executionDetail.error"
+              type="error"
+              class="mt-4"
+              prominent
+            >
+              <div class="text-h6 mb-2">执行出错</div>
+              <pre class="error-content">{{ JSON.stringify(executionDetail.error, null, 2) }}</pre>
+            </v-alert>
+
+            <!-- 节点执行结果 -->
+            <m-card v-if="executionDetail.node_results && executionDetail.node_results.length > 0" class="mt-4" title="节点执行结果" :px5="false">
+              <v-timeline dense>
+                <v-timeline-item
+                  v-for="(node, index) in executionDetail.node_results"
+                  :key="index"
+                  :color="getNodeStatusColor(node)"
+                  small
+                >
+                  <m-card :no-title="true" :no-elevation="false" :px5="false" :min-height="0" class="mb-2">
+                    <template #autoTitle>
+                      <div class="d-flex justify-space-between align-center" style="width: 100%;">
+                        <span class="font-weight-bold">{{ node.node_name }}</span>
+                        <v-chip small>{{ node.execution_time }}ms</v-chip>
+                      </div>
+                    </template>
+                    <div class="text-caption mb-2 ml-5">开始时间: {{ currentTime(node.start_time) }}</div>
+                    <v-expansion-panels>
+                      <v-expansion-panel>
+                        <v-expansion-panel-header>输出数据</v-expansion-panel-header>
+                        <v-expansion-panel-content>
+                          <pre class="node-data">{{ JSON.stringify(node.data, null, 2) }}</pre>
+                        </v-expansion-panel-content>
+                      </v-expansion-panel>
+                    </v-expansion-panels>
+                  </m-card>
+                </v-timeline-item>
+              </v-timeline>
+            </m-card>
+
+            <empty
+              v-else-if="!detailLoading && executionDetail.id"
+              class="mt-4"
+            />
+          </m-card>
+        </div>
+      </div>
+    </v-navigation-drawer>
+  </div>
+</template>
+
+<script>
+import { getWorkflowExecutions, getAllExecutions, getExecution } from '@/api/dataFactory'
+import TableList from '@/components/List/table'
+import FilterList from '@/components/Filter'
+import Empty from '@/components/Common/empty'
+import MCard from '@/components/MCard'
+import { currentTime } from '@/utils/date'
+
+export default {
+  name: 'ExecutionList',
+  components: {
+    TableList,
+    FilterList,
+    Empty,
+    MCard
+  },
+  data () {
+    return {
+      loading: false,
+      executions: [],
+      drawer: false,
+      detailLoading: false,
+      executionDetail: {},
+      currentExecutionId: null,
+      filterOption: {
+        list: [
+          {
+            type: 'select',
+            value: null,
+            label: '状态',
+            key: 'status',
+            items: [
+              { label: '全部', value: null },
+              { label: '成功', value: 'success' },
+              { label: '失败', value: 'error' },
+              { label: '等待中', value: 'waiting' }
+            ]
+          }
+        ]
+      },
+      headers: [
+        { text: '执行ID', value: 'id' },
+        { text: '工作流名称', value: 'workflow_name' },
+        { text: '状态', value: 'status' },
+        { text: '执行模式', value: 'mode' },
+        { text: '开始时间', value: 'started_at' },
+        { text: '结束时间', value: 'finished_at' },
+        { text: '耗时', value: 'duration', sortable: false },
+        { text: '操作', value: 'actions', sortable: false }
+      ],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      pagination: {
+        total: 0
+      }
+    }
+  },
+  props: {
+    workflowId: {
+      type: String,
+      default: null
+    }
+  },
+  computed: {
+    currentWorkflowId () {
+      return this.workflowId || this.$route?.params?.id
+    }
+  },
+  mounted () {
+    this.fetchExecutions()
+  },
+  methods: {
+    handleSearch (filterData) {
+      this.pageInfo.current = 1
+      this.fetchExecutions(filterData)
+    },
+    async fetchExecutions (filterData = {}) {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size
+        }
+
+        if (filterData.status) {
+          params.status = filterData.status
+        }
+
+        let response
+        if (this.currentWorkflowId) {
+          response = await getWorkflowExecutions(this.currentWorkflowId, params)
+        } else {
+          response = await getAllExecutions(params)
+        }
+
+        if (response.code === 200) {
+          this.executions = response.data.items || []
+          this.pagination.total = response.data.total || 0
+        }
+      } catch (error) {
+        console.error('获取执行记录失败', error)
+      } finally {
+        this.loading = false
+      }
+    },
+    pageHandleChange (pageInfo) {
+      this.pageInfo = pageInfo
+      this.fetchExecutions()
+    },
+    getStatusColor (status) {
+      const colors = {
+        success: 'success',
+        error: 'error',
+        waiting: 'warning',
+        running: 'primary'
+      }
+      return colors[status] || 'grey'
+    },
+    calculateDuration (start, end) {
+      if (!start || !end) return '-'
+      const startTime = new Date(start.replace(/-/g, '/')).getTime()
+      const endTime = new Date(end.replace(/-/g, '/')).getTime()
+      const duration = Math.floor((endTime - startTime) / 1000) // 秒
+      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)}分`
+    },
+    async viewExecutionDetail (executionId) {
+      this.currentExecutionId = executionId
+      this.drawer = true
+      await this.fetchExecutionDetail(executionId)
+    },
+    async fetchExecutionDetail (executionId) {
+      this.detailLoading = true
+      try {
+        const response = await getExecution(executionId)
+        if (response.code === 200) {
+          this.executionDetail = response.data || {}
+        }
+      } catch (error) {
+        console.error('获取执行详情失败', error)
+      } finally {
+        this.detailLoading = false
+      }
+    },
+    getNodeStatusColor (node) {
+      if (node.error) return 'error'
+      return 'success'
+    },
+    currentTime (timestamp) {
+      return currentTime(timestamp)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.execution-detail-drawer {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.drawer-header {
+  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+}
+
+.drawer-content {
+  flex: 1;
+  background: #f0f2f5;
+}
+
+.error-content {
+  background: rgba(0, 0, 0, 0.05);
+  padding: 12px;
+  border-radius: 4px;
+  overflow-x: auto;
+  font-size: 12px;
+  margin: 0;
+}
+
+.node-data {
+  background: #f5f5f5;
+  padding: 10px;
+  border-radius: 4px;
+  overflow-x: auto;
+  font-size: 12px;
+  margin: 0;
+}
+</style>

+ 102 - 0
src/views/dataFactory/workflow/HealthCheck.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="health-check">
+    <m-card :no-title="true" :no-elevation="false" :px5="false">
+      <template #autoTitle>
+        <span>n8n 服务状态</span>
+        <v-spacer />
+        <v-btn color="primary" small @click="checkHealth">
+          <v-icon left>mdi-refresh</v-icon>
+          检查
+        </v-btn>
+      </template>
+
+      <v-row justify="center" class="py-8">
+        <v-col cols="12" md="8">
+          <m-card :no-title="true" :no-elevation="false" :px5="false" :min-height="0" body-style="padding: 0;">
+            <div
+              :class="`text-center pa-8 elevation-4 ${health.connected ? 'success' : 'error'} white--text`"
+              style="border-radius: 4px;"
+            >
+              <v-icon size="64" class="mb-4">
+                {{ health.connected ? 'mdi-check-circle' : 'mdi-alert-circle' }}
+              </v-icon>
+              <div class="text-h4 mb-4">
+                {{ health.connected ? '服务正常' : '服务异常' }}
+              </div>
+              <div class="text-body-1 mb-2">
+                API地址: {{ health.api_url || '-' }}
+              </div>
+              <div v-if="health.error" class="text-body-1 error--text">
+                错误: {{ health.error }}
+              </div>
+            </div>
+          </m-card>
+        </v-col>
+      </v-row>
+    </m-card>
+
+    <v-overlay :value="overlay" z-index="9">
+      <div class="d-flex flex-column align-center justify-center" style="width: 300px;">
+        <div class="mb-3">正在检查</div>
+        <v-progress-linear
+          color="primary"
+          indeterminate
+          rounded
+          height="6"
+        ></v-progress-linear>
+      </div>
+    </v-overlay>
+  </div>
+</template>
+
+<script>
+import { healthCheck } from '@/api/dataFactory'
+import MCard from '@/components/MCard'
+
+export default {
+  name: 'HealthCheck',
+  components: {
+    MCard
+  },
+  data () {
+    return {
+      overlay: false,
+      health: {
+        connected: false,
+        api_url: '',
+        error: ''
+      }
+    }
+  },
+  mounted () {
+    this.checkHealth()
+  },
+  methods: {
+    async checkHealth () {
+      this.overlay = true
+      try {
+        const response = await healthCheck()
+        this.health = response.data || {
+          connected: false,
+          api_url: '',
+          error: ''
+        }
+      } catch (error) {
+        this.health = {
+          connected: false,
+          api_url: '',
+          error: '无法连接到服务'
+        }
+      } finally {
+        this.overlay = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.health-check {
+  padding: 20px 0;
+}
+</style>

+ 174 - 0
src/views/dataFactory/workflow/TriggerWorkflow.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class="trigger-workflow pa-3 white">
+    <m-card :no-title="true" :no-elevation="false" :px5="false">
+      <template #autoTitle>
+        <v-btn icon @click="goBack">
+          <v-icon>mdi-arrow-left</v-icon>
+        </v-btn>
+        <span class="ml-2">触发工作流执行</span>
+      </template>
+
+      <v-form ref="form" v-model="valid">
+        <v-text-field
+          v-model="form.webhook_path"
+          label="Webhook路径"
+          :rules="rules.webhook_path"
+          required
+          outlined
+          hint="工作流中 Webhook 节点配置的路径"
+          persistent-hint
+          class="mb-3"
+        />
+
+        <v-textarea
+          v-model="form.dataJson"
+          label="传递数据(可选)"
+          outlined
+          rows="8"
+          hint='JSON格式,例如:{"key": "value"}'
+          persistent-hint
+          class="mb-3"
+        />
+
+        <div class="d-flex">
+          <v-btn
+            color="primary"
+            :loading="loading"
+            :disabled="!valid"
+            @click="triggerWorkflow"
+          >
+            触发执行
+          </v-btn>
+          <v-btn
+            class="ml-3"
+            @click="resetForm"
+          >
+            重置
+          </v-btn>
+        </div>
+      </v-form>
+    </m-card>
+
+    <!-- 执行结果 -->
+    <m-card v-if="result" class="mt-4" title="执行结果" :px5="false">
+      <v-alert
+        :type="result.success ? 'success' : 'error'"
+        prominent
+      >
+        <div class="text-h6 mb-2">
+          {{ result.success ? '触发成功' : '触发失败' }}
+        </div>
+        <div>{{ result.message }}</div>
+        <pre v-if="result.response" class="result-content mt-3">
+          {{ JSON.stringify(result.response, null, 2) }}
+        </pre>
+      </v-alert>
+    </m-card>
+  </div>
+</template>
+
+<script>
+import { triggerWorkflow } from '@/api/dataFactory'
+import MCard from '@/components/MCard'
+
+export default {
+  name: 'TriggerWorkflow',
+  components: {
+    MCard
+  },
+  data () {
+    return {
+      valid: false,
+      loading: false,
+      result: null,
+      form: {
+        webhook_path: '',
+        dataJson: ''
+      },
+      rules: {
+        webhook_path: [
+          v => !!v || '请输入Webhook路径'
+        ]
+      }
+    }
+  },
+  computed: {
+    workflowId () {
+      return this.$route.params.id
+    }
+  },
+  methods: {
+    async triggerWorkflow () {
+      if (!this.$refs.form.validate()) {
+        return
+      }
+
+      // 解析JSON数据
+      let data = {}
+      if (this.form.dataJson) {
+        try {
+          data = JSON.parse(this.form.dataJson)
+        } catch (e) {
+          this.$snackbar.error('数据格式错误,请输入有效的JSON')
+          return
+        }
+      }
+
+      this.loading = true
+      this.result = null
+
+      try {
+        const response = await triggerWorkflow(this.workflowId, {
+          webhook_path: this.form.webhook_path,
+          data: data
+        })
+
+        if (response.code === 200) {
+          this.result = response.data
+          this.$snackbar.success('工作流触发成功')
+        } else {
+          this.result = {
+            success: false,
+            message: response.message
+          }
+          this.$snackbar.error(response.message)
+        }
+      } catch (error) {
+        this.result = {
+          success: false,
+          message: error.message || '触发失败'
+        }
+        this.$snackbar.error('触发工作流失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    resetForm () {
+      this.$refs.form.reset()
+      this.result = null
+    },
+    goBack () {
+      this.$router.back()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.trigger-workflow {
+  height: 100%;
+  width: 100%;
+  background: #f0f2f5;
+  box-sizing: border-box;
+}
+
+.result-content {
+  background: rgba(0, 0, 0, 0.05);
+  padding: 15px;
+  border-radius: 4px;
+  overflow-x: auto;
+  text-align: left;
+  font-size: 12px;
+  margin: 0;
+}
+</style>

+ 59 - 0
src/views/dataFactory/workflow/WorkflowDetail.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="white">
+    <m-card :no-title="true" :px5="false">
+      <v-tabs v-model="tab" @change="handleTabChange">
+        <v-tab>基本信息</v-tab>
+        <v-tab>状态</v-tab>
+        <v-tab>执行记录</v-tab>
+      </v-tabs>
+      <v-divider></v-divider>
+      <v-tabs-items v-model="tab" class="mt-3 px-3">
+        <v-tab-item>
+          <base-info :workflowId="workflowId" />
+        </v-tab-item>
+        <v-tab-item>
+          <status-page :workflowId="workflowId" />
+        </v-tab-item>
+        <v-tab-item>
+          <execution-record :workflowId="workflowId" />
+        </v-tab-item>
+      </v-tabs-items>
+    </m-card>
+  </div>
+</template>
+
+<script>
+import MCard from '@/components/MCard'
+import baseInfo from './baseInfo.vue'
+import statusPage from './status.vue'
+import executionRecord from './ExecutionRecord.vue'
+
+export default {
+  name: 'WorkflowDetail',
+  components: {
+    MCard,
+    baseInfo,
+    statusPage,
+    executionRecord
+  },
+  props: {
+    workflowId: {
+      type: [String, Number],
+      default: null
+    }
+  },
+  data () {
+    return {
+      tab: 0
+    }
+  },
+  methods: {
+    handleTabChange (index) {
+      this.tab = index
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 166 - 0
src/views/dataFactory/workflow/WorkflowList.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="pt-3 white">
+    <!-- 筛选栏 -->
+    <filter-list :option="filterOption" @search="handleSearch" />
+
+    <!-- 工作流表格 -->
+    <table-list
+      :loading="loading"
+      :headers="headers"
+      :items="workflows"
+      :total="total"
+      :disable-sort="true"
+      :page-info="pageInfo"
+      :is-tools="false"
+      :show-select="false"
+      @pageHandleChange="pageHandleChange"
+    >
+      <template #active="{ item }">
+        <v-chip
+          :color="item.active ? 'success' : 'error'"
+          small
+          dark
+        >
+          {{ item.active ? '激活' : '停用' }}
+        </v-chip>
+      </template>
+      <template #tags="{ item }">
+        <v-chip v-for="tag in item.tags" :key="tag" small class="mr-1">{{ tag }}</v-chip>
+      </template>
+      <template #actions="{ item }">
+        <v-btn text color="primary" @click="handleDetail(item.id)">详情</v-btn>
+        <v-btn text color="primary" @click="handleExecution(item.id)">执行</v-btn>
+        <v-btn text v-if="!item.active" color="success" @click="handleStatus(item.id, true)">激活</v-btn>
+        <v-btn text v-else color="error" @click="handleStatus(item.id, false)">停用</v-btn>
+      </template>
+    </table-list>
+
+    <!-- 详情弹窗 -->
+    <dialog-page :visible.sync="show" title="工作流详情" :width-type="1" :footer="false">
+      <workflow-detail v-if="show" :workflowId="workflowId"/>
+    </dialog-page>
+  </div>
+</template>
+
+<script>
+import { getWorkflows, activateWorkflow, deactivateWorkflow } from '@/api/dataFactory'
+import TableList from '@/components/List/table'
+import FilterList from '@/components/Filter'
+import DialogPage from '@/components/Dialog'
+import WorkflowDetail from './WorkflowDetail'
+
+export default {
+  name: 'WorkflowList',
+  components: {
+    TableList,
+    FilterList,
+    DialogPage,
+    WorkflowDetail
+  },
+  data () {
+    return {
+      loading: false,
+      workflows: [],
+      filterOption: {
+        list: [
+          {
+            type: 'textField',
+            value: null,
+            label: '工作流名称',
+            key: 'search'
+          },
+          {
+            type: 'autocomplete',
+            value: null,
+            label: '状态',
+            key: 'active',
+            items: [
+              { label: '全部', value: null },
+              { label: '激活', value: 'true' },
+              { label: '停用', value: 'false' }
+            ]
+          }
+        ]
+      },
+      headers: [
+        { text: 'ID', value: 'id' },
+        { text: '工作流名称', value: 'name' },
+        { text: '状态', value: 'active' },
+        { text: '标签', value: 'tags', sortable: false },
+        { text: '创建时间', value: 'created_at' },
+        { text: '更新时间', value: 'updated_at' },
+        { text: '操作', value: 'actions', sortable: false }
+      ],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0,
+      show: false,
+      workflowId: null
+    }
+  },
+  mounted () {
+    this.fetchWorkflows()
+  },
+  methods: {
+    // 搜索
+    handleSearch (filterData) {
+      this.pageInfo.current = 1
+      this.fetchWorkflows(filterData)
+    },
+    // 获取工作流列表
+    async fetchWorkflows (filterData = {}) {
+      this.loading = true
+      try {
+        const params = {
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size
+        }
+        if (filterData.search) params.search = filterData.search
+        if (filterData.active) params.active = filterData.active
+
+        const { data } = await getWorkflows(params)
+        this.workflows = data.items || []
+        this.total = data.total || 0
+      } catch (error) {
+        this.$snackbar.error('获取工作流列表失败')
+        console.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    pageHandleChange (pageInfo) {
+      this.pageInfo = pageInfo
+      this.fetchWorkflows()
+    },
+    // 详情
+    async handleDetail (id) {
+      this.workflowId = id
+      this.show = true
+    },
+    // 激活、停用
+    async handleStatus (id, active) {
+      if (!id) return
+      const api = active ? activateWorkflow : deactivateWorkflow
+      try {
+        await api(id)
+        this.$snackbar.success(active ? '工作流已激活' : '工作流已停用')
+        this.fetchWorkflows()
+      } catch (error) {
+        this.$snackbar.error('操作失败')
+      }
+    },
+    // 执行工作流
+    async handleExecution (id) {
+      // if (!id) return
+      // try {
+      //   await triggerWorkflow(id, {})
+      //   this.$snackbar.success('工作流执行成功')
+      // } catch (error) {
+      //   this.$snackbar.error('执行失败')
+      // }
+    }
+  }
+}
+</script>

+ 101 - 0
src/views/dataFactory/workflow/baseInfo.vue

@@ -0,0 +1,101 @@
+<template>
+  <div class="workflow-detail-dialog">
+    <v-simple-table>
+      <tbody>
+        <tr>
+          <td class="font-weight-bold" style="width: 120px;">工作流ID</td>
+          <td>{{ workflow.id }}</td>
+        </tr>
+        <tr>
+          <td class="font-weight-bold">节点数量</td>
+          <td>{{ workflow.nodes_count || 0 }}</td>
+        </tr>
+        <tr>
+          <td class="font-weight-bold">创建时间</td>
+          <td>{{ workflow.created_at }}</td>
+        </tr>
+        <tr>
+          <td class="font-weight-bold">更新时间</td>
+          <td>{{ workflow.updated_at }}</td>
+        </tr>
+        <tr>
+          <td class="font-weight-bold">标签</td>
+          <td>
+            <v-chip v-for="tag in workflow.tags" :key="tag" small class="mr-1">{{ tag }}</v-chip>
+          </td>
+        </tr>
+      </tbody>
+    </v-simple-table>
+
+    <m-card class="my-3" title="节点列表" :noElevation="true">
+      <table-list
+        :loading="false"
+        :headers="nodeHeaders"
+        :items="workflow.nodes || []"
+        :is-page="false"
+        :is-tools="false"
+        :disable-sort="true"
+        :show-select="false"
+        :items-per-page="-1"
+      >
+        <template #disabled="{ item }">
+          <v-chip
+            :color="item.disabled ? 'error' : 'success'"
+            small
+            dark
+          >
+            {{ item.disabled ? '禁用' : '启用' }}
+          </v-chip>
+        </template>
+      </table-list>
+    </m-card>
+  </div>
+</template>
+
+<script>
+import MCard from '@/components/MCard'
+import TableList from '@/components/List/table'
+import { getWorkflow } from '@/api/dataFactory'
+
+export default {
+  name: 'WorkflowDetailDialog',
+  components: {
+    MCard,
+    TableList
+  },
+  props: {
+    workflowId: {
+      type: [String, Number],
+      default: null
+    }
+  },
+  data () {
+    return {
+      workflow: {},
+      nodeHeaders: [
+        { text: '节点名称', value: 'name' },
+        { text: '节点类型', value: 'type' },
+        { text: '版本', value: 'type_version' },
+        { text: '状态', value: 'disabled' }
+      ]
+    }
+  },
+  mounted () {
+    this.fetchWorkflow()
+  },
+  methods: {
+    async fetchWorkflow () {
+      const { data } = await getWorkflow(this.workflowId)
+      this.workflow = data || {}
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.workflow-detail-dialog {
+  width: 100%;
+  background: #f0f2f5;
+  box-sizing: border-box;
+}
+</style>

+ 54 - 0
src/views/dataFactory/workflow/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="workflow-container pa-3 white">
+    <v-tabs v-model="tab" @change="handleTabChange">
+      <v-tab>工作流列表</v-tab>
+      <v-tab>执行记录</v-tab>
+      <v-tab>健康检查</v-tab>
+    </v-tabs>
+    <v-tabs-items v-model="tab">
+      <v-tab-item>
+        <WorkflowList />
+      </v-tab-item>
+      <v-tab-item>
+        <ExecutionRecord />
+      </v-tab-item>
+      <v-tab-item>
+        <HealthCheck />
+      </v-tab-item>
+    </v-tabs-items>
+  </div>
+</template>
+
+<script>
+import WorkflowList from './WorkflowList'
+import ExecutionRecord from './ExecutionRecord'
+import HealthCheck from './HealthCheck'
+
+export default {
+  name: 'WorkflowIndex',
+  components: {
+    WorkflowList,
+    ExecutionRecord,
+    HealthCheck
+  },
+  data () {
+    return {
+      tab: 0
+    }
+  },
+  methods: {
+    handleTabChange (tab) {
+      this.tab = tab
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.workflow-container {
+  height: 100%;
+  width: 100%;
+  background: #f0f2f5;
+  box-sizing: border-box;
+}
+</style>

+ 169 - 0
src/views/dataFactory/workflow/status.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="workflow-status-dialog" v-loading="loading">
+    <m-card :no-title="true" :no-elevation="true" :px5="false">
+      <!-- <template #autoTitle>
+        <span>{{ status.name || '工作流状态' }}</span>
+        <v-spacer />
+        <v-chip
+          :color="status.active ? 'success' : 'grey'"
+          large
+          dark
+        >
+          {{ status.status_label || '未知' }}
+        </v-chip>
+      </template> -->
+
+      <v-row class="pt-3">
+        <v-col cols="12" md="4" >
+          <m-card :no-title="true" :px5="false" :min-height="0" body-style="padding: 16px;">
+            <div class="text-center">
+              <div class="text-h6 mb-2">最近执行总数</div>
+              <div class="text-h4 primary--text">
+                {{ status.recent_executions?.total || 0 }}
+              </div>
+            </div>
+          </m-card>
+        </v-col>
+        <v-col cols="12" md="4">
+          <m-card :no-title="true" :px5="false" :min-height="0" body-style="padding: 16px;">
+            <div class="text-center">
+              <div class="text-h6 mb-2">成功次数</div>
+              <div class="text-h4 success--text">
+                {{ status.recent_executions?.success || 0 }}
+              </div>
+            </div>
+          </m-card>
+        </v-col>
+        <v-col cols="12" md="4">
+          <m-card :no-title="true" :min-height="0" body-style="padding: 16px;">
+            <div class="text-center">
+              <div class="text-h6 mb-2">失败次数</div>
+              <div class="text-h4 error--text">
+                {{ status.recent_executions?.error || 0 }}
+              </div>
+            </div>
+          </m-card>
+        </v-col>
+      </v-row>
+
+      <m-card class="mt-4" title="最后一次执行记录">
+        <v-simple-table v-if="status.last_execution">
+          <tbody>
+            <tr>
+              <td class="font-weight-bold" style="width: 120px;">执行ID</td>
+              <td>{{ status.last_execution.id }}</td>
+            </tr>
+            <tr>
+              <td class="font-weight-bold">状态</td>
+              <td>
+                <v-chip
+                  :color="getStatusColor(status.last_execution.status)"
+                  small
+                  dark
+                >
+                  {{ status.last_execution.status_label }}
+                </v-chip>
+              </td>
+            </tr>
+            <tr>
+              <td class="font-weight-bold">开始时间</td>
+              <td>{{ status.last_execution.started_at }}</td>
+            </tr>
+            <tr>
+              <td class="font-weight-bold">结束时间</td>
+              <td>{{ status.last_execution.finished_at }}</td>
+            </tr>
+            <tr>
+              <td class="font-weight-bold">执行模式</td>
+              <td>{{ status.last_execution.mode }}</td>
+            </tr>
+          </tbody>
+        </v-simple-table>
+        <empty v-else class="mt-4" />
+      </m-card>
+    </m-card>
+  </div>
+</template>
+
+<script>
+import { getWorkflowStatus } from '@/api/dataFactory'
+import Empty from '@/components/Common/empty'
+import MCard from '@/components/MCard'
+
+export default {
+  name: 'WorkflowStatusDialog',
+  components: {
+    Empty,
+    MCard
+  },
+  props: {
+    workflowId: {
+      type: [String, Number],
+      default: null
+    }
+  },
+  data () {
+    return {
+      loading: false,
+      status: {},
+      refreshTimer: null
+    }
+  },
+  watch: {
+    workflowId: {
+      immediate: true,
+      handler (newVal) {
+        if (newVal) {
+          this.fetchStatus()
+          // 每30秒自动刷新状态
+          this.clearRefreshTimer()
+          this.refreshTimer = setInterval(() => {
+            this.fetchStatus()
+          }, 30000)
+        } else {
+          this.clearRefreshTimer()
+        }
+      }
+    }
+  },
+  beforeDestroy () {
+    this.clearRefreshTimer()
+  },
+  methods: {
+    clearRefreshTimer () {
+      if (this.refreshTimer) {
+        clearInterval(this.refreshTimer)
+        this.refreshTimer = null
+      }
+    },
+    async fetchStatus () {
+      this.loading = true
+      try {
+        const { data } = await getWorkflowStatus(this.workflowId)
+        this.status = data || {}
+      } catch (error) {
+        console.error('获取状态失败', error)
+      } finally {
+        this.loading = false
+      }
+    },
+    getStatusColor (status) {
+      const colors = {
+        success: 'success',
+        error: 'error',
+        waiting: 'warning',
+        running: 'primary'
+      }
+      return colors[status] || 'grey'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.workflow-status-dialog {
+  width: 100%;
+  background: #f0f2f5;
+  box-sizing: border-box;
+}
+</style>