fix: PayPal payment_method column length issue; add image model selection for storyboard; remove task restore popups; sync UserWork status on task failure

This commit is contained in:
AIGC Developer
2025-12-05 09:57:09 +08:00
parent dbd06435cb
commit b4b0230ee1
484 changed files with 5238 additions and 5379 deletions

View File

@@ -190,12 +190,7 @@
<el-button circle size="small" text><el-icon><MoreFilled /></el-icon></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="download_with_watermark">{{ t('works.downloadWithWatermark') }}</el-dropdown-item>
<el-dropdown-item command="download_without_watermark">
{{ t('works.downloadWithoutWatermark') }}
<el-tag type="primary" size="small" style="margin-left: 8px;">{{ t('works.memberOnly') }}</el-tag>
</el-dropdown-item>
<el-dropdown-item command="rename" divided>{{ t('works.rename') }}</el-dropdown-item>
<el-dropdown-item command="rename">{{ t('works.rename') }}</el-dropdown-item>
<el-dropdown-item command="delete">{{ t('common.delete') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -232,16 +227,23 @@
<el-dialog
v-model="detailDialogVisible"
:title="selectedItem?.title"
width="60%"
width="70vw"
:before-close="handleClose"
class="detail-dialog"
class="work-detail-dialog"
:modal="true"
:show-close="false"
:close-on-click-modal="true"
:close-on-press-escape="true"
align-center
>
<div class="detail-content" v-if="selectedItem">
<div class="detail-left">
<div class="video-container">
<!-- 自定义关闭按钮 -->
<div class="dialog-close-btn" @click="handleClose">
<el-icon><Close /></el-icon>
</div>
<div class="detail-content" :class="{ 'vertical-content': isVerticalVideo }" v-if="selectedItem">
<div class="detail-left" :class="{ 'vertical-left': isVerticalVideo }">
<div class="video-container" :class="{ 'vertical-container': isVerticalVideo }">
<!-- 视频加载失败提示 -->
<div v-if="detailVideoError" class="video-error-overlay">
<div class="error-content">
@@ -280,7 +282,15 @@
:alt="selectedItem.title"
/>
<!-- 视频文字叠加 已移除用户要求 -->
<!-- 悬浮操作按钮 -->
<div class="overlay-actions">
<button class="icon-btn" @click="downloadWork" :title="t('common.download')">
<el-icon><Download /></el-icon>
</button>
<button class="icon-btn delete" @click="deleteFailedWork" :title="t('common.delete')">
<el-icon><Delete /></el-icon>
</button>
</div>
</div>
</div>
@@ -295,42 +305,21 @@
</div>
</div>
<!-- 标签页 -->
<div class="tabs">
<div class="tab" :class="{ active: activeDetailTab === 'detail' }" @click="activeDetailTab = 'detail'">{{ t('profile.workDetail') }}</div>
<div class="tab" :class="{ active: activeDetailTab === 'category' }" @click="activeDetailTab = 'category'">{{ selectedItem.category }}</div>
<!-- 详情标题行 -->
<div class="detail-title-row">
<h3>{{ t('works.videoDetail') }}</h3>
<span class="category-badge">{{ selectedItem.category }}</span>
</div>
<!-- 提示词区域 -->
<div class="description-section" v-if="activeDetailTab === 'detail'">
<h3 class="section-title">{{ t('video.prompt') }}</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
</div>
<!-- 参考图特殊内容 -->
<div class="reference-content" v-if="activeDetailTab === 'category' && selectedItem.category === '参考图'">
<div class="input-details-section">
<h3 class="section-title">{{ t('profile.inputDetails') }}</h3>
<div class="input-images">
<div class="input-image-item">
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
</div>
<div class="input-image-item">
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
</div>
</div>
<!-- 描述区域 -->
<div class="description-section">
<div class="section-header">
<span class="section-label">{{ t('works.description') }}</span>
</div>
<div class="description-section">
<h3 class="section-title">{{ t('video.prompt') }}</h3>
<p class="description-text">{{ t('works.referenceImagePrompt') }}</p>
</div>
</div>
<!-- 其他分类的内容 -->
<div class="description-section" v-if="activeDetailTab === 'category' && selectedItem.category !== '参考图'">
<h3 class="section-title">{{ t('video.prompt') }}</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
<p class="description-text">
{{ getDescription(selectedItem) }}
<el-icon class="copy-icon" @click="copyPrompt" :title="t('common.copy')"><CopyDocument /></el-icon>
</p>
</div>
<!-- 元数据区域 -->
@@ -341,34 +330,26 @@
</div>
<div class="metadata-item">
<span class="label">{{ t('profile.workId') }}</span>
<span class="value">{{ selectedItem.id }}</span>
<span class="value">{{ selectedItem.taskId }}</span>
</div>
<div class="metadata-item">
<span class="label">{{ t('profile.date') }}</span>
<span class="value">{{ selectedItem.date }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">{{ t('profile.duration') }}</span>
<span class="value">{{ formatDuration(selectedItem.duration) || t('profile.unknown') }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">{{ t('profile.quality') }}</span>
<span class="value">{{ selectedItem.quality || t('profile.unknown') }}</span>
<span class="value">{{ formatDuration(selectedItem.duration) || '5s' }}</span>
</div>
<div class="metadata-item">
<span class="label">{{ t('profile.category') }}</span>
<span class="value">{{ selectedItem.category }}</span>
<span class="label">{{ t('profile.quality') }}</span>
<span class="value">{{ selectedItem.quality || '1080p' }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<div class="metadata-item">
<span class="label">{{ t('profile.aspectRatio') }}</span>
<span class="value">{{ selectedItem.aspectRatio || t('profile.unknown') }}</span>
<span class="value">{{ selectedItem.aspectRatio || '16:9' }}</span>
</div>
</div>
<!-- 操作按钮 -->
<!-- 底部操作按钮 -->
<div class="action-section">
<button class="create-similar-btn" @click="createSimilar">
{{ t('profile.createSimilar') }}
<button class="create-similar-btn full-width" @click="createSimilar(selectedItem)">
{{ t('works.createSimilar') }}
</button>
</div>
</div>
@@ -428,7 +409,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 } from '@element-plus/icons-vue'
import { Star, User, Compass, Document, VideoPlay, Picture, Film, Search, MoreFilled, Loading, ArrowUp, VideoCamera, Refresh, Delete, CopyDocument, Download, Close } from '@element-plus/icons-vue'
import { getMyWorks, getWorkDetail, deleteWork, recordDownload, getWorkFileUrl } from '@/api/userWorks'
import { getCurrentUser } from '@/api/auth'
import { useUserStore } from '@/stores/user'
@@ -485,6 +466,13 @@ const activeDetailTab = ref('detail')
const detailVideoError = ref(false)
const detailVideoRef = ref(null)
// 判断是否是竖版视频9:16
const isVerticalVideo = computed(() => {
if (!selectedItem.value) return false
const ratio = selectedItem.value.aspectRatio
return ratio === '9:16' || ratio === '9/16' || ratio === '3:4' || ratio === '4:5'
})
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
@@ -496,6 +484,10 @@ const failedUrls = ref(new Set()) // 记录加载失败的URL
// 处理URL确保相对路径正确
const processUrl = (url) => {
if (!url) return null
// data: 协议Base64 图片等)直接返回,避免被当成相对路径错误加前缀
if (url.startsWith('data:')) {
return url
}
// 如果已经是完整URLhttp/https直接返回
if (url.startsWith('http://') || url.startsWith('https://')) {
return url
@@ -525,6 +517,7 @@ const transformWorkData = (work) => {
return {
id: work.id?.toString() || work.taskId || '',
taskId: work.taskId || work.id?.toString() || '',
title: work.title || work.prompt || '未命名作品',
cover: cover,
resultUrl: resultUrl || '',
@@ -1061,10 +1054,7 @@ const download = async (item) => {
}
const moreCommand = async (cmd, item) => {
if (cmd === 'download_with_watermark' || cmd === 'download_without_watermark') {
// 两种下载模式都调用同一个下载函数(暂时不区分水印)
await download(item)
} else if (cmd === 'rename') {
if (cmd === 'rename') {
ElMessage.info(t('works.renameDevMsg'))
} else if (cmd === 'delete') {
try {
@@ -1191,6 +1181,27 @@ const onVideoLoaded = (event) => {
}
}
// 复制提示词
const copyPrompt = async () => {
const prompt = getDescription(selectedItem.value)
if (!prompt || prompt === t('profile.noPrompt')) return
try {
await navigator.clipboard.writeText(prompt)
ElMessage.success(t('common.copySuccess'))
} catch (err) {
console.error('复制失败:', err)
ElMessage.error(t('common.copyFailed'))
}
}
// 下载当前作品
const downloadWork = () => {
if (selectedItem.value) {
download(selectedItem.value)
}
}
// 视频加载失败处理
const onVideoError = (event) => {
const video = event.target
@@ -1918,153 +1929,149 @@ onActivated(() => {
}
/* 模态框样式 */
:deep(.detail-dialog .el-dialog) {
background: #0a0a0a !important;
border-radius: 12px;
border: 1px solid #333;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
:deep(.work-detail-dialog) {
--el-dialog-margin-top: 5vh;
--el-dialog-bg-color: transparent;
--el-dialog-border-radius: 12px;
}
:deep(.detail-dialog .el-dialog__header) {
background: #0a0a0a !important;
border-bottom: 1px solid #333;
padding: 16px 20px;
:deep(.work-detail-dialog .el-dialog) {
background: transparent !important;
box-shadow: none !important;
border: none !important;
margin: 0 auto;
max-width: 95vw;
}
:deep(.detail-dialog .el-dialog__title) {
color: #fff !important;
font-size: 18px;
font-weight: 600;
:deep(.work-detail-dialog .el-dialog__header) {
display: none !important;
}
:deep(.detail-dialog .el-dialog__headerbtn) {
color: #fff !important;
}
:deep(.detail-dialog .el-dialog__body) {
background: #0a0a0a !important;
:deep(.work-detail-dialog .el-dialog__body) {
padding: 0 !important;
background: transparent !important;
position: relative;
}
:deep(.detail-dialog .el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
/* 遮罩层样式 */
:deep(.el-overlay) {
background-color: rgba(0, 0, 0, 0.85) !important;
backdrop-filter: blur(8px);
}
/* 强制覆盖Element Plus默认样式 */
:deep(.el-dialog) {
background: #0a0a0a !important;
/* 自定义关闭按钮 */
.dialog-close-btn {
position: absolute;
top: -50px;
right: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
color: #fff;
font-size: 18px;
z-index: 100;
}
:deep(.el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
.dialog-close-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.detail-content {
display: flex;
height: 50vh;
background: #0a0a0a;
height: 80vh;
max-height: 800px;
min-height: 500px;
background: #1a1a1a;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
border: 1px solid #333;
}
.detail-left {
flex: 2;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #000;
position: relative;
overflow: hidden;
padding: 20px;
}
.video-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.video-error-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.error-content {
text-align: center;
color: #fff;
padding: 40px;
}
.error-icon {
color: #f56c6c;
margin-bottom: 20px;
}
.error-content h3 {
font-size: 20px;
font-weight: 600;
margin: 16px 0;
color: #fff;
}
.error-content p {
font-size: 14px;
color: #9ca3af;
margin-bottom: 24px;
}
.error-actions {
display: flex;
gap: 12px;
justify-content: center;
align-items: center;
}
.detail-video, .detail-image {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
/* overlay 样式已移除(不再使用) */
.detail-right {
flex: 1;
background: #0a0a0a;
padding: 16px;
width: 280px;
flex: none;
background: #1a1a1a;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
border-left: 1px solid #333;
}
/* 竖版视频9:16特殊布局 */
.vertical-content {
height: 85vh;
max-height: 900px;
}
.vertical-left {
flex: none;
width: 45%;
min-width: 300px;
}
.vertical-container {
height: 100%;
}
.vertical-content .detail-right {
flex: 1;
width: auto;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
}
.avatar {
width: 40px;
height: 40px;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: 2px solid #333;
}
.avatar-image {
@@ -2074,113 +2081,88 @@ onActivated(() => {
}
.username {
font-size: 16px;
font-size: 14px;
font-weight: 500;
color: #fff;
}
.tabs {
.detail-title-row {
display: flex;
gap: 0;
align-items: center;
gap: 10px;
}
.tab {
padding: 8px 16px;
background: transparent;
color: #9ca3af;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s;
font-size: 14px;
}
.tab.active {
background: #409eff;
.detail-title-row h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #fff;
}
.tab:hover:not(.active) {
background: rgba(255, 255, 255, 0.1);
color: #fff;
.category-badge {
padding: 3px 8px;
background: rgba(64, 158, 255, 0.15);
border-radius: 4px;
font-size: 12px;
color: #409eff;
}
.description-section {
display: flex;
flex-direction: column;
gap: 12px;
gap: 6px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #fff;
margin: 0;
.section-label {
font-size: 13px;
color: #888;
}
.description-text {
font-size: 14px;
font-size: 13px;
line-height: 1.6;
color: #d1d5db;
color: #ccc;
margin: 0;
}
/* 参考图特殊内容样式 */
.reference-content {
display: flex;
flex-direction: column;
gap: 16px;
.copy-icon {
margin-left: 6px;
cursor: pointer;
font-size: 14px;
color: #666;
vertical-align: middle;
}
.input-details-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-images {
display: flex;
gap: 12px;
}
.input-image-item {
flex: 1;
}
.input-thumbnail {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #333;
.copy-icon:hover {
color: #409eff;
}
.metadata-section {
display: flex;
flex-direction: column;
gap: 12px;
gap: 6px;
margin-top: auto;
}
.metadata-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
padding: 4px 0;
}
.label {
font-size: 14px;
color: #9ca3af;
.metadata-item .label {
font-size: 13px;
color: #888;
}
.value {
font-size: 14px;
.metadata-item .value {
font-size: 13px;
color: #fff;
font-weight: 500;
}
.action-section {
margin-top: auto;
padding-top: 20px;
padding-top: 12px;
}
.create-similar-btn {
@@ -2189,46 +2171,50 @@ onActivated(() => {
color: #fff;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.create-similar-btn:hover {
background: #337ecc;
background: #66b1ff;
}
/* 悬浮操作按钮 */
.overlay-actions {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 12px;
z-index: 20;
}
.icon-btn {
width: 40px;
height: 40px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
font-size: 18px;
}
.icon-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
.create-similar-btn:active {
transform: translateY(0);
}
/* 更强制性的样式覆盖 */
:deep(.detail-dialog) {
background: #0a0a0a !important;
}
:deep(.detail-dialog .el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.detail-dialog .el-overlay-dialog) {
background: #0a0a0a !important;
border: none !important;
box-shadow: none !important;
}
/* 全局模态框样式覆盖 */
:deep(.el-dialog__wrapper) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
:deep(.el-overlay) {
background-color: rgba(0, 0, 0, 0.8) !important;
}
/* 回到顶部按钮样式 */
.back-to-top {
position: fixed;