Files
AIGC/demo/frontend/src/views/TaskStatusPage.vue
AIGC Developer 8c55f9f376 feat: 完成代码逻辑错误修复和任务清理系统实现
主要更新:
- 修复了所有主要的代码逻辑错误
- 实现了完整的任务清理系统
- 添加了系统设置页面的任务清理管理功能
- 修复了API调用认证问题
- 优化了密码加密和验证机制
- 统一了错误处理模式
- 添加了详细的文档和测试工具

新增功能:
- 任务清理管理界面
- 任务归档和清理日志
- API监控和诊断工具
- 完整的测试套件

技术改进:
- 修复了Repository方法调用错误
- 统一了模型方法调用
- 改进了类型安全性
- 优化了代码结构和可维护性
2025-10-27 10:46:49 +08:00

609 lines
12 KiB
Vue

<template>
<div class="task-status-page">
<div class="page-header">
<h1>任务状态监控</h1>
<div class="header-actions">
<button @click="refreshAll" class="btn-refresh" :disabled="loading">
{{ loading ? '刷新中...' : '刷新全部' }}
</button>
<button @click="triggerPolling" class="btn-poll" v-if="isAdmin">
手动轮询
</button>
</div>
</div>
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon pending"></div>
<div class="stat-content">
<div class="stat-number">{{ stats.pending }}</div>
<div class="stat-label">待处理</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon processing">🔄</div>
<div class="stat-content">
<div class="stat-number">{{ stats.processing }}</div>
<div class="stat-label">处理中</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon completed"></div>
<div class="stat-content">
<div class="stat-number">{{ stats.completed }}</div>
<div class="stat-label">已完成</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon failed"></div>
<div class="stat-content">
<div class="stat-number">{{ stats.failed }}</div>
<div class="stat-label">失败</div>
</div>
</div>
</div>
<div class="task-list">
<div class="list-header">
<h2>任务列表</h2>
<div class="filter-controls">
<select v-model="statusFilter" @change="filterTasks">
<option value="">全部状态</option>
<option value="PENDING">待处理</option>
<option value="PROCESSING">处理中</option>
<option value="COMPLETED">已完成</option>
<option value="FAILED">失败</option>
<option value="CANCELLED">已取消</option>
<option value="TIMEOUT">超时</option>
</select>
</div>
</div>
<div class="task-items">
<div
v-for="task in filteredTasks"
:key="task.taskId"
class="task-item"
:class="getTaskItemClass(task.status)"
>
<div class="task-main">
<div class="task-info">
<div class="task-id">{{ task.taskId }}</div>
<div class="task-type">{{ task.taskType?.description || task.taskType }}</div>
<div class="task-time">{{ formatDate(task.createdAt) }}</div>
</div>
<div class="task-status">
<div class="status-badge" :class="getStatusClass(task.status)">
{{ task.statusDescription || task.status }}
</div>
<div class="progress-info" v-if="task.status === 'PROCESSING'">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: task.progress + '%' }"
></div>
</div>
<span class="progress-text">{{ task.progress }}%</span>
</div>
</div>
</div>
<div class="task-actions">
<button
v-if="task.status === 'PROCESSING'"
@click="cancelTask(task.taskId)"
class="btn-cancel"
>
取消
</button>
<button
v-if="task.resultUrl"
@click="viewResult(task.resultUrl)"
class="btn-view"
>
查看结果
</button>
<button
v-if="['FAILED', 'TIMEOUT'].includes(task.status)"
@click="retryTask(task.taskId)"
class="btn-retry"
>
重试
</button>
</div>
</div>
</div>
<div v-if="filteredTasks.length === 0" class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">暂无任务</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { taskStatusApi } from '@/api/taskStatus'
import { ElMessage } from 'element-plus'
const userStore = useUserStore()
const tasks = ref([])
const loading = ref(false)
const statusFilter = ref('')
const refreshTimer = ref(null)
// 计算属性
const isAdmin = computed(() => userStore.isAdmin)
const stats = computed(() => {
const stats = {
pending: 0,
processing: 0,
completed: 0,
failed: 0
}
tasks.value.forEach(task => {
switch (task.status) {
case 'PENDING':
stats.pending++
break
case 'PROCESSING':
stats.processing++
break
case 'COMPLETED':
stats.completed++
break
case 'FAILED':
case 'CANCELLED':
case 'TIMEOUT':
stats.failed++
break
}
})
return stats
})
const filteredTasks = computed(() => {
if (!statusFilter.value) {
return tasks.value
}
return tasks.value.filter(task => task.status === statusFilter.value)
})
// 方法
const fetchTasks = async () => {
try {
loading.value = true
const response = await taskStatusApi.getUserTaskStatuses(userStore.user?.username)
tasks.value = response.data
} catch (error) {
console.error('获取任务列表失败:', error)
ElMessage.error('获取任务列表失败')
} finally {
loading.value = false
}
}
const refreshAll = async () => {
await fetchTasks()
ElMessage.success('任务列表已刷新')
}
const filterTasks = () => {
// 过滤逻辑在计算属性中处理
}
const cancelTask = async (taskId) => {
try {
const response = await taskStatusApi.cancelTask(taskId)
if (response.data.success) {
ElMessage.success('任务已取消')
await fetchTasks()
} else {
ElMessage.error(response.data.message)
}
} catch (error) {
console.error('取消任务失败:', error)
ElMessage.error('取消任务失败')
}
}
const viewResult = (resultUrl) => {
window.open(resultUrl, '_blank')
}
const retryTask = (taskId) => {
// 重试逻辑,可以导航到相应的创建页面
ElMessage.info('重试功能开发中')
}
const triggerPolling = async () => {
try {
const response = await taskStatusApi.triggerPolling()
if (response.data.success) {
ElMessage.success('轮询已触发')
}
} catch (error) {
console.error('触发轮询失败:', error)
ElMessage.error('触发轮询失败')
}
}
const getTaskItemClass = (status) => {
return `task-item-${status.toLowerCase()}`
}
const getStatusClass = (status) => {
return `status-${status.toLowerCase()}`
}
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
const startAutoRefresh = () => {
refreshTimer.value = setInterval(fetchTasks, 30000) // 30秒刷新一次
}
const stopAutoRefresh = () => {
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
refreshTimer.value = null
}
}
// 生命周期
onMounted(() => {
fetchTasks()
startAutoRefresh()
})
onUnmounted(() => {
stopAutoRefresh()
})
</script>
<style scoped>
.task-status-page {
padding: 24px;
background: #0a0a0a;
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.page-header h1 {
color: #fff;
font-size: 28px;
font-weight: 700;
margin: 0;
}
.header-actions {
display: flex;
gap: 12px;
}
.btn-refresh,
.btn-poll {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-refresh {
background: #3b82f6;
color: white;
}
.btn-refresh:hover:not(:disabled) {
background: #2563eb;
}
.btn-refresh:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-poll {
background: #10b981;
color: white;
}
.btn-poll:hover {
background: #059669;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.stat-card {
background: #1a1a1a;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
font-size: 32px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
}
.stat-icon.pending {
background: #fbbf24;
}
.stat-icon.processing {
background: #3b82f6;
}
.stat-icon.completed {
background: #10b981;
}
.stat-icon.failed {
background: #ef4444;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #fff;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #9ca3af;
}
.task-list {
background: #1a1a1a;
border-radius: 12px;
padding: 24px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.list-header h2 {
color: #fff;
font-size: 20px;
font-weight: 600;
margin: 0;
}
.filter-controls select {
padding: 8px 12px;
border: 1px solid #374151;
border-radius: 6px;
background: #1a1a1a;
color: #fff;
font-size: 14px;
}
.task-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.task-item {
background: #0a0a0a;
border-radius: 8px;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid #374151;
}
.task-item-pending {
border-left-color: #fbbf24;
}
.task-item-processing {
border-left-color: #3b82f6;
}
.task-item-completed {
border-left-color: #10b981;
}
.task-item-failed,
.task-item-cancelled,
.task-item-timeout {
border-left-color: #ef4444;
}
.task-main {
display: flex;
align-items: center;
gap: 20px;
flex: 1;
}
.task-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.task-id {
color: #fff;
font-weight: 600;
font-size: 14px;
}
.task-type {
color: #9ca3af;
font-size: 12px;
}
.task-time {
color: #6b7280;
font-size: 12px;
}
.task-status {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.status-pending {
background: #fbbf24;
color: #92400e;
}
.status-processing {
background: #3b82f6;
color: #1e40af;
}
.status-completed {
background: #10b981;
color: #064e3b;
}
.status-failed,
.status-cancelled,
.status-timeout {
background: #ef4444;
color: #7f1d1d;
}
.progress-info {
display: flex;
align-items: center;
gap: 8px;
}
.progress-bar {
width: 100px;
height: 4px;
background: #374151;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
}
.progress-text {
color: #9ca3af;
font-size: 12px;
}
.task-actions {
display: flex;
gap: 8px;
}
.btn-cancel,
.btn-view,
.btn-retry {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-cancel {
background: #ef4444;
color: white;
}
.btn-cancel:hover {
background: #dc2626;
}
.btn-view {
background: #3b82f6;
color: white;
}
.btn-view:hover {
background: #2563eb;
}
.btn-retry {
background: #10b981;
color: white;
}
.btn-retry:hover {
background: #059669;
}
.empty-state {
text-align: center;
padding: 40px;
color: #6b7280;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
}
</style>