|
|
@@ -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>
|