506 lines
13 KiB
Vue
506 lines
13 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="log-management">
|
|||
|
|
<div class="header">
|
|||
|
|
<h2>执行日志</h2>
|
|||
|
|
<el-button type="danger" @click="handleCleanLogs">
|
|||
|
|
<el-icon><Delete /></el-icon>
|
|||
|
|
清理日志
|
|||
|
|
</el-button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 搜索筛选区域 -->
|
|||
|
|
<div class="search-bar">
|
|||
|
|
<div class="search-item">
|
|||
|
|
<span class="search-label">任务名称</span>
|
|||
|
|
<el-input
|
|||
|
|
v-model="searchForm.taskName"
|
|||
|
|
placeholder="请输入任务名称"
|
|||
|
|
clearable
|
|||
|
|
@keyup.enter="handleSearch"
|
|||
|
|
style="width: 200px"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="search-item">
|
|||
|
|
<span class="search-label">任务分组</span>
|
|||
|
|
<el-input
|
|||
|
|
v-model="searchForm.taskGroup"
|
|||
|
|
placeholder="请输入任务分组"
|
|||
|
|
clearable
|
|||
|
|
@keyup.enter="handleSearch"
|
|||
|
|
style="width: 200px"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="search-item">
|
|||
|
|
<span class="search-label">执行状态</span>
|
|||
|
|
<el-select
|
|||
|
|
v-model="searchForm.executeStatus"
|
|||
|
|
placeholder="请选择状态"
|
|||
|
|
clearable
|
|||
|
|
style="width: 150px"
|
|||
|
|
>
|
|||
|
|
<el-option label="成功" :value="1" />
|
|||
|
|
<el-option label="失败" :value="0" />
|
|||
|
|
</el-select>
|
|||
|
|
</div>
|
|||
|
|
<div class="search-actions">
|
|||
|
|
<el-button type="primary" @click="handleSearch">
|
|||
|
|
<el-icon><Search /></el-icon>
|
|||
|
|
搜索
|
|||
|
|
</el-button>
|
|||
|
|
<el-button @click="handleReset">
|
|||
|
|
<el-icon><Refresh /></el-icon>
|
|||
|
|
重置
|
|||
|
|
</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 日志列表 -->
|
|||
|
|
<el-table
|
|||
|
|
:data="logList"
|
|||
|
|
style="width: 100%"
|
|||
|
|
v-loading="loading"
|
|||
|
|
border
|
|||
|
|
stripe
|
|||
|
|
>
|
|||
|
|
<el-table-column prop="taskName" label="任务名称" min-width="150" />
|
|||
|
|
<el-table-column prop="taskGroup" label="任务分组" width="120" />
|
|||
|
|
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
|
|||
|
|
<el-table-column prop="methodName" label="方法名称" width="120" />
|
|||
|
|
<el-table-column label="执行状态" width="100">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-tag :type="row.executeStatus === 1 ? 'success' : 'danger'" size="small">
|
|||
|
|
{{ row.executeStatus === 1 ? '成功' : '失败' }}
|
|||
|
|
</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="executeDuration" label="执行时长" width="100">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
{{ row.executeDuration }}ms
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="startTime" label="开始时间" width="180" />
|
|||
|
|
<el-table-column prop="endTime" label="结束时间" width="180" />
|
|||
|
|
<el-table-column label="操作" width="150" fixed="right">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-button
|
|||
|
|
type="primary"
|
|||
|
|
size="small"
|
|||
|
|
@click="handleViewDetail(row)"
|
|||
|
|
>
|
|||
|
|
查看详情
|
|||
|
|
</el-button>
|
|||
|
|
<el-button
|
|||
|
|
type="danger"
|
|||
|
|
size="small"
|
|||
|
|
@click="handleDelete(row)"
|
|||
|
|
>
|
|||
|
|
删除
|
|||
|
|
</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<!-- 分页 -->
|
|||
|
|
<div class="pagination-wrapper" v-if="total > 0">
|
|||
|
|
<el-pagination
|
|||
|
|
v-model:current-page="pageParam.page"
|
|||
|
|
v-model:page-size="pageParam.size"
|
|||
|
|
:page-sizes="[10, 20, 50, 100]"
|
|||
|
|
:total="total"
|
|||
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|||
|
|
@size-change="handleSizeChange"
|
|||
|
|
@current-change="handlePageChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 详情对话框 -->
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="detailDialogVisible"
|
|||
|
|
title="执行日志详情"
|
|||
|
|
width="700px"
|
|||
|
|
>
|
|||
|
|
<div class="detail-content" v-if="currentLog">
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">任务名称:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.taskName }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">任务分组:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.taskGroup }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">Bean名称:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.beanName }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">方法名称:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.methodName }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item" v-if="currentLog.methodParams">
|
|||
|
|
<span class="detail-label">方法参数:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.methodParams }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">执行状态:</span>
|
|||
|
|
<el-tag :type="currentLog.executeStatus === 1 ? 'success' : 'danger'" size="small">
|
|||
|
|
{{ currentLog.executeStatus === 1 ? '成功' : '失败' }}
|
|||
|
|
</el-tag>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">执行时长:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.executeDuration }}ms</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">开始时间:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.startTime }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item">
|
|||
|
|
<span class="detail-label">结束时间:</span>
|
|||
|
|
<span class="detail-value">{{ currentLog.endTime }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item" v-if="currentLog.executeMessage">
|
|||
|
|
<span class="detail-label">执行结果:</span>
|
|||
|
|
<div class="detail-message">{{ currentLog.executeMessage }}</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-item" v-if="currentLog.exceptionInfo">
|
|||
|
|
<span class="detail-label">异常信息:</span>
|
|||
|
|
<div class="detail-exception">{{ currentLog.exceptionInfo }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<template #footer>
|
|||
|
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-dialog>
|
|||
|
|
|
|||
|
|
<!-- 清理日志对话框 -->
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="cleanDialogVisible"
|
|||
|
|
title="清理日志"
|
|||
|
|
width="400px"
|
|||
|
|
>
|
|||
|
|
<div class="clean-dialog-content">
|
|||
|
|
<el-alert
|
|||
|
|
title="清理操作不可恢复,请谨慎操作!"
|
|||
|
|
type="warning"
|
|||
|
|
:closable="false"
|
|||
|
|
show-icon
|
|||
|
|
/>
|
|||
|
|
<div class="clean-item">
|
|||
|
|
<span class="clean-label">保留天数:</span>
|
|||
|
|
<el-input-number
|
|||
|
|
v-model="cleanDays"
|
|||
|
|
:min="1"
|
|||
|
|
:max="365"
|
|||
|
|
controls-position="right"
|
|||
|
|
/>
|
|||
|
|
<span class="clean-tip">天</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="clean-desc">
|
|||
|
|
将删除 {{ cleanDays }} 天前的所有执行日志
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<template #footer>
|
|||
|
|
<el-button @click="cleanDialogVisible = false">取消</el-button>
|
|||
|
|
<el-button
|
|||
|
|
type="danger"
|
|||
|
|
@click="handleConfirmClean"
|
|||
|
|
:loading="submitting"
|
|||
|
|
>
|
|||
|
|
确定清理
|
|||
|
|
</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-dialog>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, reactive, onMounted } from 'vue';
|
|||
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|||
|
|
import { Delete, Search, Refresh } from '@element-plus/icons-vue';
|
|||
|
|
import { crontabApi } from '@/apis/crontab';
|
|||
|
|
import type { CrontabLog, PageParam } from '@/types';
|
|||
|
|
|
|||
|
|
// 数据状态
|
|||
|
|
const loading = ref(false);
|
|||
|
|
const submitting = ref(false);
|
|||
|
|
const logList = ref<CrontabLog[]>([]);
|
|||
|
|
const total = ref(0);
|
|||
|
|
const currentLog = ref<CrontabLog | null>(null);
|
|||
|
|
|
|||
|
|
// 搜索表单
|
|||
|
|
const searchForm = reactive({
|
|||
|
|
taskName: '',
|
|||
|
|
taskGroup: '',
|
|||
|
|
executeStatus: undefined as number | undefined
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 分页参数
|
|||
|
|
const pageParam = reactive<PageParam>({
|
|||
|
|
page: 1,
|
|||
|
|
size: 20
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 对话框状态
|
|||
|
|
const detailDialogVisible = ref(false);
|
|||
|
|
const cleanDialogVisible = ref(false);
|
|||
|
|
const cleanDays = ref(30);
|
|||
|
|
|
|||
|
|
// 加载日志列表
|
|||
|
|
const loadLogList = async () => {
|
|||
|
|
loading.value = true;
|
|||
|
|
try {
|
|||
|
|
const filter: Partial<CrontabLog> = {};
|
|||
|
|
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
|||
|
|
if (searchForm.taskGroup) filter.taskGroup = searchForm.taskGroup;
|
|||
|
|
if (searchForm.executeStatus !== undefined) filter.executeStatus = searchForm.executeStatus;
|
|||
|
|
|
|||
|
|
const result = await crontabApi.getLogPage(filter, pageParam);
|
|||
|
|
if (result.success && result.dataList) {
|
|||
|
|
logList.value = result.dataList;
|
|||
|
|
total.value = result.pageParam?.totalElements || 0;
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(result.message || '加载日志列表失败');
|
|||
|
|
logList.value = [];
|
|||
|
|
total.value = 0;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载日志列表失败:', error);
|
|||
|
|
ElMessage.error('加载日志列表失败');
|
|||
|
|
logList.value = [];
|
|||
|
|
total.value = 0;
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 搜索
|
|||
|
|
const handleSearch = () => {
|
|||
|
|
pageParam.page = 1;
|
|||
|
|
loadLogList();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 重置搜索
|
|||
|
|
const handleReset = () => {
|
|||
|
|
searchForm.taskName = '';
|
|||
|
|
searchForm.taskGroup = '';
|
|||
|
|
searchForm.executeStatus = undefined;
|
|||
|
|
pageParam.page = 1;
|
|||
|
|
loadLogList();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 分页变化
|
|||
|
|
const handlePageChange = (page: number) => {
|
|||
|
|
pageParam.page = page;
|
|||
|
|
loadLogList();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSizeChange = (size: number) => {
|
|||
|
|
pageParam.size = size;
|
|||
|
|
pageParam.page = 1;
|
|||
|
|
loadLogList();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 查看详情
|
|||
|
|
const handleViewDetail = async (row: CrontabLog) => {
|
|||
|
|
try {
|
|||
|
|
const result = await crontabApi.getLogById(row.id!);
|
|||
|
|
if (result.success && result.data) {
|
|||
|
|
currentLog.value = result.data;
|
|||
|
|
detailDialogVisible.value = true;
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(result.message || '获取详情失败');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取日志详情失败:', error);
|
|||
|
|
ElMessage.error('获取日志详情失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 删除日志
|
|||
|
|
const handleDelete = async (row: CrontabLog) => {
|
|||
|
|
try {
|
|||
|
|
await ElMessageBox.confirm(
|
|||
|
|
'确定要删除这条日志吗?',
|
|||
|
|
'删除确认',
|
|||
|
|
{
|
|||
|
|
confirmButtonText: '确定',
|
|||
|
|
cancelButtonText: '取消',
|
|||
|
|
type: 'warning'
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const result = await crontabApi.deleteLog(row);
|
|||
|
|
if (result.success) {
|
|||
|
|
ElMessage.success('删除成功');
|
|||
|
|
loadLogList();
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(result.message || '删除失败');
|
|||
|
|
}
|
|||
|
|
} catch (error: any) {
|
|||
|
|
if (error !== 'cancel') {
|
|||
|
|
console.error('删除日志失败:', error);
|
|||
|
|
ElMessage.error('删除日志失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 清理日志
|
|||
|
|
const handleCleanLogs = () => {
|
|||
|
|
cleanDialogVisible.value = true;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 确认清理
|
|||
|
|
const handleConfirmClean = async () => {
|
|||
|
|
submitting.value = true;
|
|||
|
|
try {
|
|||
|
|
const result = await crontabApi.cleanLogs(cleanDays.value);
|
|||
|
|
if (result.success) {
|
|||
|
|
ElMessage.success(`已清理 ${result.data || 0} 条日志`);
|
|||
|
|
cleanDialogVisible.value = false;
|
|||
|
|
loadLogList();
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(result.message || '清理失败');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('清理日志失败:', error);
|
|||
|
|
ElMessage.error('清理日志失败');
|
|||
|
|
} finally {
|
|||
|
|
submitting.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 初始化
|
|||
|
|
onMounted(() => {
|
|||
|
|
loadLogList();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.log-management {
|
|||
|
|
padding: 20px;
|
|||
|
|
background-color: #fff;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
|
|||
|
|
.header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
|
|||
|
|
h2 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 24px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #141F38;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search-bar {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 16px;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
padding: 15px;
|
|||
|
|
background-color: #f8f9fa;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
|
|||
|
|
.search-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
|
|||
|
|
.search-label {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #606266;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-left: auto;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pagination-wrapper {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-content {
|
|||
|
|
.detail-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
|
|||
|
|
.detail-label {
|
|||
|
|
min-width: 100px;
|
|||
|
|
color: #606266;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-value {
|
|||
|
|
flex: 1;
|
|||
|
|
color: #303133;
|
|||
|
|
word-break: break-all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-message,
|
|||
|
|
.detail-exception {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 12px;
|
|||
|
|
background-color: #f5f7fa;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-family: 'Courier New', monospace;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #303133;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
word-break: break-all;
|
|||
|
|
max-height: 300px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-exception {
|
|||
|
|
background-color: #fef0f0;
|
|||
|
|
color: #f56c6c;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.clean-dialog-content {
|
|||
|
|
.clean-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
|
|||
|
|
.clean-label {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #606266;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.clean-tip {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #909399;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.clean-desc {
|
|||
|
|
padding: 12px;
|
|||
|
|
background-color: #f5f7fa;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #606266;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|