|
@@ -0,0 +1,262 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="task-page">
|
|
|
|
|
+ <el-card class="task-card">
|
|
|
|
|
+ <!-- 工具栏 -->
|
|
|
|
|
+ <div class="toolbar" ref="toolbarRef">
|
|
|
|
|
+ <!-- 你的筛选表单(保持不变) -->
|
|
|
|
|
+ <el-form :inline="true" :model="filters" label-width="90px" @keyup.enter.native="onSearch">
|
|
|
|
|
+ <el-form-item label="ID">
|
|
|
|
|
+ <el-input v-model.number="filters.id" placeholder="精确 ID" clearable />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="日期">
|
|
|
|
|
+ <el-date-picker v-model="filters.date_string" type="date" value-format="YYYY-MM-DD" placeholder="YYYY-MM-DD" clearable />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="Trace ID">
|
|
|
|
|
+ <el-input v-model="filters.trace_id" placeholder="模糊匹配" clearable />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="状态">
|
|
|
|
|
+ <el-select v-model="filters.task_status" placeholder="全部" clearable style="width: 160px">
|
|
|
|
|
+ <el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" @click="onSearch">查询</el-button>
|
|
|
|
|
+ <el-button @click="onReset">重置</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-switch v-model="autoRefresh" @change="toggleAutoRefresh" />
|
|
|
|
|
+ <span class="ml-8">自动刷新(5s)</span>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 表格 -->
|
|
|
|
|
+ <div class="table-wrapper">
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ :data="rows"
|
|
|
|
|
+ border
|
|
|
|
|
+ stripe
|
|
|
|
|
+ :height="tableHeight"
|
|
|
|
|
+ @sort-change="onSortChange"
|
|
|
|
|
+ class="custom-table"
|
|
|
|
|
+ v-loading="loading"
|
|
|
|
|
+ element-loading-text="加载中…"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column prop="id" label="ID" sortable="custom" width="100" />
|
|
|
|
|
+ <el-table-column prop="date_string" label="日期" sortable="custom" width="140" />
|
|
|
|
|
+ <el-table-column prop="task_name" label="任务名称" min-width="200" :show-overflow-tooltip="true" />
|
|
|
|
|
+ <el-table-column prop="task_status" label="状态" sortable="custom" width="140">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag :type="statusType(row.task_status)">{{ row.status_text || row.task_status }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="start_timestamp" label="开始时间" sortable="custom" width="180">
|
|
|
|
|
+ <template #default="{ row }">{{ formatTs(row.start_timestamp) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="finish_timestamp" label="结束时间" sortable="custom" width="180">
|
|
|
|
|
+ <template #default="{ row }">{{ formatTs(row.finish_timestamp) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="trace_id" label="Trace ID" min-width="220" :show-overflow-tooltip="true" />
|
|
|
|
|
+ <el-table-column label="操作" fixed="right" width="240">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button size="small" @click="openDetail(row)">详情</el-button>
|
|
|
|
|
+ <el-button size="small" type="warning" @click="onRetry(row)" :disabled="row.task_status === 1">重试</el-button>
|
|
|
|
|
+ <el-button size="small" type="danger" @click="onCancel(row)" :disabled="row.task_status === 2">取消</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 分页 -->
|
|
|
|
|
+ <div class="pager" ref="pagerRef">
|
|
|
|
|
+ <el-pagination
|
|
|
|
|
+ background
|
|
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
+ :page-size="query.page_size"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ :current-page="query.page"
|
|
|
|
|
+ @current-change="(p:number)=>{query.page=p; load()}"
|
|
|
|
|
+ @size-change="(s:number)=>{query.page_size=s; query.page=1; load()}"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 详情 -->
|
|
|
|
|
+ <el-drawer v-model="detailOpen" size="52%" title="任务详情">
|
|
|
|
|
+ <div class="detail">
|
|
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
|
|
+ <el-descriptions-item label="ID">{{ detail?.id }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="日期">{{ detail?.date_string }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="状态">{{ detail?.task_status }}({{ detail?.status_text }})</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="Trace ID">{{ detail?.trace_id }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="开始">{{ formatTs(detail?.start_timestamp) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="结束">{{ formatTs(detail?.finish_timestamp) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="任务名" :span="2">{{ detail?.task_name }}</el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+
|
|
|
|
|
+ <h4 class="block-title">请求参数(data)</h4>
|
|
|
|
|
+ <pre class="code-block">{{ pretty(detail?.data_json || detail?.data) }}</pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-drawer>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { onMounted, onBeforeUnmount, reactive, ref, nextTick } from 'vue'
|
|
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
+import { fetchTasks, fetchTaskDetail, retryTask, cancelTask, type TaskItem } from '../api/task'
|
|
|
|
|
+
|
|
|
|
|
+const statusOptions = [
|
|
|
|
|
+ { label: '初始化(0)', value: 0 },
|
|
|
|
|
+ { label: '处理中(1)', value: 1 },
|
|
|
|
|
+ { label: '完成(2)', value: 2 },
|
|
|
|
|
+ { label: '失败(99)', value: 99 },
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+const filters = reactive<{ id: number | null; date_string: string | null; trace_id: string; task_status: number | null }>({
|
|
|
|
|
+ id: null,
|
|
|
|
|
+ date_string: null,
|
|
|
|
|
+ trace_id: '',
|
|
|
|
|
+ task_status: null,
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const query = reactive({ page: 1, page_size: 20, sort_by: 'id', sort_dir: 'desc' as 'asc' | 'desc' })
|
|
|
|
|
+const rows = ref<TaskItem[]>([])
|
|
|
|
|
+const total = ref(0)
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+/* 自动刷新 */
|
|
|
|
|
+let timer: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
+const autoRefresh = ref(false)
|
|
|
|
|
+const toggleAutoRefresh = () => {
|
|
|
|
|
+ if (autoRefresh.value) {
|
|
|
|
|
+ timer = setInterval(load, 5000)
|
|
|
|
|
+ } else if (timer) {
|
|
|
|
|
+ clearInterval(timer)
|
|
|
|
|
+ timer = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 精准表格高度计算 */
|
|
|
|
|
+const toolbarRef = ref<HTMLElement | null>(null)
|
|
|
|
|
+const pagerRef = ref<HTMLElement | null>(null)
|
|
|
|
|
+const tableHeight = ref(400)
|
|
|
|
|
+const calcTableHeight = () => {
|
|
|
|
|
+ const winH = window.innerHeight
|
|
|
|
|
+ const toolbarH = toolbarRef.value?.offsetHeight ?? 0
|
|
|
|
|
+ const pagerH = pagerRef.value?.offsetHeight ?? 0
|
|
|
|
|
+ const outerPadding = 16 * 2 // .task-page 上下 padding
|
|
|
|
|
+ const cardPadding = 20 * 2 // el-card 内部上下 padding(大约值)
|
|
|
|
|
+ const gap = 12 + 12 // toolbar 下方间距 + pager 上方 padding
|
|
|
|
|
+ tableHeight.value = Math.max(
|
|
|
|
|
+ 260,
|
|
|
|
|
+ winH - (outerPadding + cardPadding + toolbarH + pagerH + gap)
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 加载列表(POST) */
|
|
|
|
|
+const load = async () => {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const params: any = { ...query }
|
|
|
|
|
+ if (filters.id) params.id = filters.id
|
|
|
|
|
+ if (filters.date_string) params.date_string = filters.date_string
|
|
|
|
|
+ if (filters.trace_id) params.trace_id = filters.trace_id
|
|
|
|
|
+ if (filters.task_status !== null && filters.task_status !== undefined) params.task_status = filters.task_status
|
|
|
|
|
+
|
|
|
|
|
+ const resp = await fetchTasks(params)
|
|
|
|
|
+ rows.value = resp.items
|
|
|
|
|
+ total.value = resp.total
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ calcTableHeight()
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 交互 */
|
|
|
|
|
+const onSearch = () => { query.page = 1; load() }
|
|
|
|
|
+const onReset = () => {
|
|
|
|
|
+ Object.assign(filters, { id: null, date_string: null, trace_id: '', task_status: null })
|
|
|
|
|
+ query.page = 1
|
|
|
|
|
+ load()
|
|
|
|
|
+}
|
|
|
|
|
+const onSortChange = (e: any) => {
|
|
|
|
|
+ query.sort_by = e.prop || 'id'
|
|
|
|
|
+ query.sort_dir = e.order === 'ascending' ? 'asc' : 'desc'
|
|
|
|
|
+ load()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 工具 */
|
|
|
|
|
+const statusType = (s: number) => (s === 0 ? 'info' : s === 1 ? 'warning' : s === 2 ? 'success' : s === 99 ? 'danger' : '')
|
|
|
|
|
+const formatTs = (ts?: number | null) => (!ts ? '-' : new Date(ts * 1000).toLocaleString())
|
|
|
|
|
+const pretty = (v: any) => { try { return typeof v === 'string' ? v : JSON.stringify(v, null, 2) } catch { return String(v) } }
|
|
|
|
|
+
|
|
|
|
|
+/* 详情 */
|
|
|
|
|
+const detailOpen = ref(false)
|
|
|
|
|
+const detail = ref<TaskItem | null>(null)
|
|
|
|
|
+const openDetail = async (row: TaskItem) => {
|
|
|
|
|
+ const d = await fetchTaskDetail(row.id)
|
|
|
|
|
+ detail.value = { ...row, ...d }
|
|
|
|
|
+ detailOpen.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 行操作 */
|
|
|
|
|
+const onRetry = async (row: TaskItem) => {
|
|
|
|
|
+ await ElMessageBox.confirm(`确认将任务 #${row.id} 置为初始化并重试?`, '重试确认', { type: 'warning' })
|
|
|
|
|
+ await retryTask(row.id)
|
|
|
|
|
+ ElMessage.success('已触发重试')
|
|
|
|
|
+ load()
|
|
|
|
|
+}
|
|
|
|
|
+const onCancel = async (row: TaskItem) => {
|
|
|
|
|
+ await ElMessageBox.confirm(`确认取消任务 #${row.id}(置为失败)?`, '取消确认', { type: 'warning' })
|
|
|
|
|
+ await cancelTask(row.id)
|
|
|
|
|
+ ElMessage.success('已取消')
|
|
|
|
|
+ load()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 生命周期 */
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ load()
|
|
|
|
|
+ window.addEventListener('resize', calcTableHeight)
|
|
|
|
|
+})
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ window.removeEventListener('resize', calcTableHeight)
|
|
|
|
|
+ if (timer) clearInterval(timer)
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.task-page {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ background: #f5f6fa;
|
|
|
|
|
+ height: 100vh; /* 让页面可用高度=视口高度 */
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+.task-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ box-shadow: 0 6px 18px rgba(0,0,0,0.06);
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+.toolbar { margin-bottom: 12px; }
|
|
|
|
|
+.table-wrapper { flex: 1; overflow: hidden; }
|
|
|
|
|
+.custom-table { border-radius: 8px; overflow: hidden; }
|
|
|
|
|
+.pager {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ padding: 12px 0 0 0; /* 顶部 12px,底部 0,避免留白 */
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-top: 1px solid #f0f2f5;
|
|
|
|
|
+ margin: 0; /* 清除可能的外边距 */
|
|
|
|
|
+}
|
|
|
|
|
+.detail { padding: 8px 0; }
|
|
|
|
|
+.block-title { margin: 14px 0 8px; font-weight: 600; }
|
|
|
|
|
+.code-block {
|
|
|
|
|
+ background: #0b1021; color: #d1e7ff; padding: 12px;
|
|
|
|
|
+ border-radius: 8px; overflow: auto; line-height: 1.5;
|
|
|
|
|
+}
|
|
|
|
|
+.ml-8 { margin-left: 8px; }
|
|
|
|
|
+</style>
|