Files
zmAI/demo/frontend/src/components/TaskStatusDisplay.vue

390 lines
8.4 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-status-display">
<div class="status-header">
<h3>{{ $t('taskMonitor.taskStatus') }}</h3>
<div class="status-badge" :class="statusClass">
{{ statusText }}
</div>
</div>
<div class="progress-section" v-if="taskStatus">
<!-- 排队中不确定进度条 -->
<div v-if="taskStatus.status === 'PENDING'" class="progress-bar indeterminate">
<div class="progress-fill-indeterminate"></div>
</div>
<!-- 生成中动态进度条 -->
<div v-else class="progress-bar">
<div
class="progress-fill animated"
:style="{ width: taskStatus.progress + '%' }"
></div>
</div>
<div class="progress-text" v-if="taskStatus.status !== 'PENDING'">{{ taskStatus.progress }}%</div>
<div class="progress-text" v-else>{{ $t('taskMonitor.queuing') }}</div>
</div>
<div class="task-info">
<div class="info-item">
<span class="label">{{ $t('taskMonitor.taskId') }}:</span>
<span class="value">{{ taskStatus?.taskId }}</span>
</div>
<div class="info-item">
<span class="label">{{ $t('taskMonitor.createTime') }}:</span>
<span class="value">{{ formatDate(taskStatus?.createdAt) }}</span>
</div>
<div class="info-item" v-if="taskStatus?.completedAt">
<span class="label">{{ $t('taskMonitor.completeTime') }}:</span>
<span class="value">{{ formatDate(taskStatus.completedAt) }}</span>
</div>
<div class="info-item" v-if="taskStatus?.resultUrl">
<span class="label">{{ $t('taskMonitor.resultUrl') }}:</span>
<a :href="taskStatus.resultUrl" target="_blank" class="result-link">
{{ $t('taskMonitor.viewResult') }}
</a>
</div>
<div class="info-item" v-if="taskStatus?.errorMessage">
<span class="label">{{ $t('taskMonitor.errorMessage') }}:</span>
<span class="value error">{{ taskStatus.errorMessage }}</span>
</div>
</div>
<div class="action-buttons" v-if="showActions">
<button
v-if="canRetry"
@click="retryTask"
class="btn-retry"
>
{{ $t('taskMonitor.retry') }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { message } from 'ant-design-vue'
import { taskStatusApi } from '@/api/taskStatus'
const { t } = useI18n()
const props = defineProps({
taskId: {
type: String,
required: true
},
autoRefresh: {
type: Boolean,
default: true
},
refreshInterval: {
type: Number,
default: 30000 // 30秒
}
})
const emit = defineEmits(['statusChanged', 'taskCompleted', 'taskFailed'])
const taskStatus = ref(null)
const loading = ref(false)
const refreshTimer = ref(null)
// 计算属性
const statusClass = computed(() => {
if (!taskStatus.value) return 'status-pending'
switch (taskStatus.value.status) {
case 'PENDING':
return 'status-pending'
case 'PROCESSING':
return 'status-processing'
case 'COMPLETED':
return 'status-completed'
case 'FAILED':
return 'status-failed'
case 'CANCELLED':
return 'status-cancelled'
case 'TIMEOUT':
return 'status-timeout'
default:
return 'status-pending'
}
})
const statusText = computed(() => {
if (!taskStatus.value) return t('taskMonitor.unknown')
return taskStatus.value.statusDescription || taskStatus.value.status
})
const showActions = computed(() => {
if (!taskStatus.value) return false
return ['FAILED', 'TIMEOUT'].includes(taskStatus.value.status)
})
const canRetry = computed(() => {
if (!taskStatus.value) return false
return ['FAILED', 'TIMEOUT'].includes(taskStatus.value.status)
})
// 方法
const fetchTaskStatus = async () => {
try {
loading.value = true
const response = await taskStatusApi.getTaskStatus(props.taskId)
taskStatus.value = response.data
// 触发状态变化事件
emit('statusChanged', taskStatus.value)
// 检查任务是否完成
if (taskStatus.value.status === 'COMPLETED') {
emit('taskCompleted', taskStatus.value)
} else if (['FAILED', 'TIMEOUT', 'CANCELLED'].includes(taskStatus.value.status)) {
emit('taskFailed', taskStatus.value)
}
} catch (error) {
console.error('获取任务状态失败:', error)
} finally {
loading.value = false
}
}
const retryTask = () => {
// 重试逻辑,这里可以触发重新创建任务
emit('retryTask', props.taskId)
}
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
const startAutoRefresh = () => {
if (props.autoRefresh && !refreshTimer.value) {
refreshTimer.value = setInterval(fetchTaskStatus, props.refreshInterval)
}
}
const stopAutoRefresh = () => {
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
refreshTimer.value = null
}
}
// 生命周期
onMounted(() => {
fetchTaskStatus()
startAutoRefresh()
})
onUnmounted(() => {
stopAutoRefresh()
})
</script>
<style scoped>
.task-status-display {
background: #1a1a1a;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.status-header h3 {
margin: 0;
color: #fff;
font-size: 18px;
font-weight: 600;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-pending {
background: #fbbf24;
color: #92400e;
}
.status-processing {
background: #3b82f6;
color: #1e40af;
}
.status-completed {
background: #10b981;
color: #064e3b;
}
.status-failed {
background: #ef4444;
color: #7f1d1d;
}
.status-cancelled {
background: #6b7280;
color: #374151;
}
.status-timeout {
background: #f59e0b;
color: #78350f;
}
.progress-section {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #374151;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
transition: width 0.3s ease;
position: relative;
}
/* 动态进度条动画 */
.progress-fill.animated {
background: linear-gradient(90deg, #3b82f6, #60a5fa, #3b82f6);
background-size: 200% 100%;
animation: progress-gradient 2s ease infinite, progress-pulse 1.5s ease-in-out infinite;
}
.progress-fill.animated::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: progress-shine 1.5s ease-in-out infinite;
}
@keyframes progress-gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes progress-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.85; }
}
@keyframes progress-shine {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* 不确定进度条(排队中) */
.progress-bar.indeterminate {
overflow: hidden;
}
.progress-fill-indeterminate {
width: 30%;
height: 100%;
background: linear-gradient(90deg, transparent, #3b82f6, #60a5fa, #3b82f6, transparent);
border-radius: 4px;
animation: indeterminate-slide 1.5s ease-in-out infinite;
}
@keyframes indeterminate-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
.progress-text {
text-align: center;
color: #9ca3af;
font-size: 14px;
font-weight: 500;
}
.task-info {
margin-bottom: 20px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #374151;
}
.info-item:last-child {
border-bottom: none;
}
.label {
color: #9ca3af;
font-size: 14px;
}
.value {
color: #fff;
font-size: 14px;
font-weight: 500;
}
.value.error {
color: #ef4444;
}
.result-link {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
}
.result-link:hover {
text-decoration: underline;
}
.action-buttons {
display: flex;
gap: 12px;
}
.btn-retry {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
background: #3b82f6;
color: white;
}
.btn-retry:hover {
background: #2563eb;
}
</style>