iFileProxyAdmin/src/views/TaskManagement.vue

1037 lines
No EOL
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="task-management">
<el-card class="task-card">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="title">任务列表</span>
<el-tag class="task-count" type="info" effect="plain">
{{ filteredTasks.length }}
</el-tag>
</div>
<div class="header-right">
<el-input
v-model="searchQuery"
placeholder="搜索任务..."
class="search-input"
clearable
@clear="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleAddTask">
<el-icon><Plus /></el-icon>新建任务
</el-button>
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="filteredTasks"
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="tid" label="任务UUID" width="350" />
<el-table-column prop="client_ip" label="客户端IP" width="140">
<template #default="{ row }">
<el-link
type="primary"
:href="`https://ip138.com/iplookup.php?ip=${row.client_ip}`"
target="_blank"
:underline="false">
{{ row.client_ip }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="add_time" label="添加时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.add_time) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="130">
<template #default="scope">
<el-tag
:type="getStatusType(scope.row.status, scope.row.queue_position)"
effect="light"
:class="['status-tag', `status-${scope.row.status}`]">
{{ getStatusLabel(scope.row.status) }}
{{ scope.row.status === 6 ?
(scope.row.queue_position === -1 ? ' 错误' : '#' + scope.row.queue_position)
: ''
}}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
:width="isMobile ? '70' : '420'"
fixed="right"
class-name="operation-column">
<template #default="scope">
<el-button
link
type="primary"
@click="handleViewDetails(scope.row)">
<el-icon><InfoFilled /></el-icon>详情
</el-button>
<el-button
v-if="scope.row.status === 1"
link
type="primary"
@click="handleViewProcess(scope.row)">
<el-icon><Monitor /></el-icon>进程信息
</el-button>
<el-button
v-if="scope.row.status === 6"
link
type="success"
@click="handlePrioritize(scope.row)">
<el-icon><Top /></el-icon>插至队首
</el-button>
<el-button
v-if="scope.row.status === 6"
link
type="success"
@click="handleExecuteNow(scope.row)">
<el-icon><VideoPlay /></el-icon>立即执行
</el-button>
<el-button
v-if="[2, 7].includes(scope.row.status)"
link
type="success"
@click="handleRetry(scope.row)">
<el-icon><RefreshRight /></el-icon>重试
</el-button>
<el-button
link
:type="scope.row.status === 1 ? 'warning' : 'danger'"
@click="scope.row.status === 1 ? handleStop(scope.row) : handleDelete(scope.row)">
<el-icon>
<component :is="scope.row.status === 1 ? 'VideoPause' : 'Delete'" />
</el-icon>
{{ scope.row.status === 1 ? '停止' : '删除' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<el-dialog
v-model="processDialogVisible"
title="进程信息"
width="700px"
destroy-on-close>
<div v-if="currentProcess" class="process-info">
<el-descriptions :column="2" border>
<el-descriptions-item label="进程ID">
{{ currentProcess.processId }}
</el-descriptions-item>
<el-descriptions-item label="进程名称">
{{ currentProcess.processName }}
</el-descriptions-item>
<el-descriptions-item label="启动时间">
{{ formatDateTime(currentProcess.startTime) }}
</el-descriptions-item>
<el-descriptions-item label="运行时长">
{{ formatDuration(currentProcess.runningTime) }}
</el-descriptions-item>
<el-descriptions-item label="线程数">
{{ currentProcess.threadCount }}
</el-descriptions-item>
<el-descriptions-item label="优先级">
{{ currentProcess.priorityClass }}
</el-descriptions-item>
<el-descriptions-item label="工作目录" :span="2">
{{ currentProcess.workingDirectory }}
</el-descriptions-item>
<el-descriptions-item label="命令行" :span="2">
<el-input
type="textarea"
:rows="2"
v-model="currentProcess.commandLine"
readonly
/>
</el-descriptions-item>
</el-descriptions>
<div class="threads-title">线程列表</div>
<el-table :data="currentProcess.threads" size="small" max-height="200">
<el-table-column prop="threadId" label="线程ID" width="100" />
<el-table-column prop="threadState" label="状态" width="120">
<template #default="scope">
<el-tag size="small" :type="getThreadStateType(scope.row.threadState)">
{{ getThreadStateLabel(scope.row.threadState) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" />
</el-table>
</div>
</el-dialog>
<el-dialog
v-model="detailsDialogVisible"
title="任务详细信息"
width="700px"
destroy-on-close>
<div v-if="currentTaskDetails" class="task-details">
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID" :span="2">
{{ currentTaskDetails.tid }}
</el-descriptions-item>
<el-descriptions-item label="文件名称">
{{ currentTaskDetails.file_name }}
</el-descriptions-item>
<el-descriptions-item label="文件大小">
{{ formatFileSize(currentTaskDetails.size) }}
</el-descriptions-item>
<el-descriptions-item label="客户端IP">
{{ currentTaskDetails.client_ip }}
</el-descriptions-item>
<el-descriptions-item label="队列位置">
{{ currentTaskDetails.queue_position || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(currentTaskDetails.add_time) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(currentTaskDetails.update_time) }}
</el-descriptions-item>
<el-descriptions-item label="文件Hash" :span="2">
{{ currentTaskDetails.hash }}
</el-descriptions-item>
<el-descriptions-item label="标签" :span="2">
<el-tag
v-if="currentTaskDetails.tag"
:type="getTagType(currentTaskDetails.tag)"
effect="light">
{{ currentTaskDetails.tag }}
</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="下载地址" :span="2">
<el-input
type="textarea"
:rows="2"
v-model="currentTaskDetails.url"
readonly
/>
</el-descriptions-item>
</el-descriptions>
<div class="details-actions">
<el-button type="primary" @click="copyUrl">
<el-icon><DocumentCopy /></el-icon>复制下载地址
</el-button>
<el-button type="primary" @click="downloadFile" :disabled="!currentTaskDetails.url">
<el-icon><Download /></el-icon>下载文件
</el-button>
<el-button
type="success"
@click="downloadProxiedFile"
:disabled="!canDownloadProxied">
<el-icon><Connection /></el-icon>下载文件(已代理)
</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { TaskAPI } from '../api/task'
import {
Plus, Delete, Monitor, Search,
InfoFilled, DocumentCopy, Download,
VideoPlay, VideoPause, RefreshRight,
Top, Connection
} from '@element-plus/icons-vue'
import { API_BASE_URL } from '../config/api.config'
const loading = ref(false)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const tasks = ref([])
const searchQuery = ref('')
// 添加过滤后的任务列表计算属性
const filteredTasks = computed(() => {
if (!searchQuery.value) return tasks.value
const query = searchQuery.value.toLowerCase()
return tasks.value.filter(task => {
return (
task.tid?.toLowerCase().includes(query) ||
task.file_name?.toLowerCase().includes(query) ||
task.client_ip?.toLowerCase().includes(query) ||
formatDateTime(task.add_time)?.toLowerCase().includes(query)
)
})
})
// 获取任务列表
const fetchTasks = async () => {
loading.value = true
try {
console.log('Fetching tasks with params:', {
page: currentPage.value,
pageSize: pageSize.value
})
const response = await TaskAPI.getTaskList({
page: currentPage.value,
pageSize: pageSize.value
})
console.log('Raw response:', response)
if (response.data) {
tasks.value = response.data.data || []
total.value = response.data.total || 0
}
} catch (error) {
console.error('Error details:', {
message: error.message,
config: error.config,
response: error.response,
request: error.request
})
ElMessage.error('获取任务列表失败')
} finally {
loading.value = false
}
}
// 处理搜索
const handleSearch = () => {
// 如果需要额外的搜索处理逻辑可以在这里添加
console.log('Searching for:', searchQuery.value)
}
// 处理页码变化
const handlePageChange = (page) => {
currentPage.value = page
fetchTasks()
}
// 处理每页数量变化
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchTasks()
}
// 初始化加载
onMounted(() => {
fetchTasks()
})
// 状态映射表
const statusMap = {
0: {
label: '未初始化',
type: 'info'
},
1: {
label: '正在进行',
type: 'primary'
},
2: {
label: '执行错误',
type: 'danger'
},
3: {
label: '已完成',
type: 'success'
},
4: {
label: '已缓存',
type: 'success'
},
5: {
label: '已清理',
type: 'info'
},
6: {
label: '队列',
type: 'warning'
},
7: {
label: '已取消',
type: 'info'
}
}
// 修改状态类型获取函数,添加队列位置参数
const getStatusType = (status, queuePosition) => {
// 如果是队列状态且位置为-1返回错误类型
if (status === 6 && queuePosition === -1) {
return 'danger'
}
return statusMap[status]?.type || 'info'
}
const getStatusLabel = (status) => {
return statusMap[status]?.label || `未知状态(${status})`
}
const handleAddTask = () => {
ElMessageBox.prompt('请输入下载地址', '新建任务', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入文件下载地址',
inputPattern: /^https?:\/\/.+/,
inputErrorMessage: '请输入有效的下载地址',
inputType: 'textarea',
inputRows: 3,
customClass: 'add-task-dialog'
}).then(async ({ value }) => {
try {
const response = await TaskAPI.addOfflineTask(value)
if (response.retcode === 0) {
ElMessage.success('任务已添加')
fetchTasks() // 刷新任务列表
}
} catch (error) {
console.error('添加任务失败:', error)
}
}).catch(() => {
// 用户取消输入,不做处理
})
}
const handleEdit = (row) => {
// 实现编辑任务的逻辑
}
const handleDelete = (row) => {
ElMessageBox.confirm(
'确定要删除该任务吗?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const response = await TaskAPI.deleteTask(row.tid)
// 如果成功删除,刷新任务列表
ElMessage.success('删除成功')
fetchTasks()
} catch (error) {
// 错误已经在响应拦截器中处理
console.error('删除任务失败:', error)
}
})
}
const processDialogVisible = ref(false)
const currentProcess = ref(null)
const handleViewProcess = async (task) => {
try {
const response = await TaskAPI.getProcessInfo(task.tid)
if (response.retcode === 0) {
currentProcess.value = response.data
processDialogVisible.value = true
}
} catch (error) {
console.error('获取进程信息失败:', error)
}
}
// 格式化时间
const formatDateTime = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString()
}
// 格式化运行时长
const formatDuration = (seconds) => {
if (!seconds) return '-'
const minutes = Math.floor(seconds / 60)
const remainingSeconds = (seconds % 60).toFixed(1)
if (minutes === 0) {
return `${remainingSeconds}`
}
return `${minutes}${remainingSeconds}`
}
// 线程状态映射
const threadStateMap = {
0: { label: '已初始化', type: 'info' }, // Initialized
1: { label: '就绪', type: 'warning' }, // Ready
2: { label: '运行中', type: 'success' }, // Running
3: { label: '待机', type: 'info' }, // Standby
4: { label: '已终止', type: 'danger' }, // Terminated
5: { label: '等待中', type: 'info' }, // Wait
6: { label: '过渡中', type: 'warning' }, // Transition
7: { label: '未知', type: 'info' } // Unknown
}
const getThreadStateType = (state) => {
return threadStateMap[state]?.type || 'info'
}
const getThreadStateLabel = (state) => {
return threadStateMap[state]?.label || `未知(${state})`
}
const detailsDialogVisible = ref(false)
const currentTaskDetails = ref(null)
const handleViewDetails = async (task) => {
try {
const response = await TaskAPI.getTaskDetails(task.tid)
if (response.retcode === 0) {
currentTaskDetails.value = response.data
detailsDialogVisible.value = true
}
} catch (error) {
console.error('获取任务详情失败:', error)
}
}
const formatFileSize = (bytes) => {
if (!bytes) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let i = 0
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024
i++
}
return `${bytes.toFixed(2)} ${units[i]}`
}
const copyUrl = () => {
if (currentTaskDetails.value?.url) {
navigator.clipboard.writeText(currentTaskDetails.value.url)
.then(() => {
ElMessage.success('下载地址已复制到剪贴板')
})
.catch(() => {
ElMessage.error('复制失败')
})
}
}
const downloadFile = () => {
if (currentTaskDetails.value?.url) {
window.open(currentTaskDetails.value.url, '_blank')
}
}
const handleStop = (row) => {
ElMessageBox.confirm(
'确定要停止该任务吗?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const response = await TaskAPI.killTask(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已停止')
fetchTasks() // 刷新任务列表
}
} catch (error) {
console.error('停止任务失败:', error)
}
})
}
const handlePrioritize = (row) => {
ElMessageBox.confirm(
'确定要将该任务移至队首吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
).then(async () => {
try {
const response = await TaskAPI.prioritizeTask(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已移至队首')
fetchTasks()
}
} catch (error) {
console.error('移动任务失败:', error)
}
})
}
const handleExecuteNow = (row) => {
ElMessageBox.confirm(
'确定要立即执行该任务吗?这将跳过队列直接开始执行',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const response = await TaskAPI.executeImmediately(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已开始执行')
fetchTasks()
}
} catch (error) {
console.error('执行任务失败:', error)
}
})
}
const handleRetry = (row) => {
ElMessageBox.confirm(
'确定要重试该任务吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
).then(async () => {
try {
const response = await TaskAPI.retryTask(row.tid)
if (response.retcode === 0) {
ElMessage.success('任务已重新加入队列')
fetchTasks() // 刷新任务列表
}
} catch (error) {
console.error('重试任务失败:', error)
}
})
}
// 获取标签类型
const getTagType = (tag) => {
switch (tag?.toUpperCase()) {
case 'CLEANED':
return 'info'
case 'CACHED':
return 'success'
default:
return 'default'
}
}
// 添加移动端检测
const isMobile = computed(() => {
return window.innerWidth <= 768
})
// 监听窗口大小变化
onMounted(() => {
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768
})
})
onUnmounted(() => {
window.removeEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768
})
})
// 判断是否可以使用代理下载
const canDownloadProxied = computed(() => {
if (!currentTaskDetails.value) return false
const validStatus = [3, 4] // 3: 已完成, 4: 已缓存
return (
validStatus.includes(currentTaskDetails.value.status) &&
currentTaskDetails.value.tag !== 'CLEANED'
)
})
// 添加代理下载方法
const downloadProxiedFile = () => {
if (!currentTaskDetails.value || !canDownloadProxied.value) return
const downloadUrl = `${API_BASE_URL}/Download/${currentTaskDetails.value.tid}`
window.open(downloadUrl, '_blank')
}
</script>
<style scoped>
.task-management {
padding: 20px;
}
.task-card {
background: #fff;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 500;
margin-right: 12px;
}
.task-count {
font-size: 13px;
}
:deep(.el-button) {
display: inline-flex;
align-items: center;
gap: 4px;
}
:deep(.el-table) {
margin-top: 20px;
}
.status-tag {
width: 100px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.status-1 {
animation: pulse 1.5s infinite;
}
.status-6 {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
.process-info {
padding: 10px;
}
.threads-title {
font-size: 16px;
font-weight: 500;
margin: 20px 0 10px;
color: #606266;
}
:deep(.el-descriptions) {
margin-bottom: 20px;
}
:deep(.el-descriptions__label) {
width: 120px;
font-weight: 500;
}
:deep(.el-textarea__inner) {
font-family: monospace;
font-size: 13px;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-input {
width: 240px;
}
:deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__wrapper:hover) {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #409eff inset !important;
}
.task-details {
padding: 10px;
}
.details-actions {
margin-top: 20px;
display: flex;
gap: 12px;
justify-content: flex-end;
flex-wrap: wrap; /* 允许按钮换行 */
}
:deep(.el-descriptions__cell) {
word-break: break-all;
}
:deep(.el-button .el-icon) {
margin-right: 4px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 添加队列错误状态的样式 */
.status-6.el-tag--danger {
animation: none; /* 移除脉冲动画 */
}
/* 添加任务对话框样式 */
:deep(.add-task-dialog) {
.el-message-box__input {
textarea {
font-family: monospace;
font-size: 13px;
}
}
}
/* 添加响应式样式 */
@media screen and (max-width: 768px) {
.task-management {
padding: 10px;
}
.card-header {
flex-direction: column;
gap: 12px;
}
.header-right {
width: 100%;
flex-direction: column;
gap: 8px;
}
.search-input {
width: 100%;
}
/* 让按钮占满宽度 */
.header-right .el-button {
width: 100%;
justify-content: center;
}
/* 调整表格在移动端的显示 */
:deep(.el-table) {
font-size: 12px;
}
/* 隐藏不重要的列 */
:deep(.el-table .cell) {
padding: 8px;
}
/* 调整操作按钮布局 */
:deep(.el-table .el-button) {
padding: 4px 8px;
font-size: 12px;
}
/* 调整对话框宽度 */
:deep(.el-dialog) {
width: 90% !important;
margin: 0 auto;
}
/* 调整分页器样式 */
.pagination-container {
.el-pagination {
justify-content: center;
flex-wrap: wrap;
padding: 8px;
.el-pagination__total,
.el-pagination__sizes,
.el-pagination__jump {
display: none; /* 在移动端隐藏这些元素 */
}
}
}
.details-actions {
flex-direction: column;
gap: 8px;
.el-button {
width: 100%;
justify-content: center;
}
}
}
/* 平板端样式调整 */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.task-management {
padding: 15px;
}
.search-input {
width: 200px;
}
:deep(.el-table .el-button) {
padding: 6px 10px;
}
}
/* 添加一些通用的响应式辅助类 */
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/* 优化表格在不同屏幕尺寸下的显示 */
:deep(.el-table) {
@media screen and (max-width: 768px) {
/* UUID列宽度调整 */
.el-table-column--tid {
min-width: 120px;
max-width: 150px;
}
/* 状态列固定宽度 */
.el-table-column--status {
width: 15px !important;
}
/* 操作列自适应 */
.el-table-column--operation {
width: auto !important;
min-width: 500px;
}
}
}
/* 优化对话框内容在移动端的显示 */
:deep(.el-dialog__body) {
@media screen and (max-width: 768px) {
padding: 15px;
.el-descriptions__label {
width: 80px;
min-width: 80px;
}
.el-descriptions__content {
word-break: break-all;
}
}
}
/* 优化按钮组在移动端的显示 */
.details-actions {
@media screen and (max-width: 768px) {
flex-direction: column;
gap: 8px;
.el-button {
width: 100%;
}
}
}
/* 优化固定列的样式 */
:deep(.operation-column) {
background-color: #fff; /* 确保背景色与表格一致 */
.cell {
white-space: nowrap; /* 防止按钮换行 */
}
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
:deep(.operation-column) {
.cell {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px !important; /* 减小内边距 */
.el-button {
width: 100%;
justify-content: center;
padding: 4px 0; /* 调整按钮内边距 */
margin: 0; /* 移除按钮外边距 */
}
}
}
}
/* 优化对话框内容在移动端的显示 */
:deep(.el-dialog) {
@media screen and (max-width: 768px) {
.details-actions {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
padding: 0 20px;
}
.el-button {
margin: 0;
width: 100%;
}
}
}
</style>