This commit is contained in:
yuhawu 2025-07-22 10:00:01 +08:00
parent a821bef69f
commit 082abbb60a
4 changed files with 359 additions and 56 deletions

View File

@ -8,6 +8,18 @@ const taskApi = {
// 停止指定任务 // 停止指定任务
stopTask: (taskId) => instance.get(`/task/stopTask?taskId=${taskId}`), stopTask: (taskId) => instance.get(`/task/stopTask?taskId=${taskId}`),
// 获取任务日志列表
getLogsList: (id) => instance.get(`/task/logsList?id=${id}`),
// 获取任务日志消息
getLogsMsg: (id) => instance.get(`/task/logsMsg?id=${id}`),
// 获取任务详细日志列表
getLogsDetailList: (taskId, shopId) => instance.get(`/task/logsDetailList/${taskId}/${shopId}`),
// 下载日志文件
downloadLogs: (fileName) => instance.get(`/task/downloadLogs/${fileName}`, { responseType: 'blob' }),
}; };
// 导出模块 // 导出模块

View File

@ -13,15 +13,8 @@
<div v-else-if="taskList.length === 0" class="empty-content"> <div v-else-if="taskList.length === 0" class="empty-content">
暂无运行中的任务 暂无运行中的任务
</div> </div>
<el-table <el-table v-else :data="taskList" border style="width: 100%" height="500" max-height="500"
v-else :header-cell-style="{ backgroundColor: '#f5f7fa', color: '#606266', textAlign: 'center' }">
:data="taskList"
border
style="width: 100%"
height="500"
max-height="500"
:header-cell-style="{ backgroundColor: '#f5f7fa', color: '#606266', textAlign: 'center' }"
>
<el-table-column prop="id" label="任务ID" width="180" align="center" /> <el-table-column prop="id" label="任务ID" width="180" align="center" />
<el-table-column prop="fileName" label="文件名称" align="center" /> <el-table-column prop="fileName" label="文件名称" align="center" />
<el-table-column prop="shopNames" label="店铺名称" align="center" /> <el-table-column prop="shopNames" label="店铺名称" align="center" />
@ -34,35 +27,39 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" align="center" /> <el-table-column prop="createTime" label="创建时间" width="160" align="center" />
<el-table-column label="操作" width="100" fixed="right" align="center"> <el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<el-button <div class="operation-buttons">
type="danger" <el-tooltip content="查看日志" placement="top" :hide-after="1500">
size="small" <el-button type="primary" size="small" @click="handleViewLogs(scope.row.id)" circle>
@click="handleStopTask(scope.row.id)" <el-icon><Document /></el-icon>
:loading="stoppingTasks.includes(scope.row.id)" </el-button>
:disabled="scope.row.taskStatus !== '1'" </el-tooltip>
> <el-tooltip content="停止任务" placement="top" :hide-after="1500">
停止任务 <el-button
</el-button> type="danger"
size="small"
@click="handleStopTask(scope.row.id)"
:loading="stoppingTasks.includes(scope.row.id)"
:disabled="scope.row.taskStatus !== '1'"
circle
>
<el-icon><VideoPause /></el-icon>
</el-button>
</el-tooltip>
</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
<!-- 分页控件独立于卡片外部 --> <!-- 分页控件独立于卡片外部 -->
<div class="pagination-container"> <div class="pagination-container">
<el-pagination <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
v-model:current-page="currentPage" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange"
v-model:page-size="pageSize" @current-change="handleCurrentChange" />
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div> </div>
<el-dialog v-model="detailVisible" title="任务详情" width="500px"> <el-dialog v-model="detailVisible" title="任务详情" width="500px">
<el-descriptions :column="1" border> <el-descriptions :column="1" border>
<el-descriptions-item label="任务ID">{{ currentTask.id }}</el-descriptions-item> <el-descriptions-item label="任务ID">{{ currentTask.id }}</el-descriptions-item>
@ -71,16 +68,76 @@
<el-descriptions-item label="数据总数">{{ currentTask.dataNum }}</el-descriptions-item> <el-descriptions-item label="数据总数">{{ currentTask.dataNum }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentTask.createTime }}</el-descriptions-item> <el-descriptions-item label="创建时间">{{ currentTask.createTime }}</el-descriptions-item>
<el-descriptions-item label="执行进度"> <el-descriptions-item label="执行进度">
<el-progress <el-progress :percentage="calculateProgress(currentTask)"
:percentage="calculateProgress(currentTask)" :status="currentTask.taskStatus === '1' ? 'success' : 'exception'" />
:status="currentTask.taskStatus === '1' ? 'success' : 'exception'"
/>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="执行消息"> <el-descriptions-item label="执行消息">
<div class="task-msg">{{ currentTask.msg }}</div> <div class="task-msg">{{ currentTask.msg }}</div>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
<!-- 任务日志弹窗 -->
<el-dialog v-model="logsVisible" title="任务日志" width="900px" :close-on-click-modal="false">
<div class="logs-container">
<!-- 日志消息区域 -->
<div class="logs-message-section">
<div class="logs-message-content">
<div v-if="logsLoading" class="loading-text">加载中...</div>
<div v-else class="logs-text">
<div v-for="(line, index) in formatLogMessage(logsMessage)" :key="index" class="log-line">
{{ line }}
</div>
</div>
</div>
<div class="refresh-btn-container">
<el-button type="primary" size="small" @click="refreshLogs" :loading="logsLoading" icon="Refresh">
刷新
</el-button>
</div>
</div>
<!-- 店铺日志列表 -->
<div class="logs-table-section">
<el-table :data="logsList" border style="width: 100%"
:header-cell-style="{ backgroundColor: '#f5f7fa', color: '#606266', textAlign: 'center' }">
<el-table-column prop="shopName" label="店铺名称" align="center" />
<el-table-column prop="progress" label="进度" width="100" align="center">
<template #default="scope">
{{ scope.row.progress }}%
</template>
</el-table-column>
<el-table-column label="创建时间/修改时间" width="300" align="center">
<template #default="scope">
{{ scope.row.createTime }} / {{ scope.row.updateTime }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<div class="operation-buttons">
<el-tooltip content="查看详情" placement="top" :hide-after="1500">
<el-button type="primary" size="small" @click="handleViewDetail(scope.row)" circle>
<el-icon><View /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="下载日志" placement="top" :hide-after="1500">
<el-button type="success" size="small" @click="handleDownload(scope.row)" circle>
<el-icon><Download /></el-icon>
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="logsVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
@ -88,6 +145,7 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { taskApi } from '@/api/modules/task' import { taskApi } from '@/api/modules/task'
import { Document, VideoPause, View, Download, Refresh } from '@element-plus/icons-vue'
// //
const taskList = ref([]) const taskList = ref([])
@ -101,16 +159,23 @@ const total = ref(0)
const currentPage = ref(1) // 1 const currentPage = ref(1) // 1
const pageSize = ref(10) // const pageSize = ref(10) //
//
const logsVisible = ref(false)
const logsLoading = ref(false)
const logsList = ref([])
const logsMessage = ref('')
const currentTaskId = ref('')
// //
const fetchTaskList = async () => { const fetchTaskList = async () => {
loading.value = true loading.value = true
try { try {
// currentPagepageSize(0) // currentPagepageSize(0)
const pageIndex = currentPage.value - 1 const pageIndex = currentPage.value - 1
const res = await taskApi.getRunningTasks(pageIndex, pageSize.value) const res = await taskApi.getRunningTasks(pageIndex, pageSize.value)
console.log('API响应数据:', res) console.log('API响应数据:', res)
if (res.code === 200) { if (res.code === 200) {
// //
if (res.data) { if (res.data) {
@ -175,7 +240,7 @@ const refreshTasks = () => {
// //
const handleStopTask = async (taskId) => { const handleStopTask = async (taskId) => {
console.log("taskId",taskId) console.log("taskId", taskId)
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`确定要停止任务吗?`, `确定要停止任务吗?`,
@ -186,10 +251,10 @@ const handleStopTask = async (taskId) => {
type: 'warning', type: 'warning',
} }
) )
stoppingTasks.value.push(taskId) stoppingTasks.value.push(taskId)
const res = await taskApi.stopTask(taskId) const res = await taskApi.stopTask(taskId)
if (res.code === 200) { if (res.code === 200) {
ElMessage.success('任务已成功停止') ElMessage.success('任务已成功停止')
// //
@ -239,27 +304,151 @@ const showTaskDetail = (task) => {
// //
const calculateProgress = (task) => { const calculateProgress = (task) => {
if (!task || !task.msg) return 0 if (!task || !task.msg) return 0
try { try {
// //
const totalMatch = task.msg.match(/总执行数据:(\d+)条/) const totalMatch = task.msg.match(/总执行数据:(\d+)条/)
const executedMatch = task.msg.match(/已执行条数:(\d+)条/) const executedMatch = task.msg.match(/已执行条数:(\d+)条/)
if (totalMatch && executedMatch) { if (totalMatch && executedMatch) {
const total = parseInt(totalMatch[1]) const total = parseInt(totalMatch[1])
const executed = parseInt(executedMatch[1]) const executed = parseInt(executedMatch[1])
if (total > 0) { if (total > 0) {
return Math.floor((executed / total) * 100) return Math.floor((executed / total) * 100)
} }
} }
return 0 return 0
} catch (e) { } catch (e) {
return 0 return 0
} }
} }
//
const handleViewLogs = async (taskId) => {
currentTaskId.value = taskId
logsVisible.value = true
await fetchLogs()
}
//
const fetchLogs = async () => {
logsLoading.value = true
try {
//
const [logsListRes, logsMsgRes] = await Promise.all([
taskApi.getLogsList(currentTaskId.value),
taskApi.getLogsMsg(currentTaskId.value)
])
console.log('日志列表响应:', logsListRes)
console.log('日志消息响应:', logsMsgRes)
//
if (logsListRes.code === 200) {
if (Array.isArray(logsListRes.data)) {
logsList.value = logsListRes.data
} else if (logsListRes.data && Array.isArray(logsListRes.data.data)) {
logsList.value = logsListRes.data.data
} else {
logsList.value = []
}
} else {
logsList.value = []
ElMessage.error('获取日志列表失败: ' + (logsListRes.message || '未知错误'))
}
//
if (logsMsgRes.code === 200) {
// 使
if (typeof logsMsgRes.data === 'string') {
logsMessage.value = logsMsgRes.data
}
//
else if (logsMsgRes.data && typeof logsMsgRes.data === 'object') {
logsMessage.value = logsMsgRes.data.message || logsMsgRes.data.msg || JSON.stringify(logsMsgRes.data)
} else {
logsMessage.value = '暂无日志消息'
}
} else {
logsMessage.value = '获取日志消息失败'
ElMessage.error('获取日志消息失败: ' + (logsMsgRes.message || '未知错误'))
}
} catch (error) {
console.error('获取日志数据出错:', error)
ElMessage.error('获取日志数据失败: ' + (error.message || '未知错误'))
logsList.value = []
logsMessage.value = '获取日志数据失败'
} finally {
logsLoading.value = false
}
}
//
const refreshLogs = () => {
fetchLogs()
}
//
const handleViewDetail = async (row) => {
try {
const res = await taskApi.getLogsDetailList(row.taskId, row.shopId)
console.log('详细日志响应:', res)
if (res.code === 200) {
//
ElMessage.success('查看详细日志功能待实现')
} else {
ElMessage.error('获取详细日志失败: ' + (res.message || '未知错误'))
}
} catch (error) {
console.error('获取详细日志出错:', error)
ElMessage.error('获取详细日志失败: ' + (error.message || '未知错误'))
}
}
//
const handleDownload = async (row) => {
try {
// taskId + shopId + ".txt"
const fileName = `${row.taskId}${row.shopId}.txt`
const response = await taskApi.downloadLogs(fileName)
//
const blob = new Blob([response.data])
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('文件下载成功')
} catch (error) {
console.error('下载日志文件出错:', error)
ElMessage.error('下载日志文件失败: ' + (error.message || '未知错误'))
}
}
//
const formatLogMessage = (message) => {
if (!message) return ['暂无日志消息']
//
const lines = message.split(/\r?\n|\r/).filter(line => line.trim() !== '')
if (lines.length === 0) {
return ['暂无日志消息']
}
return lines
}
// //
onMounted(() => { onMounted(() => {
fetchTaskList() fetchTaskList()
@ -270,18 +459,22 @@ onMounted(() => {
.task-list-container { .task-list-container {
padding: 20px; padding: 20px;
} }
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.empty-content, .loading-content {
.empty-content,
.loading-content {
min-height: 300px; min-height: 300px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: #909399; color: #909399;
} }
.task-msg { .task-msg {
white-space: pre-line; white-space: pre-line;
max-height: 150px; max-height: 150px;
@ -290,6 +483,7 @@ onMounted(() => {
background-color: #f8f8f8; background-color: #f8f8f8;
border-radius: 4px; border-radius: 4px;
} }
.pagination-container { .pagination-container {
margin-top: 15px; margin-top: 15px;
background-color: #fff; background-color: #fff;
@ -300,6 +494,21 @@ onMounted(() => {
justify-content: flex-end; justify-content: flex-end;
} }
.operation-buttons {
display: flex;
justify-content: center;
gap: 10px;
}
.operation-buttons .el-button {
margin: 0;
transition: transform 0.2s;
}
.operation-buttons .el-button:hover {
transform: scale(1.1);
}
/* 表格样式 */ /* 表格样式 */
:deep(.el-table) { :deep(.el-table) {
border-radius: 4px; border-radius: 4px;
@ -341,4 +550,92 @@ onMounted(() => {
padding: 8px; padding: 8px;
height: 50px; height: 50px;
} }
</style>
/* 日志弹窗样式 */
.logs-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.logs-message-section {
background-color: #f5f7fa;
border-radius: 8px;
padding: 16px;
position: relative;
}
.logs-message-content {
margin-bottom: 10px;
}
.logs-text {
color: #606266;
line-height: 1.6;
white-space: pre-line;
font-size: 14px;
min-height: 120px;
padding: 12px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.log-line {
margin-bottom: 4px;
color: #606266;
font-size: 14px;
}
.loading-text {
color: #909399;
text-align: center;
padding: 40px;
font-size: 14px;
}
.refresh-btn-container {
display: flex;
justify-content: flex-end;
}
.logs-table-section {
margin-top: 10px;
}
/* 日志弹窗表格样式 */
.logs-table-section :deep(.el-table) {
border-radius: 8px;
}
.logs-table-section :deep(.el-table th) {
background-color: #f5f7fa !important;
color: #606266;
font-weight: 500;
}
.logs-table-section :deep(.el-button) {
margin-right: 4px;
}
.logs-table-section :deep(.el-button:last-child) {
margin-right: 0;
}
/* 弹窗底部样式 */
.dialog-footer {
text-align: right;
}
/* 日志弹窗标题样式 */
:deep(.el-dialog__header) {
padding: 20px 20px 10px;
border-bottom: 1px solid #e4e7ed;
}
:deep(.el-dialog__title) {
font-size: 18px;
font-weight: 500;
color: #303133;
}
</style>

View File

@ -368,7 +368,7 @@ const submitCardSecretRequest = async () => {
}; };
const res = await cardsApi.createCardSecret(cardData) const res = await cardsApi.createCardSecret(cardData)
// createCardSecret使res // createCardSecret使res
console.log("res",res) console.log("res",res)
if (res.code === 200) { if (res.code === 200) {
cardSecretValue.value = res.data cardSecretValue.value = res.data
@ -380,7 +380,10 @@ const submitCardSecretRequest = async () => {
} }
} catch (error) { } catch (error) {
console.error('获取卡密失败:', error) console.error('获取卡密失败:', error)
ElMessage.error('获取卡密失败: ' + (error.message || '未知错误')) // if(error.status === 400){
// ElMessage.error(': ' + (error.message || ''))
// }
ElMessage.error('获取卡密失败: ' + (error.response.data.message || '未知错误'))
} }
} }

View File

@ -1,9 +0,0 @@
<template>
编辑/新增
</template>
<script>
</script>
<style>
</style>