feat: 添加任务状态级联触发器,优化支付和做同款功能

主要更新:
- 添加 MySQL 触发器实现 task_status 表到其他表的状态级联
- 移除控制器中的多表状态检查代码
- 完善做同款功能,支持参数传递
- 支付宝 USD 转 CNY 汇率转换
- 修复状态枚举映射问题

注意: 触发器仅在 task_status 更新时触发,部分代码仍直接更新业务表
This commit is contained in:
AIGC Developer
2025-12-08 13:54:02 +08:00
parent 624d560fb4
commit 3c37006ebd
84 changed files with 5325 additions and 1668 deletions

View File

@@ -176,9 +176,19 @@
@error="onImageError"
/>
<!-- 否则使用默认占位符 -->
<div v-else class="work-placeholder">
<div v-else class="work-placeholder" :class="{ 'is-processing': item.status === 'PROCESSING' || item.status === 'PENDING' }">
<el-icon><VideoPlay /></el-icon>
<div class="placeholder-text">{{ item.status === 'PROCESSING' ? t('works.processing') : t('works.noPreview') }}</div>
<div class="placeholder-text">{{ item.status === 'PROCESSING' ? t('works.processing') : (item.status === 'PENDING' ? t('works.queuing') : t('works.noPreview')) }}</div>
</div>
<!-- 生成中/排队中状态的覆盖层始终显示 -->
<div v-if="item.status === 'PROCESSING' || item.status === 'PENDING'" class="processing-overlay">
<div class="processing-content">
<el-icon class="processing-icon"><VideoPlay /></el-icon>
<div class="processing-text">{{ item.status === 'PROCESSING' ? t('works.processing') : t('works.queuing') }}</div>
<div class="progress-bar-container">
<div class="progress-bar-animated"></div>
</div>
</div>
</div>
<div class="checker" v-if="multiSelect">
@@ -396,6 +406,11 @@
<span>{{ t('profile.systemSettings') }}</span>
</div>
</template>
<!-- 修改密码 -->
<div class="menu-item" @click.stop="goToChangePassword" style="cursor: pointer;">
<el-icon><Lock /></el-icon>
<span>{{ t('profile.changePassword') }}</span>
</div>
<div class="menu-item" @click="logout">
<el-icon><User /></el-icon>
<span>{{ t('common.logout') }}</span>
@@ -409,7 +424,7 @@
import { ref, onMounted, onActivated, computed, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Star, User, Compass, Document, VideoPlay, Picture, Film, Search, MoreFilled, Loading, ArrowUp, VideoCamera, Refresh, Delete, CopyDocument, Download, Close } from '@element-plus/icons-vue'
import { Star, User, Compass, Document, VideoPlay, Picture, Film, Search, MoreFilled, Loading, ArrowUp, VideoCamera, Refresh, Delete, CopyDocument, Download, Close, Setting, Lock } from '@element-plus/icons-vue'
import { getMyWorks, getWorkDetail, deleteWork, recordDownload, getWorkFileUrl } from '@/api/userWorks'
import { getCurrentUser } from '@/api/auth'
import { useUserStore } from '@/stores/user'
@@ -578,6 +593,11 @@ const goToSettings = () => {
}
}
const goToChangePassword = () => {
showUserMenu.value = false
router.push('/change-password')
}
// 退出登录
const logout = async () => {
try {
@@ -618,7 +638,9 @@ const loadList = async () => {
})
// 转换数据格式
const transformedData = data.map(transformWorkData)
const transformedData = data
.map(transformWorkData)
.filter(work => work.status !== 'FAILED' && work.status !== 'DELETED')
// 调试日志: 查看转换后的数据
console.log('转换后的作品数据:', transformedData)
@@ -924,18 +946,19 @@ const deleteFailedWork = async () => {
)
// 执行删除
console.log('删除作品:', selectedItem.value.id)
const response = await deleteWork(selectedItem.value.id)
const itemId = selectedItem.value.id // 先保存 id因为 handleClose 会将 selectedItem 设为 null
console.log('删除作品:', itemId)
const response = await deleteWork(itemId)
if (response.data.success) {
ElMessage.success(t('works.deleteSuccess'))
// 从列表中移除该作品(在关闭详情页之前)
items.value = items.value.filter(item => item.id !== itemId)
// 关闭详情页
handleClose()
// 从列表中移除该作品
items.value = items.value.filter(item => item.id !== selectedItem.value.id)
// 或者重新加载列表
// reload()
} else {
@@ -951,19 +974,31 @@ const deleteFailedWork = async () => {
// 创建同款
const createSimilar = (item) => {
if (item) {
ElMessage.info(t('works.createSimilarInfo', { title: item.title }))
// 根据作品类型跳转到相应的创建页面
if (item.type === 'video') {
router.push('/text-to-video/create')
} else if (item.type === 'image') {
router.push('/image-to-video/create')
} else {
router.push('/text-to-video/create')
}
} else {
if (!item) {
ElMessage.info(t('works.goToCreate'))
return
}
// 根据作品类别跳转到对应的创建页面,并携带参数
const query = {
taskId: item.taskId,
prompt: item.prompt || '',
aspectRatio: item.aspectRatio || '',
duration: item.duration || ''
}
if (item.category === '文生视频') {
router.push({ path: '/text-to-video/create', query })
} else if (item.category === '图生视频') {
router.push({ path: '/image-to-video/create', query })
} else if (item.category === '分镜视频') {
router.push({ path: '/storyboard-video/create', query })
} else {
// 默认跳转到文生视频
router.push({ path: '/text-to-video/create', query })
}
ElMessage.success(t('works.createSimilarInfo', { title: item.title }))
}
const download = async (item) => {
@@ -974,7 +1009,7 @@ const download = async (item) => {
return
}
ElMessage.success(t('works.downloadStart', { title: item.title }))
ElMessage.info(t('works.downloadStart', { title: item.title }))
// 记录下载次数
try {
@@ -983,7 +1018,41 @@ const download = async (item) => {
console.warn('记录下载次数失败:', err)
}
// 构建下载URL使用代理下载模式download=true避免 CORS 问题
// 尝试直接从 resultUrl 下载(绕过后端代理)
const videoUrl = item.resultUrl
console.log('直接下载视频URL:', videoUrl)
try {
const response = await fetch(videoUrl)
if (!response.ok) {
throw new Error(`下载失败: ${response.status}`)
}
const blob = await response.blob()
console.log('文件大小:', blob.size, 'bytes')
if (blob.size === 0) {
throw new Error('文件内容为空')
}
const blobUrl = window.URL.createObjectURL(blob)
const filename = `${item.title || 'work'}_${Date.now()}${item.type === 'video' ? '.mp4' : '.png'}`
const a = document.createElement('a')
a.href = blobUrl
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000)
ElMessage.success(t('works.downloadComplete'))
return
} catch (directError) {
console.warn('直接下载失败,尝试后端代理:', directError)
}
// 备用方案:使用后端代理
const downloadUrl = getWorkFileUrl(item.id, true)
const token = sessionStorage.getItem('token')
@@ -1773,6 +1842,22 @@ onActivated(() => {
.filters-bar :deep(.el-select__placeholder) {
opacity: 1 !important;
}
/* 搜索框深色样式 */
.filters-bar :deep(.el-input__wrapper) {
background-color: rgba(255, 255, 255, 0.1) !important;
border: none !important;
box-shadow: none !important;
}
.filters-bar :deep(.el-input__inner) {
color: #ffffff !important;
}
.filters-bar :deep(.el-input__inner::placeholder) {
color: rgba(255, 255, 255, 0.6) !important;
}
.select-row { padding: 4px 0 8px; }
.works-grid {
margin-top: 12px;
@@ -1820,6 +1905,84 @@ onActivated(() => {
font-size: 12px;
color: #666;
}
/* 动态进度条 */
.progress-bar-container {
width: 60%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
margin-top: 12px;
}
.progress-bar-animated {
width: 30%;
height: 100%;
background: linear-gradient(90deg, #409eff, #67c23a, #409eff);
background-size: 200% 100%;
border-radius: 2px;
animation: progress-move 1.5s ease-in-out infinite, progress-gradient 2s linear infinite;
}
@keyframes progress-move {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(233%);
}
100% {
transform: translateX(-100%);
}
}
@keyframes progress-gradient {
0% {
background-position: 0% 50%;
}
100% {
background-position: 200% 50%;
}
}
/* 生成中/排队中状态覆盖层 */
.processing-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
border-radius: 8px;
}
.processing-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
}
.processing-icon {
font-size: 32px;
color: #409eff;
}
.processing-text {
font-size: 14px;
color: #fff;
font-weight: 500;
}
.work-placeholder.is-processing {
background: rgba(0, 0, 0, 0.5);
}
.checker { position: absolute; left: 6px; top: 6px; }
.actions { position: absolute; right: 6px; top: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity .2s ease; }
.thumb:hover .actions { opacity: 1; }
@@ -2273,4 +2436,58 @@ onActivated(() => {
}
</style>
<!-- 全局下拉框深色样式弹出层传送到body需要非scoped样式 -->
<style>
.el-select-dropdown {
background-color: #1a1a1a !important;
border: 1px solid #333 !important;
}
.el-select-dropdown__item {
color: #e5e7eb !important;
}
.el-select-dropdown__item:hover,
.el-select-dropdown__item.hover {
background-color: #2a2a2a !important;
}
.el-select-dropdown__item.is-selected {
color: #409eff !important;
background-color: rgba(64, 158, 255, 0.1) !important;
}
.el-popper.is-light {
background-color: #1a1a1a !important;
border: 1px solid #333 !important;
}
.el-popper.is-light .el-popper__arrow::before {
background-color: #1a1a1a !important;
border-color: #333 !important;
}
</style>
<!-- scoped 样式用于 @keyframes 动画 -->
<style>
@keyframes progress-move {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(233%);
}
100% {
transform: translateX(-100%);
}
}
@keyframes progress-gradient {
0% {
background-position: 0% 50%;
}
100% {
background-position: 200% 50%;
}
}
</style>