@@ -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 @@
-
¥{{ 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') }}