feat: 使用banana模型生成分镜图片,修复数据库列类型问题

- 修改RealAIService.submitTextToImageTask使用nano-banana/nano-banana-hd模型
- 支持根据hdMode参数选择模型(标准/高清)
- 修复数据库列类型:将result_url等字段改为TEXT类型以支持Base64图片
- 添加数据库修复SQL脚本(fix_database_columns.sql, update_database_schema.sql)
- 改进StoryboardVideoService的错误处理和空值检查
- 添加GlobalExceptionHandler全局异常处理
- 优化图片URL提取逻辑,支持url和b64_json两种格式
- 改进响应格式验证,确保data字段不为空
This commit is contained in:
AIGC Developer
2025-11-05 18:18:53 +08:00
parent 0b0ad442a0
commit b5820d9be2
63 changed files with 2207 additions and 341 deletions

View File

@@ -130,7 +130,25 @@
<el-col v-for="item in filteredItems" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6">
<el-card class="work-card" :class="{ selected: selectedIds.has(item.id) }" shadow="hover">
<div class="thumb" @click="multiSelect ? toggleSelect(item.id) : openDetail(item)">
<img :src="item.cover" :alt="item.title" />
<!-- 如果是视频类型且有视频URL使用video元素显示首帧 -->
<video
v-if="item.type === 'video' && item.resultUrl"
:src="item.resultUrl"
class="work-thumbnail-video"
muted
preload="metadata"
@loadedmetadata="onVideoLoaded"
></video>
<!-- 如果有封面图thumbnailUrl使用图片 -->
<img
v-else-if="item.cover && item.cover !== item.resultUrl"
:src="item.cover"
:alt="item.title"
/>
<!-- 否则使用默认占位符 -->
<div v-else class="work-placeholder">
<el-icon><VideoPlay /></el-icon>
</div>
<div class="checker" v-if="multiSelect">
<el-checkbox :model-value="selectedIds.has(item.id)" @change="() => toggleSelect(item.id)" />
@@ -191,7 +209,7 @@
<video
v-if="selectedItem.type === 'video'"
class="detail-video"
:src="selectedItem.cover"
:src="selectedItem.resultUrl || selectedItem.cover"
:poster="selectedItem.cover"
controls
>
@@ -314,7 +332,7 @@
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Star, User, Compass, Document, VideoPlay, Picture, Film, Bell, Setting, Search } from '@element-plus/icons-vue'
import { Star, User, Compass, Document, VideoPlay, Picture, Film, Bell, Setting, Search, MoreFilled } from '@element-plus/icons-vue'
import { getMyWorks } from '@/api/userWorks'
const router = useRouter()
@@ -341,6 +359,28 @@ const loading = ref(false)
const hasMore = ref(true)
const items = ref([])
// 将后端返回的UserWork数据转换为前端需要的格式
const transformWorkData = (work) => {
return {
id: work.id?.toString() || work.taskId || '',
title: work.title || work.prompt || '未命名作品',
cover: work.thumbnailUrl || work.resultUrl || '/images/backgrounds/welcome.jpg',
resultUrl: work.resultUrl || '',
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' ? 'video' : 'image',
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : '未知',
sizeText: work.fileSize || '未知大小',
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : '',
date: work.createdAt ? new Date(work.createdAt).toLocaleDateString('zh-CN') : '',
description: work.description || work.prompt || '',
prompt: work.prompt || '',
duration: work.duration || '',
aspectRatio: work.aspectRatio || '',
quality: work.quality || '',
status: work.status || 'COMPLETED',
overlayText: work.prompt || ''
}
}
const loadList = async () => {
loading.value = true
try {
@@ -352,8 +392,11 @@ const loadList = async () => {
if (response.data.success) {
const data = response.data.data || []
// 转换数据格式
const transformedData = data.map(transformWorkData)
if (page.value === 1) items.value = []
items.value = items.value.concat(data)
items.value = items.value.concat(transformedData)
hasMore.value = data.length === pageSize.value
} else {
throw new Error(response.data.message || '获取作品列表失败')
@@ -545,6 +588,17 @@ const resetFilters = () => {
ElMessage.success('筛选器已重置')
}
// 视频加载元数据后,跳转到第一帧(但不播放)
const onVideoLoaded = (event) => {
const video = event.target
if (video && video.duration) {
// 跳转到第一帧0秒
video.currentTime = 0.1
// 确保视频不播放
video.pause()
}
}
onMounted(() => {
loadList()
})
@@ -799,6 +853,28 @@ onMounted(() => {
.work-card { margin-bottom: 14px; }
.thumb { position: relative; width: 100%; padding-top: 56.25%; overflow: hidden; border-radius: 6px; cursor: pointer; }
.thumb img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
.work-thumbnail-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.work-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 24px;
}
.checker { position: absolute; left: 6px; top: 6px; }
.actions { position: absolute; right: 6px; top: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity .2s ease; }
.thumb:hover .actions { opacity: 1; }