|
|
@@ -0,0 +1,398 @@
|
|
|
+<template>
|
|
|
+ <div class="queue-job-log-container">
|
|
|
+ <!-- 搜索区域 -->
|
|
|
+ <el-card shadow="never" class="search-card">
|
|
|
+ <el-form :model="searchForm" inline>
|
|
|
+ <el-form-item label="任务名称">
|
|
|
+ <el-input
|
|
|
+ v-model="searchForm.name"
|
|
|
+ placeholder="请输入任务名称"
|
|
|
+ clearable
|
|
|
+ style="width: 240px"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="执行状态">
|
|
|
+ <el-select
|
|
|
+ v-model="searchForm.success"
|
|
|
+ placeholder="请选择状态"
|
|
|
+ clearable
|
|
|
+ style="width: 150px"
|
|
|
+ >
|
|
|
+ <el-option label="成功" :value="1" />
|
|
|
+ <el-option label="失败" :value="0" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="执行时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="searchForm.date_range"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="-"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ :default-time="[new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]"
|
|
|
+ style="width: 360px"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleSearch">
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="handleReset">
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <el-row :gutter="16" class="stats-row">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">总执行次数</div>
|
|
|
+ <div class="stat-value">{{ stats.total }}</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">成功次数</div>
|
|
|
+ <div class="stat-value stat-success">{{ stats.success_count }}</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">失败次数</div>
|
|
|
+ <div class="stat-value stat-fail">{{ stats.fail_count }}</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-label">成功率</div>
|
|
|
+ <div class="stat-value">{{ stats.success_rate }}%</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 表格区域 -->
|
|
|
+ <el-card shadow="never" class="table-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>日志列表</span>
|
|
|
+ <div class="header-actions">
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ :disabled="selectedIds.length === 0"
|
|
|
+ @click="handleBatchDelete"
|
|
|
+ >
|
|
|
+ 批量删除
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ plain
|
|
|
+ @click="handleClearAll"
|
|
|
+ >
|
|
|
+ 清空所有日志
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ v-loading="loading"
|
|
|
+ :data="tableData"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="50" align="center" />
|
|
|
+ <el-table-column prop="id" label="ID" width="80" align="center" />
|
|
|
+ <el-table-column prop="name" label="任务名称" min-width="240" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="connection" label="连接" width="100" align="center" />
|
|
|
+ <el-table-column prop="queue" label="队列" width="100" align="center" />
|
|
|
+ <el-table-column prop="attempts" label="尝试次数" width="100" align="center" />
|
|
|
+ <el-table-column prop="duration" label="执行耗时" width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.duration ? row.duration + 's' : '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="success" label="执行结果" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.success ? 'success' : 'danger'" size="small">
|
|
|
+ {{ row.success ? '成功' : '失败' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="exception" label="异常信息" min-width="200" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span v-if="row.exception" class="exception-text">{{ row.exception }}</span>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="create_time" label="执行时间" width="180" align="center" />
|
|
|
+ <el-table-column label="操作" width="100" align="center" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ link
|
|
|
+ size="small"
|
|
|
+ @click="handleDelete(row)"
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination-wrapper">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.page"
|
|
|
+ v-model:page-size="pagination.list_rows"
|
|
|
+ :total="pagination.total"
|
|
|
+ :page-sizes="[10, 15, 30, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, reactive } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { Search, Refresh } from '@element-plus/icons-vue'
|
|
|
+import { getQueueJobLogList, deleteQueueJobLog, batchDeleteQueueJobLog, clearAllQueueJobLog } from '../api'
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const tableData = ref([])
|
|
|
+const selectedIds = ref([])
|
|
|
+
|
|
|
+const searchForm = reactive({
|
|
|
+ name: '',
|
|
|
+ success: null,
|
|
|
+ date_range: []
|
|
|
+})
|
|
|
+
|
|
|
+const pagination = reactive({
|
|
|
+ page: 1,
|
|
|
+ list_rows: 15,
|
|
|
+ total: 0
|
|
|
+})
|
|
|
+
|
|
|
+const stats = reactive({
|
|
|
+ total: 0,
|
|
|
+ success_count: 0,
|
|
|
+ fail_count: 0,
|
|
|
+ success_rate: 0
|
|
|
+})
|
|
|
+
|
|
|
+// 获取列表
|
|
|
+const getList = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ page: pagination.page,
|
|
|
+ list_rows: pagination.list_rows,
|
|
|
+ name: searchForm.name
|
|
|
+ }
|
|
|
+ if (searchForm.success !== null && searchForm.success !== '') {
|
|
|
+ params.success = searchForm.success
|
|
|
+ }
|
|
|
+ if (searchForm.date_range && searchForm.date_range.length === 2) {
|
|
|
+ params.start_time = searchForm.date_range[0]
|
|
|
+ params.end_time = searchForm.date_range[1]
|
|
|
+ }
|
|
|
+ const res = await getQueueJobLogList(params)
|
|
|
+ if (res.code === 200) {
|
|
|
+ tableData.value = res.page?.data || []
|
|
|
+ pagination.total = res.page?.total || 0
|
|
|
+ // 统计数据
|
|
|
+ const s = res.data || {}
|
|
|
+ stats.total = s.total || 0
|
|
|
+ stats.success_count = s.success_count || 0
|
|
|
+ stats.fail_count = s.fail_count || 0
|
|
|
+ stats.success_rate = stats.total > 0 ? parseFloat((stats.success_count / stats.total * 100).toFixed(4)) : 0
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取日志列表失败:', e)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 搜索
|
|
|
+const handleSearch = () => {
|
|
|
+ pagination.page = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置
|
|
|
+const handleReset = () => {
|
|
|
+ searchForm.name = ''
|
|
|
+ searchForm.success = null
|
|
|
+ searchForm.date_range = []
|
|
|
+ pagination.page = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+// 分页
|
|
|
+const handlePageChange = () => {
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = () => {
|
|
|
+ pagination.page = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+// 选择变化
|
|
|
+const handleSelectionChange = (selection) => {
|
|
|
+ selectedIds.value = selection.map(item => item.id)
|
|
|
+}
|
|
|
+
|
|
|
+// 删除单条
|
|
|
+const handleDelete = (row) => {
|
|
|
+ ElMessageBox.confirm(
|
|
|
+ `确定要删除任务「${row.name}」的这条执行日志吗?`,
|
|
|
+ '删除确认',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ ).then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await deleteQueueJobLog(row.id)
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ getList()
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('删除失败:', e)
|
|
|
+ }
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+// 批量删除
|
|
|
+const handleBatchDelete = () => {
|
|
|
+ if (selectedIds.value.length === 0) return
|
|
|
+ ElMessageBox.confirm(
|
|
|
+ `确定要删除选中的 ${selectedIds.value.length} 条日志吗?`,
|
|
|
+ '批量删除确认',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ ).then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await batchDeleteQueueJobLog(selectedIds.value)
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage.success('批量删除成功')
|
|
|
+ selectedIds.value = []
|
|
|
+ getList()
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('批量删除失败:', e)
|
|
|
+ }
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+// 清空所有
|
|
|
+const handleClearAll = () => {
|
|
|
+ ElMessageBox.confirm(
|
|
|
+ '确定要清空所有队列任务执行日志吗?此操作不可恢复!',
|
|
|
+ '清空确认',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定清空',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ confirmButtonClass: 'el-button--danger'
|
|
|
+ }
|
|
|
+ ).then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await clearAllQueueJobLog()
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage.success('清空成功')
|
|
|
+ pagination.page = 1
|
|
|
+ getList()
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('清空失败:', e)
|
|
|
+ }
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.queue-job-log-container {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-row {
|
|
|
+ margin-bottom: 16px;
|
|
|
+
|
|
|
+ .stat-item {
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-success {
|
|
|
+ color: #67c23a;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-fail {
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.table-card {
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-wrapper {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.exception-text {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+</style>
|