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