diff --git a/demo/frontend/src/api/payments.js b/demo/frontend/src/api/payments.js index ba7d33b..5089cdc 100644 --- a/demo/frontend/src/api/payments.js +++ b/demo/frontend/src/api/payments.js @@ -47,6 +47,11 @@ export const handleAlipayCallback = (params) => { return api.post('/payments/alipay/callback', params) } +// 噜噜支付(彩虹易支付)API +export const createLuluPayment = (paymentData) => { + return api.post('/payments/lulupay/create', paymentData) +} + // PayPal支付API export const createPayPalPayment = (paymentData) => { return api.post('/payment/paypal/create', paymentData) diff --git a/demo/frontend/src/api/storyboardVideo.js b/demo/frontend/src/api/storyboardVideo.js index 0a97012..a25e8f0 100644 --- a/demo/frontend/src/api/storyboardVideo.js +++ b/demo/frontend/src/api/storyboardVideo.js @@ -7,6 +7,14 @@ export const createStoryboardTask = async (data) => { return api.post('/storyboard-video/create', data) } +/** + * 直接使用上传的分镜图创建视频任务(跳过分镜图生成) + * @param {object} data - 包含 storyboardImage, prompt, aspectRatio, hdMode, duration, referenceImages + */ +export const createVideoDirectTask = async (data) => { + return api.post('/storyboard-video/create-video-direct', data) +} + /** * 获取任务详情 */ diff --git a/demo/frontend/src/api/userWorks.js b/demo/frontend/src/api/userWorks.js index d0acf92..790dd1a 100644 --- a/demo/frontend/src/api/userWorks.js +++ b/demo/frontend/src/api/userWorks.js @@ -6,7 +6,20 @@ export const getMyWorks = (params = {}) => { params: { page: params.page || 0, size: params.size || 10, - includeProcessing: params.includeProcessing !== false // 默认包含正在处理中的作品 + includeProcessing: params.includeProcessing !== false, // 默认包含正在处理中的作品 + workType: params.workType || null // 按作品类型筛选 + } + }) +} + +// 按类型获取我的作品(用于历史记录) +export const getMyWorksByType = (workType, params = {}) => { + return api.get('/works/my-works', { + params: { + page: params.page || 0, + size: params.size || 1000, + includeProcessing: true, + workType: workType // TEXT_TO_VIDEO, IMAGE_TO_VIDEO, STORYBOARD_VIDEO, STORYBOARD_IMAGE } }) } diff --git a/demo/frontend/src/components/PaymentModal.vue b/demo/frontend/src/components/PaymentModal.vue index a9ff665..beac2fb 100644 --- a/demo/frontend/src/components/PaymentModal.vue +++ b/demo/frontend/src/components/PaymentModal.vue @@ -40,7 +40,7 @@
金额
-
${{ amount }}
+
${{ formatAmount(amount) }}
@@ -128,6 +128,12 @@ const loading = ref(false) const currentPaymentId = ref(null) let paymentPollingTimer = null let isPaymentStarted = false // 防止重复调用 + +// 格式化金额,保留两位小数 +const formatAmount = (amount) => { + if (amount === null || amount === undefined) return '0.00' + return Number(amount).toFixed(2) +} let lastPlanType = '' // 记录上一次的套餐类型 // 从orderId中提取套餐类型(如 SUB_standard_xxx -> standard) diff --git a/demo/frontend/src/locales/en.js b/demo/frontend/src/locales/en.js index 84fd24c..d305a7c 100644 --- a/demo/frontend/src/locales/en.js +++ b/demo/frontend/src/locales/en.js @@ -348,6 +348,7 @@ export default { serverError: 'Server error, please try again later', networkError: 'Network error, please check your connection', storyboardImage: 'Storyboard', + referenceImage: 'Reference Image', noStoryboard: 'No storyboard yet', hdMode: 'HD Mode (1080P)', hdCost: 'Costs 20 points when enabled', @@ -361,11 +362,13 @@ export default { progress: 'Progress: {progress}%', generatingStoryboardText: 'Generating storyboard, please wait...', generatingVideoText: 'Generating video, please wait...', + generatingText: 'Generating', startCreating: 'Start creating your first work!', noDescription: 'No description', queuing: 'Queuing', subscribeToSpeed: 'Subscribe to improve generation speed', noResult: 'No result yet', + noStoryboardImage: 'Please wait for storyboard generation to complete, or upload a storyboard image', uploadOrGenerateFirst: 'Please upload or generate storyboard first', uploadOrInputPrompt: 'Please upload storyboard or enter prompt', startGenerateVideo: 'Start Generate Video', @@ -476,6 +479,8 @@ export default { bulkDeleteSuccess: 'Selected items deleted', filtersReset: 'Filters reset', processing: 'Processing...', + queuing: 'Queuing...', + pleaseWait: 'Please wait, video is being generated', noPreview: 'No Preview', videoLoadFailed: 'Video Load Failed', videoFileNotExist: 'Video file may not exist or has been deleted', diff --git a/demo/frontend/src/locales/zh.js b/demo/frontend/src/locales/zh.js index 798673a..cd9a1c9 100644 --- a/demo/frontend/src/locales/zh.js +++ b/demo/frontend/src/locales/zh.js @@ -351,6 +351,7 @@ export default { serverError: '服务器错误,请稍后重试', networkError: '网络错误,请检查网络连接', storyboardImage: '分镜图', + referenceImage: '参考图', noStoryboard: '暂无分镜图', hdMode: '高清模式 (1080P)', hdCost: '开启消耗20积分', @@ -364,11 +365,13 @@ export default { progress: '进度: {progress}%', generatingStoryboardText: '正在生成分镜图,请稍候...', generatingVideoText: '正在生成视频,请稍候...', + generatingText: '生成中', startCreating: '开始创作您的第一个作品吧!', noDescription: '无描述', queuing: '排队中', subscribeToSpeed: '订阅套餐以提升生成速度', noResult: '暂无结果', + noStoryboardImage: '请等待分镜图生成完成,或上传一张分镜图', uploadOrGenerateFirst: '请先上传参考图片或输入描述生成分镜图', uploadOrInputPrompt: '请上传参考图片或输入提示词', startGenerateVideo: '开始生成视频', @@ -490,6 +493,7 @@ export default { filtersReset: '筛选器已重置', processing: '生成中...', queuing: '排队中...', + pleaseWait: '请耐心等待,视频正在生成中', noPreview: '无预览', videoLoadFailed: '视频加载失败', videoFileNotExist: '视频文件可能不存在或已被删除', @@ -589,7 +593,10 @@ export default { qrCodeGenerationError: '二维码生成失败:{message}', pleaseTryAgain: '请重试', refreshPage: '请刷新页面重试', - paymentSuccess: '支付成功!正在更新信息...', + paymentSuccess: '支付成功!积分已到账', + paymentPending: '支付处理中,请稍候...', + paymentCancelled: '支付已取消', + paymentError: '支付处理异常,如有疑问请联系客服', infoUpdated: '信息已更新!', paymentProcessingFailed: '支付成功但处理订单失败,请联系客服', paymentFailed: '支付失败,请重试', diff --git a/demo/frontend/src/views/ApiManagement.vue b/demo/frontend/src/views/ApiManagement.vue index 06ee7fe..e121d1a 100644 --- a/demo/frontend/src/views/ApiManagement.vue +++ b/demo/frontend/src/views/ApiManagement.vue @@ -76,7 +76,25 @@

{{ $t('apiManagement.title') }}

+ +
+

当前配置

+
+ API密钥: + {{ currentMaskedKey || '未配置' }} +
+
+ API端点: + {{ currentApiBaseUrl || '未配置' }} +
+
+ Token过期时间: + {{ apiForm.tokenExpireHours ? apiForm.tokenExpireHours + ' 小时' : '未配置' }} +
+
+
+

修改配置

{ @@ -214,6 +233,7 @@ const loadApiKey = async () => { try { const response = await api.get('/api-key') if (response.data?.maskedKey) { + currentMaskedKey.value = response.data.maskedKey console.log('当前API密钥已配置') } // 加载当前API基础URL @@ -498,6 +518,49 @@ const fetchSystemStats = async () => { max-width: 800px; } +.current-config { + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + border: 1px solid #bae6fd; + border-radius: 12px; + padding: 24px; + margin-bottom: 32px; +} + +.current-config h3 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: #0369a1; +} + +.config-item { + display: flex; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid rgba(186, 230, 253, 0.5); +} + +.config-item:last-child { + border-bottom: none; +} + +.config-label { + font-size: 14px; + color: #64748b; + width: 120px; + flex-shrink: 0; +} + +.config-value { + font-size: 14px; + color: #1e293b; + font-weight: 500; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + background: rgba(255, 255, 255, 0.6); + padding: 4px 12px; + border-radius: 6px; +} + .api-form { background: #f9fafb; padding: 32px; diff --git a/demo/frontend/src/views/ImageToVideoCreate.vue b/demo/frontend/src/views/ImageToVideoCreate.vue index e212259..0efa9e6 100644 --- a/demo/frontend/src/views/ImageToVideoCreate.vue +++ b/demo/frontend/src/views/ImageToVideoCreate.vue @@ -148,9 +148,10 @@
- -
-
+ +
+
+
{{ taskProgress }}%
@@ -262,11 +263,11 @@
- -
-
+ +
+
+
-
@@ -366,7 +367,7 @@ import { useUserStore } from '@/stores/user' import { User, VideoCamera, Star, Setting, SwitchButton, Lock, Document, Warning } from '@element-plus/icons-vue' import { imageToVideoApi } from '@/api/imageToVideo' import { optimizePrompt } from '@/api/promptOptimizer' -import { getProcessingWorks } from '@/api/userWorks' +import { getProcessingWorks, getMyWorksByType } from '@/api/userWorks' import LanguageSwitcher from '@/components/LanguageSwitcher.vue' const router = useRouter() @@ -720,7 +721,7 @@ const startGenerate = async () => { currentTask.value = response.data.data inProgress.value = true taskProgress.value = 0 - taskStatus.value = 'PENDING' + taskStatus.value = 'PROCESSING' // 直接显示生成中 ElMessage.success(t('video.imageToVideo.taskCreatedSuccess')) setTimeout(() => userStore.fetchCurrentUser(), 0) startPollingTask() @@ -776,7 +777,7 @@ const startGenerate = async () => { currentTask.value = response.data.data inProgress.value = true taskProgress.value = 0 - taskStatus.value = 'PENDING' + taskStatus.value = 'PROCESSING' // 直接显示生成中 ElMessage.success(t('video.imageToVideo.taskCreatedSuccess')) @@ -1116,7 +1117,7 @@ const retryTask = async () => { currentTask.value = response.data.data inProgress.value = true taskProgress.value = 0 - taskStatus.value = 'PENDING' + taskStatus.value = 'PROCESSING' // 直接显示生成中 ElMessage.success('重试任务已提交') @@ -1183,6 +1184,10 @@ const deleteWork = async () => { // 处理历史记录URL const processHistoryUrl = (url) => { if (!url) return '' + // data: 协议(Base64 图片等)直接返回,避免被当成相对路径错误加前缀 + if (url.startsWith('data:')) { + return url + } // 如果是相对路径,确保格式正确 if (url.startsWith('/') || !url.startsWith('http')) { if (!url.startsWith('/uploads/') && !url.startsWith('/api/')) { @@ -1192,7 +1197,7 @@ const processHistoryUrl = (url) => { return url } -// 加载历史记录 +// 加载历史记录(从user_works表获取IMAGE_TO_VIDEO类型的作品) const loadHistory = async () => { // 只有登录用户才能查看历史记录 if (!userStore.isAuthenticated) { @@ -1201,20 +1206,24 @@ const loadHistory = async () => { } try { - // 请求全部已完成的任务,不限制数量 - const response = await imageToVideoApi.getTasks(0, 1000) + // 从user_works表获取IMAGE_TO_VIDEO类型的作品 + const response = await getMyWorksByType('IMAGE_TO_VIDEO', { page: 0, size: 1000 }) console.log('历史记录API响应:', response.data) if (response.data && response.data.success) { - // 只显示已完成的任务,不显示失败的任务 - const tasks = (response.data.data || []).filter(task => - task.status === 'COMPLETED' - ) - - // 处理URL,确保相对路径正确 - historyTasks.value = tasks.map(task => ({ - ...task, - resultUrl: task.resultUrl ? processHistoryUrl(task.resultUrl) : null, - firstFrameUrl: task.firstFrameUrl ? processHistoryUrl(task.firstFrameUrl) : null + // 转换数据格式,适配历史记录展示 + const works = response.data.data || [] + + historyTasks.value = works.map(work => ({ + taskId: work.taskId, + prompt: work.prompt, + aspectRatio: work.aspectRatio, + duration: work.duration ? parseInt(work.duration) : 5, + hdMode: work.quality === 'HD', + status: work.status, + resultUrl: work.resultUrl ? processHistoryUrl(work.resultUrl) : null, + firstFrameUrl: work.thumbnailUrl ? processHistoryUrl(work.thumbnailUrl) : null, + createdAt: work.createdAt, + progress: work.status === 'PROCESSING' ? 50 : (work.status === 'COMPLETED' ? 100 : 0) })) console.log('历史记录加载成功:', historyTasks.value.length, '条') @@ -1375,47 +1384,47 @@ const restoreProcessingTask = async () => { // 取最新的一个任务 const work = imageToVideoWorks[0] - // 恢复任务状态 - currentTask.value = { - taskId: work.taskId, - prompt: work.prompt, - aspectRatio: work.aspectRatio, - duration: work.duration, - resultUrl: work.resultUrl, - createdAt: work.createdAt - } - - // 恢复输入参数 - if (work.prompt) { - inputText.value = work.prompt - } - if (work.aspectRatio) { - aspectRatio.value = work.aspectRatio - } - if (work.duration) { - duration.value = String(work.duration) - } - - // 恢复首帧图片(从 thumbnailUrl 或结果中获取) - if (work.thumbnailUrl) { - const imageUrl = processHistoryUrl(work.thumbnailUrl) - firstFrameImage.value = imageUrl - console.log('[Task Restore] 已恢复首帧图片:', work.thumbnailUrl) - // 尝试从URL加载图片并转换为File对象,以便重新生成时可以提交 - try { - const response = await fetch(imageUrl) - const blob = await response.blob() - const fileName = `first_frame_${work.taskId || Date.now()}.${blob.type.split('/')[1] || 'png'}` - firstFrameFile.value = new File([blob], fileName, { type: blob.type }) - console.log('[Task Restore] 首帧图片已转换为文件对象:', fileName) - } catch (error) { - console.warn('[Task Restore] 无法从URL加载首帧图片:', error) - } - } - - // 只有真正在进行中的任务才恢复和显示消息 + // 只有真正在进行中的任务才恢复 const workStatus = work.status || 'PROCESSING' if (workStatus === 'PROCESSING' || workStatus === 'PENDING') { + // 恢复任务状态 + currentTask.value = { + taskId: work.taskId, + prompt: work.prompt, + aspectRatio: work.aspectRatio, + duration: work.duration, + resultUrl: work.resultUrl, + createdAt: work.createdAt + } + + // 恢复输入参数 + if (work.prompt) { + inputText.value = work.prompt + } + if (work.aspectRatio) { + aspectRatio.value = work.aspectRatio + } + if (work.duration) { + duration.value = String(work.duration) + } + + // 恢复首帧图片(从 thumbnailUrl 或结果中获取) + if (work.thumbnailUrl) { + const imageUrl = processHistoryUrl(work.thumbnailUrl) + firstFrameImage.value = imageUrl + console.log('[Task Restore] 已恢复首帧图片:', work.thumbnailUrl) + // 尝试从URL加载图片并转换为File对象,以便重新生成时可以提交 + try { + const imgResponse = await fetch(imageUrl) + const blob = await imgResponse.blob() + const fileName = `first_frame_${work.taskId || Date.now()}.${blob.type.split('/')[1] || 'png'}` + firstFrameFile.value = new File([blob], fileName, { type: blob.type }) + console.log('[Task Restore] 首帧图片已转换为文件对象:', fileName) + } catch (error) { + console.warn('[Task Restore] 无法从URL加载首帧图片:', error) + } + } + inProgress.value = true taskStatus.value = workStatus taskProgress.value = 50 // 初始进度设为50% @@ -1430,7 +1439,7 @@ const restoreProcessingTask = async () => { startPollingTask() return true } else { - // 如果任务已失败或取消,不显示恢复消息,让 checkLastTaskStatus 处理 + // 如果任务已完成、失败或取消,不恢复任何参数 console.log('[Task Skip]', work.taskId, 'Status:', workStatus, '- 不是进行中状态') return false } @@ -1492,13 +1501,19 @@ const checkLastTaskStatus = async () => { } } +// 标记是否已完成首次加载(用于 watch 判断) +const isInitialized = ref(false) + onMounted(async () => { // 强制刷新用户信息,确保获取管理员修改后的最新数据 await userStore.fetchCurrentUser() + // 标记是否从"做同款"或"生视频"进入(有路由参数) + const isFromExternalEntry = !!(route.query.prompt || route.query.referenceImage) + // 处理"做同款"传递的路由参数 - if (route.query.prompt || route.query.referenceImage) { - console.log('[做同款] 接收参数:', route.query) + if (isFromExternalEntry) { + console.log('[做同款/生视频] 接收参数:', route.query) if (route.query.prompt) { inputText.value = route.query.prompt @@ -1513,7 +1528,7 @@ onMounted(async () => { // 处理参考图 if (route.query.referenceImage) { firstFrameImage.value = route.query.referenceImage - console.log('[做同款] 设置参考图:', route.query.referenceImage) + console.log('[做同款/生视频] 设置参考图:', route.query.referenceImage) // 注意:firstFrameFile 为 null,用户需要重新上传或点击生成时会提示 } @@ -1524,17 +1539,62 @@ onMounted(async () => { loadHistory() // 延迟恢复任务,避免与创建任务冲突 + // 注意:如果是从"做同款"或"生视频"进入,不恢复正在进行的任务,让用户开始新任务 setTimeout(async () => { - if (!isCreatingTask.value) { + if (!isCreatingTask.value && !isFromExternalEntry) { const restored = await restoreProcessingTask() // 如果没有恢复进行中的任务,则检查最近一条是否失败 if (!restored) { checkLastTaskStatus() } } + // 标记初始化完成 + isInitialized.value = true }, 500) }) +// 监听路由参数变化(同一页面内跳转时触发) +watch(() => route.query._t, (newT, oldT) => { + // 只在初始化完成后且有新的时间戳时触发 + if (!isInitialized.value || !newT || newT === oldT) { + return + } + + console.log('[路由参数变化] 检测到新参数:', route.query) + + // 停止当前轮询(如果有) + if (stopPolling.value) { + stopPolling.value() + stopPolling.value = null + } + + // 重置状态 + inProgress.value = false + taskId.value = '' + taskStatus.value = '' + resultUrl.value = '' + + // 应用新参数 + if (route.query.prompt) { + inputText.value = route.query.prompt + } + if (route.query.aspectRatio) { + aspectRatio.value = route.query.aspectRatio + } + if (route.query.duration) { + duration.value = route.query.duration + } + if (route.query.referenceImage) { + firstFrameImage.value = route.query.referenceImage + console.log('[路由参数变化] 设置参考图') + } + + // 清除URL中的query参数 + router.replace({ path: route.path }) + + ElMessage.success(t('works.readyToGenerateVideo')) +}) + // 组件卸载时清理资源 onUnmounted(() => { // 停止轮询 diff --git a/demo/frontend/src/views/MyWorks.vue b/demo/frontend/src/views/MyWorks.vue index 7c6b8c5..7e6dd8b 100644 --- a/demo/frontend/src/views/MyWorks.vue +++ b/demo/frontend/src/views/MyWorks.vue @@ -264,8 +264,19 @@
- -
+ +
+
+ +

{{ selectedItem.status === 'PROCESSING' ? t('works.processing') : t('works.queuing') }}

+

{{ t('works.pleaseWait') }}

+
+
+
+
+
+ +

{{ t('works.videoLoadFailed') }}

@@ -284,7 +295,7 @@
+ + - -
+ +
@@ -1102,8 +1120,8 @@ const createSimilar = (item) => { } // 根据作品类别跳转到对应的创建页面,并携带参数 + // 注意:不传递 taskId,因为"做同款"是创建新任务,不是继续原任务 const query = { - taskId: item.taskId, prompt: item.prompt || '', aspectRatio: item.aspectRatio || '', duration: item.duration || '', @@ -1130,6 +1148,9 @@ const createSimilar = (item) => { // 传递分镜图阶段的参考图 if (item.uploadedImages) { query.uploadedImages = item.uploadedImages + console.log('[做同款-分镜图] 传递参考图:', item.uploadedImages) + } else { + console.log('[做同款-分镜图] 无参考图数据, item:', item) } router.push({ path: '/storyboard-video/create', query }) } else if (item.category === '分镜视频') { @@ -1139,6 +1160,14 @@ const createSimilar = (item) => { if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') { query.storyboardImage = item.cover } + // 传递视频提示词(videoPrompt) + if (item.videoPrompt) { + query.prompt = item.videoPrompt + } else if (item.imagePrompt) { + query.prompt = item.imagePrompt + } else if (item.prompt) { + query.prompt = item.prompt + } // 传递视频阶段的参考图(videoReferenceImages),不是分镜图阶段的参考图 if (item.videoReferenceImages) { query.videoReferenceImages = item.videoReferenceImages @@ -1160,7 +1189,8 @@ const goToGenerateVideo = (item) => { const query = { aspectRatio: item.aspectRatio || '', duration: item.duration || '', - hdMode: item.quality === 'HD' ? 'true' : 'false' + hdMode: item.quality === 'HD' ? 'true' : 'false', + _t: Date.now() // 添加时间戳确保路由参数变化被检测到 } // 使用分镜图作为参考图 @@ -2638,6 +2668,114 @@ onActivated(() => { z-index: 20; } +/* 详情弹窗中的生成中状态 */ +.video-processing-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 15; + border-radius: 8px; +} + +.video-processing-overlay .processing-content { + text-align: center; + color: #fff; +} + +.video-processing-overlay .processing-icon { + color: #409eff; + margin-bottom: 16px; +} + +.video-processing-overlay h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; +} + +.video-processing-overlay p { + margin: 0 0 20px 0; + font-size: 14px; + color: #999; +} + +.video-processing-overlay .progress-bar-container { + width: 200px; + height: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + overflow: hidden; + margin: 0 auto; +} + +.video-processing-overlay .progress-bar-animated { + height: 100%; + width: 30%; + background: linear-gradient(90deg, #409eff, #66b1ff); + border-radius: 2px; + animation: progressSlide 1.5s ease-in-out infinite; +} + +@keyframes progressSlide { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(400%); } +} + +/* 视频加载失败样式 */ +.video-error-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 15; + border-radius: 8px; +} + +.video-error-overlay .error-content { + text-align: center; + color: #fff; +} + +.video-error-overlay .error-icon { + color: #f56c6c; + margin-bottom: 16px; +} + +.video-error-overlay h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; +} + +.video-error-overlay p { + margin: 0 0 20px 0; + font-size: 14px; + color: #999; +} + +.video-error-overlay .error-actions { + display: flex; + gap: 12px; + justify-content: center; +} + +/* 生成中预览图样式 */ +.processing-preview { + opacity: 0.5; + filter: blur(2px); +} + .icon-btn { width: 40px; height: 40px; diff --git a/demo/frontend/src/views/Profile.vue b/demo/frontend/src/views/Profile.vue index 890c007..4d1c8ae 100644 --- a/demo/frontend/src/views/Profile.vue +++ b/demo/frontend/src/views/Profile.vue @@ -567,6 +567,14 @@ const createSimilar = (item) => { if (item.cover && item.cover !== '/images/backgrounds/welcome.jpg') { query.storyboardImage = item.cover } + // 传递视频提示词(videoPrompt) + if (item.videoPrompt) { + query.prompt = item.videoPrompt + } else if (item.imagePrompt) { + query.prompt = item.imagePrompt + } else if (item.prompt) { + query.prompt = item.prompt + } // 传递视频阶段的参考图(videoReferenceImages),不是分镜图阶段的参考图 if (item.videoReferenceImages) { query.videoReferenceImages = item.videoReferenceImages @@ -590,7 +598,8 @@ const goToGenerateVideo = (item) => { const query = { aspectRatio: item.aspectRatio || '', duration: item.duration || '', - hdMode: item.quality === 'HD' ? 'true' : 'false' + hdMode: item.quality === 'HD' ? 'true' : 'false', + _t: Date.now() // 添加时间戳确保路由参数变化被检测到 } // 使用分镜图作为参考图 diff --git a/demo/frontend/src/views/StoryboardVideoCreate.vue b/demo/frontend/src/views/StoryboardVideoCreate.vue index 7c73311..3b42227 100644 --- a/demo/frontend/src/views/StoryboardVideoCreate.vue +++ b/demo/frontend/src/views/StoryboardVideoCreate.vue @@ -85,27 +85,18 @@
+ +
+
+ +
+
- - - - - @@ -361,12 +352,9 @@
-
{{ taskStatus === 'PENDING' ? t('video.storyboard.queuing') : t('video.storyboard.generatingStoryboardText') }}
-
-
-
-
-
+
{{ t('video.storyboard.generatingStoryboardText') }}
+
+
50%
@@ -438,12 +426,9 @@
-
{{ taskStatus === 'PENDING' ? t('video.storyboard.queuing') : t('video.storyboard.generatingVideoText') }}
-
-
-
-
-
+
{{ t('video.storyboard.generatingVideoText') }}
+
+
50%
@@ -488,12 +473,9 @@
-
{{ taskStatus === 'PENDING' ? t('video.storyboard.queuing') : t('video.storyboard.generatingStoryboardText') }}
-
-
-
-
-
+
{{ t('video.storyboard.generatingStoryboardText') }}
+
+
50%
@@ -513,12 +495,9 @@
-
{{ taskStatus === 'PENDING' ? t('video.storyboard.queuing') : t('video.storyboard.generatingVideoText') }}
-
-
-
-
-
+
{{ t('video.storyboard.generatingVideoText') }}
+
+
50%
@@ -548,7 +527,7 @@
- {{ t('home.storyboardVideo') }} + {{ currentStep === 'generate' ? t('video.storyboard.storyboardImage') : t('home.storyboardVideo') }} {{ formatDate(task.createdAt) }}
@@ -558,8 +537,7 @@
-
{{ t('video.storyboard.queuing') }}
- +
{{ t('video.storyboard.generatingText') }}
@@ -633,7 +611,7 @@ import { useUserStore } from '@/stores/user' import { createStoryboardTask, getStoryboardTask, getUserStoryboardTasks, retryStoryboardTask } from '@/api/storyboardVideo' import { imageToVideoApi } from '@/api/imageToVideo' import { optimizePrompt } from '@/api/promptOptimizer' -import { getProcessingWorks, getMyWorks } from '@/api/userWorks' +import { getProcessingWorks, getMyWorksByType } from '@/api/userWorks' import LanguageSwitcher from '@/components/LanguageSwitcher.vue' import { useI18n } from 'vue-i18n' @@ -1760,12 +1738,24 @@ const pollTaskStatus = async (taskId) => { generatedImageUrl.value = taskResultUrl isAIGeneratedImage.value = true // AI生成的分镜图 mainReferenceImage.value = taskResultUrl // 同时填入左侧分镜图框 + + // 分镜图生成完成后,填充 videoPrompt 到视频提示词框 + if (task.videoPrompt && task.videoPrompt.trim()) { + videoPrompt.value = task.videoPrompt + console.log('[轮询] 分镜图生成完成,已填充 videoPrompt:', task.videoPrompt.substring(0, 100)) + } + // 只有在分镜图生成阶段才设置 inProgress = false // 如果是视频生成阶段(currentStep === 'video'),保持 inProgress = true // 因为用户可能正在等待视频生成 if (currentStep.value !== 'video') { inProgress.value = false console.log('[轮询] 分镜图生成完成,设置 inProgress = false') + + // 分镜图生成完成后,清空左侧的输入参数,让用户从空白状态开始新任务 + inputText.value = '' + uploadedImages.value = [] + console.log('[轮询] 分镜图生成完成,已清空左侧输入参数') } else { console.log('[轮询] 当前在视频步骤,保持 inProgress =', inProgress.value, ', progress:', taskProgress) } @@ -1773,19 +1763,15 @@ const pollTaskStatus = async (taskId) => { // 不再将生成的分镜图添加到参考图数组中,只显示在右侧预览区域 } - // 每次轮询都尝试填充优化后的提示词 - // 填充 imagePrompt 到生图提示词框(替换用户原始输入) - if (task.imagePrompt && task.imagePrompt.trim()) { - if (inputText.value !== task.imagePrompt) { - inputText.value = task.imagePrompt - console.log('[DEBUG] 已自动填充 imagePrompt:', task.imagePrompt.substring(0, 100)) - } - } - // 填充 videoPrompt 到视频提示词框 - if (task.videoPrompt && task.videoPrompt.trim()) { - if (videoPrompt.value !== task.videoPrompt) { - videoPrompt.value = task.videoPrompt - console.log('[DEBUG] 已自动填充 videoPrompt:', task.videoPrompt.substring(0, 100)) + // 分镜图生成完成后,不再自动填充提示词到左侧输入框 + // 只在视频步骤时填充 videoPrompt + if (currentStep.value === 'video') { + // 填充 videoPrompt 到视频提示词框 + if (task.videoPrompt && task.videoPrompt.trim()) { + if (videoPrompt.value !== task.videoPrompt) { + videoPrompt.value = task.videoPrompt + console.log('[DEBUG] 已自动填充 videoPrompt:', task.videoPrompt.substring(0, 100)) + } } } @@ -1990,12 +1976,23 @@ const startVideoGenerate = async () => { console.log('[生成视频] 第一张参考图前50字符:', referenceImages[0].substring(0, 50)) } + // 确定要使用的分镜图:优先使用AI生成的分镜图,其次使用用户上传的参考图 + const effectiveStoryboardImage = generatedImageUrl.value || mainReferenceImage.value + + if (!effectiveStoryboardImage) { + ElMessage.warning(t('video.storyboard.noStoryboardImage')) + inProgress.value = false + return + } + + console.log('[生成视频] 使用的分镜图:', effectiveStoryboardImage.substring(0, 80) + '...') + const response = await startVideoGeneration(taskId.value, { duration: parseInt(duration.value), aspectRatio: aspectRatio.value, hdMode: hdMode.value, referenceImages: referenceImages, // 只传递视频阶段的参考图 - storyboardImage: generatedImageUrl.value // 传递分镜图URL,用于恢复被覆盖的分镜图 + storyboardImage: effectiveStoryboardImage // 传递分镜图URL(AI生成或用户上传) }) if (response.data && response.data.success) { @@ -2098,15 +2095,22 @@ const startVideoGenerate = async () => { ElMessage.info(t('video.storyboard.startingVideoGenerate')) - const { createStoryboardTask, startVideoGeneration } = await import('@/api/storyboardVideo') + // 使用新的 API 直接创建视频任务(跳过分镜图生成) + const { createVideoDirectTask } = await import('@/api/storyboardVideo') - // 第一步:创建任务(传入上传的分镜图) - const response = await createStoryboardTask({ - prompt: prompt || '根据图片生成分镜', + // 收集视频参考图 + let referenceImages = videoReferenceImages.value + .filter(img => img && img.url) + .map(img => img.url) + + // 直接创建视频任务 + const response = await createVideoDirectTask({ + storyboardImage: imageUrl, // 用户上传的分镜图 + prompt: prompt || '根据分镜图生成视频', aspectRatio: aspectRatio.value, duration: parseInt(duration.value), hdMode: hdMode.value, - imageUrl: imageUrl // 传入上传的分镜图 + referenceImages: referenceImages }) if (response.data && response.data.success) { @@ -2117,23 +2121,9 @@ const startVideoGenerate = async () => { // 设置分镜图URL(用于显示) generatedImageUrl.value = imageUrl - // 第二步:立即开始视频生成 - const videoResponse = await startVideoGeneration(newTaskId, { - duration: parseInt(duration.value), - aspectRatio: aspectRatio.value, - hdMode: hdMode.value, - storyboardImage: imageUrl - }) - - if (videoResponse.data && videoResponse.data.success) { - ElMessage.success(t('video.storyboard.videoTaskStarted')) - refreshUserPoints() - pollTaskStatus(newTaskId) - } else { - const errorMsg = videoResponse.data?.message || t('video.storyboard.videoStartFailed') - handleInsufficientPointsError(errorMsg, errorMsg) - inProgress.value = false - } + ElMessage.success(t('video.storyboard.videoTaskStarted')) + refreshUserPoints() + pollTaskStatus(newTaskId) } else { const errorMsg = response.data?.message || t('video.storyboard.createTaskFailed') handleInsufficientPointsError(errorMsg, errorMsg) @@ -2222,6 +2212,10 @@ const pollVideoTaskStatus = async (taskId) => { // 处理历史记录URL const processHistoryUrl = (url) => { if (!url) return '' + // data: 协议(Base64 图片等)直接返回,避免被当成相对路径错误加前缀 + if (url.startsWith('data:')) { + return url + } // 如果是相对路径,确保格式正确 if (url.startsWith('/') || !url.startsWith('http')) { if (!url.startsWith('/uploads/') && !url.startsWith('/api/')) { @@ -2231,7 +2225,7 @@ const processHistoryUrl = (url) => { return url } -// 加载历史记录(同时加载分镜图和视频两种历史记录) +// 加载历史记录(从user_works表获取分镜图和分镜视频) const loadHistory = async () => { // 只有登录用户才能查看历史记录 if (!userStore.isAuthenticated) { @@ -2244,17 +2238,29 @@ const loadHistory = async () => { // 同时加载分镜图和视频历史记录 // 1. 加载视频历史记录:从 user_works 获取 STORYBOARD_VIDEO 类型的作品 - const videoResponse = await getMyWorks({ page: 0, size: 1000 }) + const videoResponse = await getMyWorksByType('STORYBOARD_VIDEO', { page: 0, size: 1000 }) if (videoResponse.data && videoResponse.data.success) { - const works = (videoResponse.data.data || []).filter(work => - work.workType === 'STORYBOARD_VIDEO' && work.status === 'COMPLETED' - ) + const works = videoResponse.data.data || [] - console.log(`[历史记录-视频] 结果数量: ${works.length}`) + // 过滤掉 resultUrl 是图片的记录(这些是分镜图完成但视频还没生成的) + const videoWorks = works.filter(work => { + // 如果是 PROCESSING/PENDING 状态,保留 + if (work.status === 'PROCESSING' || work.status === 'PENDING') { + return true + } + // 如果是 COMPLETED 状态,只保留 resultUrl 是视频的 + if (work.status === 'COMPLETED' && work.resultUrl) { + const isImage = /\.(png|jpg|jpeg|gif|webp|bmp)(\?|$)/i.test(work.resultUrl) + return !isImage // 只保留视频 + } + return false + }) + + console.log(`[历史记录-视频] 原始数量: ${works.length}, 过滤后: ${videoWorks.length}`) // 转换为任务格式 - 分镜视频用 resultUrl 显示视频 - videoHistoryTasks.value = works.map(work => ({ + videoHistoryTasks.value = videoWorks.map(work => ({ taskId: work.taskId || work.id?.toString(), prompt: work.prompt, resultUrl: work.resultUrl, // 视频URL(用于播放) @@ -2263,6 +2269,7 @@ const loadHistory = async () => { videoResultUrl: work.resultUrl, // 视频URL(用于下载) imageUrl: work.imageUrl, uploadedImages: work.uploadedImages, // 用户上传的参考图,用于做同款 + videoReferenceImages: work.videoReferenceImages, // 视频阶段用户上传的参考图 imagePrompt: work.imagePrompt, videoPrompt: work.videoPrompt, aspectRatio: work.aspectRatio, @@ -2271,37 +2278,36 @@ const loadHistory = async () => { quality: work.quality, status: work.status, workType: work.workType, - createdAt: work.createdAt + createdAt: work.createdAt, + progress: work.status === 'PROCESSING' ? 50 : (work.status === 'COMPLETED' ? 100 : 0) })) } else { videoHistoryTasks.value = [] } - // 2. 加载分镜图历史记录:从 storyboard_video_tasks 获取分镜图任务 - const storyboardResponse = await getUserStoryboardTasks(0, 1000) + // 2. 加载分镜图历史记录:从 user_works 获取 STORYBOARD_IMAGE 类型的作品 + const storyboardResponse = await getMyWorksByType('STORYBOARD_IMAGE', { page: 0, size: 1000 }) if (storyboardResponse.data && storyboardResponse.data.success) { - // 显示有分镜图的任务 - const tasks = (storyboardResponse.data.data || []).filter(task => { - if (task.status !== 'COMPLETED') return false - // 检查 resultUrl 是否存在且是图片 - const resultUrl = task.resultUrl || '' - if (!resultUrl) return false - const isVideo = /\.(mp4|webm|mov|avi)(\?|$)/i.test(resultUrl) - return !isVideo // 只要不是视频就显示 - }) + const works = storyboardResponse.data.data || [] - console.log(`[历史记录-分镜图] 结果数量: ${tasks.length}`) + console.log(`[历史记录-分镜图] 结果数量: ${works.length}`) - // 处理URL - storyboardHistoryTasks.value = tasks.map(task => ({ - ...task, - resultUrl: task.resultUrl && !task.resultUrl.startsWith('data:') && !task.resultUrl.startsWith('http') - ? processHistoryUrl(task.resultUrl) - : task.resultUrl, - imageUrl: task.imageUrl && !task.imageUrl.startsWith('data:') && !task.imageUrl.startsWith('http') - ? processHistoryUrl(task.imageUrl) - : task.imageUrl + // 转换为任务格式 + storyboardHistoryTasks.value = works.map(work => ({ + taskId: work.taskId?.replace('_image', '') || work.id?.toString(), // 移除_image后缀 + prompt: work.prompt, + resultUrl: work.resultUrl ? processHistoryUrl(work.resultUrl) : null, + imageUrl: work.resultUrl ? processHistoryUrl(work.resultUrl) : null, + uploadedImages: work.uploadedImages, + imagePrompt: work.imagePrompt, + aspectRatio: work.aspectRatio, + hdMode: work.quality === 'HD', + quality: work.quality, + status: work.status, + workType: work.workType, + createdAt: work.createdAt, + progress: work.status === 'PROCESSING' ? 50 : (work.status === 'COMPLETED' ? 100 : 0) })) } else { storyboardHistoryTasks.value = [] @@ -2360,39 +2366,31 @@ const createSimilarFromHistory = (task) => { // 清空 Step 1 的 inputText,避免混淆 inputText.value = '' - // 恢复用户上传的参考图片(显示在下方3个上传框中) + // 恢复视频阶段用户上传的参考图片(显示在下方3个上传框中) + // 注意:使用 videoReferenceImages(视频阶段参考图),不是 uploadedImages(分镜图阶段参考图) videoReferenceImages.value = [null, null, null] // 先清空 - // 优先从 uploadedImages 恢复(JSON数组) - if (task.uploadedImages) { + // 优先从 videoReferenceImages 恢复(JSON数组)- 这是视频阶段的参考图 + if (task.videoReferenceImages) { try { - const parsedImages = typeof task.uploadedImages === 'string' - ? JSON.parse(task.uploadedImages) - : task.uploadedImages + const parsedImages = typeof task.videoReferenceImages === 'string' + ? JSON.parse(task.videoReferenceImages) + : task.videoReferenceImages if (Array.isArray(parsedImages)) { parsedImages.filter(img => img && img !== 'null').forEach((img, idx) => { if (idx < 3) { videoReferenceImages.value[idx] = { url: img, file: null, - name: `参考图片${idx + 1}` + name: `视频参考图${idx + 1}` } } }) - console.log('[做同款-视频] 恢复用户上传图片(uploadedImages):', parsedImages.length, '张') + console.log('[做同款-视频] 恢复视频阶段参考图(videoReferenceImages):', parsedImages.length, '张') } } catch (e) { - console.warn('[做同款-视频] 解析 uploadedImages 失败:', e) + console.warn('[做同款-视频] 解析 videoReferenceImages 失败:', e) } } - // 兼容旧字段 imageUrl - if (!videoReferenceImages.value[0] && task.imageUrl) { - videoReferenceImages.value[0] = { - url: task.imageUrl, - file: null, - name: '参考图片' - } - console.log('[做同款-视频] 恢复用户上传图片(imageUrl):', task.imageUrl) - } // 清空 Step 1 的参考图 uploadedImages.value = [] @@ -2473,6 +2471,12 @@ const goToVideoStepWithStoryboard = (task) => { // 切换到视频生成步骤 currentStep.value = 'video' + // 更新 taskId 为选中任务的 ID(重要:确保后续操作针对正确的任务) + if (task.taskId) { + taskId.value = task.taskId + console.log('[生成视频] 切换到任务:', task.taskId) + } + // 设置分镜图 if (task.resultUrl && isImageUrl(task.resultUrl)) { generatedImageUrl.value = task.resultUrl @@ -2490,27 +2494,28 @@ const goToVideoStepWithStoryboard = (task) => { videoPrompt.value = task.prompt } - // 恢复用户上传的参考图片到视频参考图框 + // 恢复视频阶段用户上传的参考图片到视频参考图框 + // 注意:使用 videoReferenceImages(视频阶段参考图),不是 uploadedImages(分镜图阶段参考图) videoReferenceImages.value = [null, null, null] - if (task.uploadedImages) { + if (task.videoReferenceImages) { try { - const parsedImages = typeof task.uploadedImages === 'string' - ? JSON.parse(task.uploadedImages) - : task.uploadedImages + const parsedImages = typeof task.videoReferenceImages === 'string' + ? JSON.parse(task.videoReferenceImages) + : task.videoReferenceImages if (Array.isArray(parsedImages)) { parsedImages.filter(img => img && img !== 'null').forEach((img, idx) => { if (idx < 3) { videoReferenceImages.value[idx] = { url: img, file: null, - name: `参考图片${idx + 1}` + name: `视频参考图${idx + 1}` } } }) - console.log('[生成视频] 恢复用户上传图片:', parsedImages.length, '张') + console.log('[生成视频] 恢复视频阶段参考图:', parsedImages.length, '张') } } catch (e) { - console.warn('[生成视频] 解析 uploadedImages 失败:', e) + console.warn('[生成视频] 解析 videoReferenceImages 失败:', e) } } @@ -2626,7 +2631,7 @@ const formatDate = (dateString) => { // 获取状态文本 const getStatusText = (status) => { const statusMap = { - 'PENDING': t('video.storyboard.statusPending'), + 'PENDING': t('video.storyboard.statusProcessing'), // 统一显示为"生成中" 'PROCESSING': t('video.storyboard.statusProcessing'), 'COMPLETED': t('video.completed'), 'FAILED': t('video.failed'), @@ -2640,7 +2645,7 @@ const getDisplayStatusText = () => { // 分镜图创作页面 if (currentStep.value === 'generate') { if (inProgress.value) { - return taskStatus.value === 'PENDING' ? t('video.storyboard.statusPending') : t('video.storyboard.statusProcessing') + return t('video.storyboard.statusProcessing') // 统一显示为"生成中" } if (generatedImageUrl.value && isAIGeneratedImage.value) { return t('video.storyboard.statusCompleted') // AI生成的分镜图已完成 @@ -2787,10 +2792,17 @@ const restoreProcessingTask = async () => { if (detailResponse.data && detailResponse.data.success) { const taskDetail = detailResponse.data.data + // 判断任务状态,如果已完成则不恢复任何参数 + const detailStatus = taskDetail.status || 'PROCESSING' + if (detailStatus === 'COMPLETED') { + console.log('[恢复任务] 任务已完成,不恢复参数') + return false // 不需要恢复 + } + currentTask.value = taskDetail taskId.value = taskDetail.taskId - // 恢复输入参数 + // 只有 PROCESSING/PENDING 状态才恢复输入参数 if (taskDetail.prompt) { inputText.value = taskDetail.prompt } @@ -2810,43 +2822,19 @@ const restoreProcessingTask = async () => { // 判断任务进度,决定恢复到哪个步骤 const taskProgress = Number(taskDetail.progress) || 0 const taskResultUrl = taskDetail.resultUrl || '' - const detailStatus = taskDetail.status || 'PROCESSING' - // 1. 如果分镜图生成任务已完成(有resultUrl且状态是COMPLETED),不应该恢复 - if (taskResultUrl && isImageUrl(taskResultUrl) && detailStatus === 'COMPLETED') { - // 设置分镜图URL,但不启动任务 - generatedImageUrl.value = taskResultUrl - isAIGeneratedImage.value = true // AI生成的分镜图 - mainReferenceImage.value = taskResultUrl // 填充到分镜图框 - - // 恢复 videoPrompt - if (taskDetail.videoPrompt) { - videoPrompt.value = taskDetail.videoPrompt - } else if (taskDetail.prompt) { - videoPrompt.value = taskDetail.prompt - } - - // 将分镜图添加到上传列表 - const alreadyInList = uploadedImages.value.some(img => img.url === taskResultUrl) - if (!alreadyInList) { - uploadedImages.value.unshift({ - url: taskResultUrl, - file: null, - name: '生成的分镜图' - }) - } - currentStep.value = 'video' // 切换到视频生成步骤 - inProgress.value = false // 不显示"生成中" - console.log('[恢复任务] 分镜图已完成,填充分镜图框和videoPrompt') - return false // 不需要恢复轮询 - } + console.log('[恢复任务] taskResultUrl:', taskResultUrl ? taskResultUrl.substring(0, 100) : '空') + console.log('[恢复任务] isImageUrl:', isImageUrl(taskResultUrl)) + console.log('[恢复任务] detailStatus:', detailStatus) + console.log('[恢复任务] taskProgress:', taskProgress) - // 2. 如果分镜图已生成(有resultUrl),状态是PROCESSING + // 1. 如果分镜图已生成(有resultUrl),状态是PROCESSING // 说明视频正在生成中(分镜图完成后,状态仍为PROCESSING表示视频生成中) if (taskResultUrl && isImageUrl(taskResultUrl) && detailStatus === 'PROCESSING') { generatedImageUrl.value = taskResultUrl isAIGeneratedImage.value = true // AI生成的分镜图 mainReferenceImage.value = taskResultUrl // 填充到分镜图框 + console.log('[恢复任务] 已设置 mainReferenceImage:', taskResultUrl.substring(0, 50)) // 恢复 videoPrompt if (taskDetail.videoPrompt) { @@ -2885,15 +2873,37 @@ const restoreProcessingTask = async () => { pollTaskStatus(taskDetail.taskId) return true } - // 3. 如果分镜图还在生成中(没有resultUrl,状态是PROCESSING) + // 2. 如果分镜图还在生成中(没有resultUrl,状态是PROCESSING) else if (!taskResultUrl && detailStatus === 'PROCESSING') { - // 恢复用户上传的参考图片 - if (taskDetail.imageUrl && isImageUrl(taskDetail.imageUrl)) { + // 恢复用户上传的参考图片(分镜图阶段) + // 优先从 uploadedImages 恢复(JSON数组,支持多张图片) + if (taskDetail.uploadedImages) { + try { + const parsedImages = typeof taskDetail.uploadedImages === 'string' + ? JSON.parse(taskDetail.uploadedImages) + : taskDetail.uploadedImages + if (Array.isArray(parsedImages) && parsedImages.length > 0) { + uploadedImages.value = parsedImages + .filter(img => img && img !== 'null') + .map((url, idx) => ({ + url: url, + file: null, + name: `参考图片${idx + 1}` + })) + console.log('[恢复任务-分镜图生成中] 恢复参考图:', uploadedImages.value.length, '张') + } + } catch (e) { + console.warn('[恢复任务] 解析 uploadedImages 失败:', e) + } + } + // 兼容旧字段 imageUrl(单张图片) + if (uploadedImages.value.length === 0 && taskDetail.imageUrl && isImageUrl(taskDetail.imageUrl)) { uploadedImages.value = [{ url: taskDetail.imageUrl, file: null, name: '参考图片' }] + console.log('[恢复任务-分镜图生成中] 恢复参考图(imageUrl):', taskDetail.imageUrl) } currentStep.value = 'generate' @@ -2904,8 +2914,29 @@ const restoreProcessingTask = async () => { } // 4. 其他情况(PENDING等),也恢复 else if (detailStatus === 'PENDING') { - // 恢复用户上传的参考图片 - if (taskDetail.imageUrl && isImageUrl(taskDetail.imageUrl)) { + // 恢复用户上传的参考图片(分镜图阶段) + // 优先从 uploadedImages 恢复(JSON数组,支持多张图片) + if (taskDetail.uploadedImages) { + try { + const parsedImages = typeof taskDetail.uploadedImages === 'string' + ? JSON.parse(taskDetail.uploadedImages) + : taskDetail.uploadedImages + if (Array.isArray(parsedImages) && parsedImages.length > 0) { + uploadedImages.value = parsedImages + .filter(img => img && img !== 'null') + .map((url, idx) => ({ + url: url, + file: null, + name: `参考图片${idx + 1}` + })) + console.log('[恢复任务-PENDING] 恢复参考图:', uploadedImages.value.length, '张') + } + } catch (e) { + console.warn('[恢复任务] 解析 uploadedImages 失败:', e) + } + } + // 兼容旧字段 imageUrl(单张图片) + if (uploadedImages.value.length === 0 && taskDetail.imageUrl && isImageUrl(taskDetail.imageUrl)) { uploadedImages.value = [{ url: taskDetail.imageUrl, file: null, @@ -2946,13 +2977,25 @@ const restoreProcessingTask = async () => { taskId.value = work.taskId inputText.value = work.prompt || '' - // 恢复参考图片(从 thumbnailUrl 获取) - if (work.thumbnailUrl && isImageUrl(work.thumbnailUrl)) { - uploadedImages.value = [{ - url: work.thumbnailUrl, - file: null, - name: '参考图片' - }] + // 恢复参考图片(优先从 uploadedImages 恢复) + if (work.uploadedImages) { + try { + const parsedImages = typeof work.uploadedImages === 'string' + ? JSON.parse(work.uploadedImages) + : work.uploadedImages + if (Array.isArray(parsedImages) && parsedImages.length > 0) { + uploadedImages.value = parsedImages + .filter(img => img && img !== 'null') + .map((url, idx) => ({ + url: url, + file: null, + name: `参考图片${idx + 1}` + })) + console.log('[恢复任务-兜底] 恢复参考图:', uploadedImages.value.length, '张') + } + } catch (e) { + console.warn('[恢复任务-兜底] 解析 uploadedImages 失败:', e) + } } inProgress.value = true @@ -2980,43 +3023,10 @@ const checkLastTaskStatus = async () => { if (response.data && response.data.success && response.data.data && response.data.data.length > 0) { const lastTask = response.data.data[0] - // 检查是否是"分镜图已完成但视频未生成"的任务 - // 条件:状态是 COMPLETED,有分镜图结果(resultUrl),但没有视频结果(videoUrls) - const hasStoryboard = lastTask.resultUrl && isImageUrl(lastTask.resultUrl) - const hasVideo = lastTask.videoUrls || lastTask.videoUrl - if (lastTask.status === 'COMPLETED' && hasStoryboard && !hasVideo) { - // 这是分镜图已完成的任务,恢复到 Step 2 - const taskResultUrl = processHistoryUrl(lastTask.resultUrl) - generatedImageUrl.value = taskResultUrl - isAIGeneratedImage.value = true // AI生成的分镜图 - mainReferenceImage.value = taskResultUrl // 填充到分镜图框 - - // 恢复 taskId(关键!用于生成视频时识别任务) - taskId.value = lastTask.taskId || '' - - // 恢复 videoPrompt - if (lastTask.videoPrompt) { - videoPrompt.value = lastTask.videoPrompt - } else if (lastTask.prompt) { - videoPrompt.value = lastTask.prompt - } - - // 恢复提示词 - if (lastTask.prompt) { - inputText.value = lastTask.prompt - } - - // 恢复其他参数 - if (lastTask.aspectRatio) { - aspectRatio.value = lastTask.aspectRatio - } - if (lastTask.hdMode !== undefined) { - hdMode.value = lastTask.hdMode - } - - // 切换到视频生成步骤 - currentStep.value = 'video' - console.log('[恢复任务] 分镜图已完成(从历史记录),taskId:', lastTask.taskId, '切换到视频生成步骤') + // 如果最近的任务是完全完成的(COMPLETED 状态),不恢复任何参数 + // 让用户从空白状态开始新任务 + if (lastTask.status === 'COMPLETED') { + console.log('[checkLastTaskStatus] 最近任务已完成,不恢复参数') return } @@ -3536,6 +3546,7 @@ onBeforeUnmount(() => { padding: 0; margin: 8px 0; margin-left: 30px; + position: relative; } .storyboard-steps-svg svg { @@ -3551,6 +3562,25 @@ onBeforeUnmount(() => { opacity: 0.8; } +/* 步骤点击容器 */ +.step-click-container { + position: absolute; + top: 0; + height: 100%; + cursor: pointer; + z-index: 10; +} + +.step-click-container.step-1 { + left: 0; + width: 122px; /* SVG宽度的一半 */ +} + +.step-click-container.step-2 { + left: 122px; + width: 122px; /* SVG宽度的一半 */ +} + /* 生成的图片预览 */ .generated-image-preview { width: 100%; @@ -4784,6 +4814,15 @@ onBeforeUnmount(() => { max-width: 220px; } + .step-click-container.step-1, + .step-click-container.step-2 { + width: 110px; /* 220px的一半 */ + } + + .step-click-container.step-2 { + left: 110px; + } + .step { text-align: left; } @@ -5522,18 +5561,63 @@ onBeforeUnmount(() => { 0% { background-position: 0% 50%; } 100% { background-position: 200% 50%; } } + +/* 视频完成后的操作按钮区域 */ +.result-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + padding: 16px 0; +} + +.result-actions .action-btn { + padding: 10px 24px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.result-actions .action-btn.primary { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: white; +} + +.result-actions .action-btn.primary:hover { + background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +.result-actions .action-icons { + display: flex; + gap: 12px; +} + +.result-actions .icon-btn { + width: 40px; + height: 40px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; +} + +.result-actions .icon-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-2px); +} + +.result-actions .icon-btn svg { + width: 20px; + height: 20px; +} - - - - - - - - - - -C:\Users\UI\Desktop\AIGC\demo>java -jar target/demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev -Error: Unable to access jarfile target/demo-0.0.1-SNAPSHOT.jar - -C:\Users\UI\Desktop\AIGC\demo> \ No newline at end of file diff --git a/demo/frontend/src/views/Subscription.vue b/demo/frontend/src/views/Subscription.vue index bffaef0..05b9e9d 100644 --- a/demo/frontend/src/views/Subscription.vue +++ b/demo/frontend/src/views/Subscription.vue @@ -127,7 +127,7 @@

{{ $t('subscription.free') }}

-
¥{{ membershipPrices.free }}/{{ $t('subscription.perMonth') }}
+
¥{{ formatPrice(membershipPrices.free) }}/{{ $t('subscription.perMonth') }}
 
@@ -160,9 +160,8 @@

{{ $t('subscription.standard') }}

{{ $t('subscription.firstPurchaseDiscount') }}
-
¥{{ membershipPrices.standard }}/{{ Math.floor((membershipPoints.standard || 0) / 30) }}{{ $t('subscription.items') }}
-
{{ membershipPoints.standard }}{{ $t('subscription.points') }}
-
{{ $t('subscription.loading') }}
+
¥{{ formatPrice(membershipPrices.standard) }}/{{ membershipPoints.standard || 0 }}{{ $t('subscription.points') }}
+
 
@@ -202,9 +201,8 @@

{{ $t('subscription.professional') }}

{{ $t('subscription.bestValue') }}
-
¥{{ membershipPrices.premium }}/{{ Math.floor((membershipPoints.premium || 0) / 30) }}{{ $t('subscription.items') }}
-
{{ membershipPoints.premium }}{{ $t('subscription.points') }}
-
{{ $t('subscription.loading') }}
+
¥{{ formatPrice(membershipPrices.premium) }}/{{ membershipPoints.premium || 0 }}{{ $t('subscription.points') }}
+
 
@@ -414,7 +412,7 @@ const subscriptionInfo = ref({ // 套餐名称映射函数:将后端返回的中文套餐名映射到国际化key const mapPlanNameToI18nKey = (planName) => { - if (!planName) return t('subscription.free') + if (!planName || planName.trim() === '') return t('subscription.free') // 移除"会员"、"套餐"等后缀,并转为小写 const planLower = planName.replace(/会员|套餐|版本/g, '').toLowerCase() @@ -636,6 +634,12 @@ const loadMembershipPrices = async () => { } } +// 格式化价格,保留两位小数 +const formatPrice = (price) => { + if (price === null || price === undefined) return '--' + return Number(price).toFixed(2) +} + // 组件挂载时加载数据 onMounted(async () => { // 先加载会员等级价格配置(不需要登录) @@ -662,6 +666,29 @@ onMounted(async () => { // 然后从API获取完整的订阅信息 await loadUserSubscriptionInfo() + + // 检查URL参数,处理支付回调 + const urlParams = new URLSearchParams(window.location.search) + const paymentStatus = urlParams.get('paymentStatus') + const orderId = urlParams.get('orderId') + + if (paymentStatus) { + // 清除URL参数,避免刷新时重复显示 + window.history.replaceState({}, document.title, window.location.pathname) + + if (paymentStatus === 'success') { + ElMessage.success(t('subscription.paymentSuccess')) + // 刷新用户积分信息 + await userStore.fetchCurrentUser() + await loadUserSubscriptionInfo() + } else if (paymentStatus === 'pending') { + ElMessage.info(t('subscription.paymentPending')) + } else if (paymentStatus === 'cancelled') { + ElMessage.warning(t('subscription.paymentCancelled')) + } else if (paymentStatus === 'error') { + ElMessage.error(t('subscription.paymentError')) + } + } } else { // 路由守卫应该已经处理了跳转,但这里作为双重保险 console.warn('用户未登录,路由守卫应该已处理跳转') @@ -1323,10 +1350,10 @@ const createSubscriptionOrder = async (planType, planInfo) => { /* 套餐选择 */ .subscription-packages { - padding: 0 30px 30px; /* 与顶部盒子保持一致的左右留白 */ - margin: 0 auto; /* 居中显示,与上方盒子对齐 */ - max-width: calc(100% - 80px); /* 限制最大宽度,与上方盒子一致 */ - margin-left: 15px; /* 增加左边距,与上方盒子更精确对齐 */ + padding: 0 30px 30px; + margin: 0 auto; + max-width: calc(100% - 80px); + margin-left: 15px; } .subscription-packages .section-title { @@ -1354,9 +1381,9 @@ const createSubscriptionOrder = async (planType, planInfo) => { grid-template-columns: repeat(3, 1fr); gap: 24px; max-width: 1440px; - margin: 0 30px 0 0; /* 右侧留白与左侧 padding(30px) 保持一致 */ + margin: 0 30px 0 0; width: 100%; - align-items: stretch; /* 卡片等高 */ + min-height: calc(100vh - 450px); /* 调整高度 */ } .subscription-packages .package-card { @@ -1366,7 +1393,6 @@ const createSubscriptionOrder = async (planType, planInfo) => { border: 1px solid #333; position: relative; transition: all 0.3s ease; - min-height: 700px; /* 进一步拉长 */ display: flex; flex-direction: column; } diff --git a/demo/frontend/src/views/SystemSettings.vue b/demo/frontend/src/views/SystemSettings.vue index 8cb9556..cfc3f92 100644 --- a/demo/frontend/src/views/SystemSettings.vue +++ b/demo/frontend/src/views/SystemSettings.vue @@ -110,7 +110,7 @@

{{ level.name }}

-

¥{{ level.price || 0 }}/{{ Math.floor((level.resourcePoints || level.pointsBonus || 0) / 30) }}{{ $t('subscription.items') }}

+

¥{{ formatPrice(level.price) }}/{{ Math.floor((level.resourcePoints || level.pointsBonus || 0) / 30) }}{{ $t('subscription.items') }}

{{ level.resourcePoints || level.pointsBonus || 0 }}{{ $t('subscription.points') }}