Files
AIGC/demo/frontend/src/components/TaskStatusDisplay.vue
AIGC Developer 3c37006ebd feat: 添加任务状态级联触发器,优化支付和做同款功能
主要更新:
- 添加 MySQL 触发器实现 task_status 表到其他表的状态级联
- 移除控制器中的多表状态检查代码
- 完善做同款功能,支持参数传递
- 支付宝 USD 转 CNY 汇率转换
- 修复状态枚举映射问题

注意: 触发器仅在 task_status 更新时触发,部分代码仍直接更新业务表
2025-12-08 13:54:02 +08:00

436 lines
9.1 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>任务状态</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>排队中...</div>
</div>
<div class="task-info">
<div class="info-item">
<span class="label">任务ID:</span>
<span class="value">{{ taskStatus?.taskId }}</span>
</div>
<div class="info-item">
<span class="label">创建时间:</span>
<span class="value">{{ formatDate(taskStatus?.createdAt) }}</span>
</div>
<div class="info-item" v-if="taskStatus?.completedAt">
<span class="label">完成时间:</span>
<span class="value">{{ formatDate(taskStatus.completedAt) }}</span>
</div>
<div class="info-item" v-if="taskStatus?.resultUrl">
<span class="label">结果URL:</span>
<a :href="taskStatus.resultUrl" target="_blank" class="result-link">
查看结果
</a>
</div>
<div class="info-item" v-if="taskStatus?.errorMessage">
<span class="label">错误信息:</span>
<span class="value error">{{ taskStatus.errorMessage }}</span>
</div>
</div>
<div class="action-buttons" v-if="showActions">
<button
v-if="canCancel"
@click="cancelTask"
class="btn-cancel"
:disabled="cancelling"
>
{{ cancelling ? '取消中...' : '取消任务' }}
</button>
<button
v-if="canRetry"
@click="retryTask"
class="btn-retry"
>
重试
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { taskStatusApi } from '@/api/taskStatus'
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 cancelling = 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 '未知'
return taskStatus.value.statusDescription || taskStatus.value.status
})
const showActions = computed(() => {
if (!taskStatus.value) return false
return ['PENDING', 'PROCESSING'].includes(taskStatus.value.status)
})
const canCancel = computed(() => {
if (!taskStatus.value) return false
return taskStatus.value.status === 'PROCESSING'
})
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 cancelTask = async () => {
try {
cancelling.value = true
const response = await taskStatusApi.cancelTask(props.taskId)
if (response.data.success) {
await fetchTaskStatus() // 刷新状态
} else {
alert('取消失败: ' + response.data.message)
}
} catch (error) {
console.error('取消任务失败:', error)
alert('取消任务失败: ' + error.message)
} finally {
cancelling.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-cancel,
.btn-retry {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-cancel {
background: #ef4444;
color: white;
}
.btn-cancel:hover:not(:disabled) {
background: #dc2626;
}
.btn-cancel:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-retry {
background: #3b82f6;
color: white;
}
.btn-retry:hover {
background: #2563eb;
}
</style>