TaskManager.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <template>
  2. <div class="task-page">
  3. <el-card class="task-card">
  4. <!-- 工具栏 -->
  5. <div class="toolbar" ref="toolbarRef">
  6. <!-- 你的筛选表单(保持不变) -->
  7. <el-form :inline="true" :model="filters" label-width="90px" @keyup.enter.native="onSearch">
  8. <el-form-item label="ID">
  9. <el-input v-model.number="filters.id" placeholder="精确 ID" clearable />
  10. </el-form-item>
  11. <el-form-item label="日期">
  12. <el-date-picker v-model="filters.date_string" type="date" value-format="YYYY-MM-DD" placeholder="YYYY-MM-DD" clearable />
  13. </el-form-item>
  14. <el-form-item label="Trace ID">
  15. <el-input v-model="filters.trace_id" placeholder="模糊匹配" clearable />
  16. </el-form-item>
  17. <el-form-item label="状态">
  18. <el-select v-model="filters.task_status" placeholder="全部" clearable style="width: 160px">
  19. <el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item>
  23. <el-button type="primary" @click="onSearch">查询</el-button>
  24. <el-button @click="onReset">重置</el-button>
  25. </el-form-item>
  26. <el-form-item>
  27. <el-switch v-model="autoRefresh" @change="toggleAutoRefresh" />
  28. <span class="ml-8">自动刷新(5s)</span>
  29. </el-form-item>
  30. </el-form>
  31. </div>
  32. <!-- 表格 -->
  33. <div class="table-wrapper">
  34. <el-table
  35. :data="rows"
  36. border
  37. stripe
  38. :height="tableHeight"
  39. @sort-change="onSortChange"
  40. class="custom-table"
  41. v-loading="loading"
  42. element-loading-text="加载中…"
  43. >
  44. <el-table-column prop="id" label="ID" sortable="custom" width="100" />
  45. <el-table-column prop="date_string" label="日期" sortable="custom" width="140" />
  46. <el-table-column prop="task_name" label="任务名称" min-width="200" :show-overflow-tooltip="true" />
  47. <el-table-column prop="task_status" label="状态" sortable="custom" width="140">
  48. <template #default="{ row }">
  49. <el-tag :type="statusType(row.task_status)">{{ row.status_text || row.task_status }}</el-tag>
  50. </template>
  51. </el-table-column>
  52. <el-table-column prop="start_timestamp" label="开始时间" sortable="custom" width="180">
  53. <template #default="{ row }">{{ formatTs(row.start_timestamp) }}</template>
  54. </el-table-column>
  55. <el-table-column prop="finish_timestamp" label="结束时间" sortable="custom" width="180">
  56. <template #default="{ row }">{{ formatTs(row.finish_timestamp) }}</template>
  57. </el-table-column>
  58. <el-table-column prop="trace_id" label="Trace ID" min-width="220" :show-overflow-tooltip="true" />
  59. <el-table-column label="操作" fixed="right" width="240">
  60. <template #default="{ row }">
  61. <el-button size="small" @click="openDetail(row)">详情</el-button>
  62. <el-button size="small" type="warning" @click="onRetry(row)" :disabled="row.task_status === 1">重试</el-button>
  63. <el-button size="small" type="danger" @click="onCancel(row)" :disabled="row.task_status === 2">取消</el-button>
  64. </template>
  65. </el-table-column>
  66. </el-table>
  67. </div>
  68. <!-- 分页 -->
  69. <div class="pager" ref="pagerRef">
  70. <el-pagination
  71. background
  72. layout="total, sizes, prev, pager, next, jumper"
  73. :page-sizes="[10, 20, 50, 100]"
  74. :page-size="query.page_size"
  75. :total="total"
  76. :current-page="query.page"
  77. @current-change="(p:number)=>{query.page=p; load()}"
  78. @size-change="(s:number)=>{query.page_size=s; query.page=1; load()}"
  79. />
  80. </div>
  81. </el-card>
  82. <!-- 详情 -->
  83. <el-drawer v-model="detailOpen" size="52%" title="任务详情">
  84. <div class="detail">
  85. <el-descriptions :column="2" border>
  86. <el-descriptions-item label="ID">{{ detail?.id }}</el-descriptions-item>
  87. <el-descriptions-item label="日期">{{ detail?.date_string }}</el-descriptions-item>
  88. <el-descriptions-item label="状态">{{ detail?.task_status }}({{ detail?.status_text }})</el-descriptions-item>
  89. <el-descriptions-item label="Trace ID">{{ detail?.trace_id }}</el-descriptions-item>
  90. <el-descriptions-item label="开始">{{ formatTs(detail?.start_timestamp) }}</el-descriptions-item>
  91. <el-descriptions-item label="结束">{{ formatTs(detail?.finish_timestamp) }}</el-descriptions-item>
  92. <el-descriptions-item label="任务名" :span="2">{{ detail?.task_name }}</el-descriptions-item>
  93. </el-descriptions>
  94. <h4 class="block-title">请求参数(data)</h4>
  95. <pre class="code-block">{{ pretty(detail?.data_json || detail?.data) }}</pre>
  96. </div>
  97. </el-drawer>
  98. </div>
  99. </template>
  100. <script setup lang="ts">
  101. import { onMounted, onBeforeUnmount, reactive, ref, nextTick } from 'vue'
  102. import { ElMessage, ElMessageBox } from 'element-plus'
  103. import { fetchTasks, fetchTaskDetail, retryTask, cancelTask, type TaskItem } from '../api/task'
  104. const statusOptions = [
  105. { label: '初始化(0)', value: 0 },
  106. { label: '处理中(1)', value: 1 },
  107. { label: '完成(2)', value: 2 },
  108. { label: '失败(99)', value: 99 },
  109. ]
  110. const filters = reactive<{ id: number | null; date_string: string | null; trace_id: string; task_status: number | null }>({
  111. id: null,
  112. date_string: null,
  113. trace_id: '',
  114. task_status: null,
  115. })
  116. const query = reactive({ page: 1, page_size: 20, sort_by: 'id', sort_dir: 'desc' as 'asc' | 'desc' })
  117. const rows = ref<TaskItem[]>([])
  118. const total = ref(0)
  119. const loading = ref(false)
  120. /* 自动刷新 */
  121. let timer: ReturnType<typeof setInterval> | null = null
  122. const autoRefresh = ref(false)
  123. const toggleAutoRefresh = () => {
  124. if (autoRefresh.value) {
  125. timer = setInterval(load, 5000)
  126. } else if (timer) {
  127. clearInterval(timer)
  128. timer = null
  129. }
  130. }
  131. /* 精准表格高度计算 */
  132. const toolbarRef = ref<HTMLElement | null>(null)
  133. const pagerRef = ref<HTMLElement | null>(null)
  134. const tableHeight = ref(400)
  135. const calcTableHeight = () => {
  136. const winH = window.innerHeight
  137. const toolbarH = toolbarRef.value?.offsetHeight ?? 0
  138. const pagerH = pagerRef.value?.offsetHeight ?? 0
  139. const outerPadding = 16 * 2 // .task-page 上下 padding
  140. const cardPadding = 20 * 2 // el-card 内部上下 padding(大约值)
  141. const gap = 12 + 12 // toolbar 下方间距 + pager 上方 padding
  142. tableHeight.value = Math.max(
  143. 260,
  144. winH - (outerPadding + cardPadding + toolbarH + pagerH + gap)
  145. )
  146. }
  147. /* 加载列表(POST) */
  148. const load = async () => {
  149. loading.value = true
  150. try {
  151. const params: any = { ...query }
  152. if (filters.id) params.id = filters.id
  153. if (filters.date_string) params.date_string = filters.date_string
  154. if (filters.trace_id) params.trace_id = filters.trace_id
  155. if (filters.task_status !== null && filters.task_status !== undefined) params.task_status = filters.task_status
  156. const resp = await fetchTasks(params)
  157. rows.value = resp.items
  158. total.value = resp.total
  159. await nextTick()
  160. calcTableHeight()
  161. } finally {
  162. loading.value = false
  163. }
  164. }
  165. /* 交互 */
  166. const onSearch = () => { query.page = 1; load() }
  167. const onReset = () => {
  168. Object.assign(filters, { id: null, date_string: null, trace_id: '', task_status: null })
  169. query.page = 1
  170. load()
  171. }
  172. const onSortChange = (e: any) => {
  173. query.sort_by = e.prop || 'id'
  174. query.sort_dir = e.order === 'ascending' ? 'asc' : 'desc'
  175. load()
  176. }
  177. /* 工具 */
  178. const statusType = (s: number) => (s === 0 ? 'info' : s === 1 ? 'warning' : s === 2 ? 'success' : s === 99 ? 'danger' : '')
  179. const formatTs = (ts?: number | null) => (!ts ? '-' : new Date(ts * 1000).toLocaleString())
  180. const pretty = (v: any) => { try { return typeof v === 'string' ? v : JSON.stringify(v, null, 2) } catch { return String(v) } }
  181. /* 详情 */
  182. const detailOpen = ref(false)
  183. const detail = ref<TaskItem | null>(null)
  184. const openDetail = async (row: TaskItem) => {
  185. const d = await fetchTaskDetail(row.id)
  186. detail.value = { ...row, ...d }
  187. detailOpen.value = true
  188. }
  189. /* 行操作 */
  190. const onRetry = async (row: TaskItem) => {
  191. await ElMessageBox.confirm(`确认将任务 #${row.id} 置为初始化并重试?`, '重试确认', { type: 'warning' })
  192. await retryTask(row.id)
  193. ElMessage.success('已触发重试')
  194. load()
  195. }
  196. const onCancel = async (row: TaskItem) => {
  197. await ElMessageBox.confirm(`确认取消任务 #${row.id}(置为失败)?`, '取消确认', { type: 'warning' })
  198. await cancelTask(row.id)
  199. ElMessage.success('已取消')
  200. load()
  201. }
  202. /* 生命周期 */
  203. onMounted(() => {
  204. load()
  205. window.addEventListener('resize', calcTableHeight)
  206. })
  207. onBeforeUnmount(() => {
  208. window.removeEventListener('resize', calcTableHeight)
  209. if (timer) clearInterval(timer)
  210. })
  211. </script>
  212. <style scoped>
  213. .task-page {
  214. padding: 16px;
  215. background: #f5f6fa;
  216. height: 100vh; /* 让页面可用高度=视口高度 */
  217. box-sizing: border-box;
  218. }
  219. .task-card {
  220. display: flex;
  221. flex-direction: column;
  222. height: 100%;
  223. box-shadow: 0 6px 18px rgba(0,0,0,0.06);
  224. border-radius: 10px;
  225. }
  226. .toolbar { margin-bottom: 12px; }
  227. .table-wrapper { flex: 1; overflow: hidden; }
  228. .custom-table { border-radius: 8px; overflow: hidden; }
  229. .pager {
  230. display: flex;
  231. justify-content: flex-end;
  232. padding: 12px 0 0 0; /* 顶部 12px,底部 0,避免留白 */
  233. background: #fff;
  234. border-top: 1px solid #f0f2f5;
  235. margin: 0; /* 清除可能的外边距 */
  236. }
  237. .detail { padding: 8px 0; }
  238. .block-title { margin: 14px 0 8px; font-weight: 600; }
  239. .code-block {
  240. background: #0b1021; color: #d1e7ff; padding: 12px;
  241. border-radius: 8px; overflow: auto; line-height: 1.5;
  242. }
  243. .ml-8 { margin-left: 8px; }
  244. </style>