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

@@ -81,18 +81,38 @@
<section class="published-section">
<h3 class="section-title">已发布</h3>
<div class="video-grid">
<div class="video-item" v-for="(video, index) in videos" :key="index">
<div class="video-thumbnail">
<div class="video-item" v-for="(video, index) in videos" :key="video.id || index" v-loading="loading">
<div class="video-thumbnail" @click="goToCreate(video)">
<div class="thumbnail-image">
<div class="figure"></div>
<div class="text-overlay">What Does it Mean To You</div>
<!-- 如果是视频类型且有视频URL使用video元素显示首帧 -->
<video
v-if="video.type === 'video' && video.resultUrl"
:src="video.resultUrl"
class="video-cover-img"
muted
preload="metadata"
@loadedmetadata="onVideoLoaded"
></video>
<!-- 如果有封面图thumbnailUrl使用图片 -->
<img
v-else-if="video.cover && video.cover !== video.resultUrl"
:src="video.cover"
:alt="video.title"
class="video-cover-img"
/>
<!-- 否则使用占位符 -->
<div v-else class="figure"></div>
<div class="text-overlay" v-if="video.text">{{ video.text }}</div>
</div>
<div class="video-action">
<el-button v-if="index === 0" type="primary" size="small">做同款</el-button>
<el-button v-if="index === 0" type="primary" size="small" @click.stop="goToCreate(video)">做同款</el-button>
<span v-else class="director-text">DIRECTED BY VANNOCENT</span>
</div>
</div>
</div>
<div v-if="!loading && videos.length === 0" class="empty-works">
<div class="empty-text">暂无作品开始创作吧</div>
</div>
</div>
</section>
</main>
@@ -145,6 +165,7 @@ import {
Picture,
Film
} from '@element-plus/icons-vue'
import { getMyWorks } from '@/api/userWorks'
const router = useRouter()
const userStore = useUserStore()
@@ -154,7 +175,8 @@ const showUserMenu = ref(false)
const userStatusRef = ref(null)
// 视频数据
const videos = ref(Array(6).fill({}))
const videos = ref([])
const loading = ref(false)
// 计算菜单位置
const menuStyle = computed(() => {
@@ -251,6 +273,44 @@ const logout = async () => {
}
}
// 将后端返回的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',
text: work.prompt || '',
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : '未知',
size: work.fileSize || '未知大小',
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : ''
}
}
// 加载用户作品列表
const loadVideos = async () => {
loading.value = true
try {
const response = await getMyWorks({
page: 0,
size: 6 // 只加载前6个作品
})
if (response.data.success) {
const data = response.data.data || []
// 转换数据格式
videos.value = data.map(transformWorkData)
} else {
console.error('获取作品列表失败:', response.data.message)
}
} catch (error) {
console.error('加载作品列表失败:', error)
} finally {
loading.value = false
}
}
// 点击外部关闭菜单
const handleClickOutside = (event) => {
const userStatus = event.target.closest('.user-status')
@@ -259,8 +319,31 @@ const handleClickOutside = (event) => {
}
}
// 跳转到作品详情或创作页面
const goToCreate = (video) => {
if (video && video.category === '文生视频') {
router.push('/text-to-video/create')
} else if (video && video.category === '图生视频') {
router.push('/image-to-video/create')
} else {
router.push('/text-to-video/create')
}
}
// 视频加载元数据后,跳转到第一帧(但不播放)
const onVideoLoaded = (event) => {
const video = event.target
if (video && video.duration) {
// 跳转到第一帧0秒
video.currentTime = 0.1
// 确保视频不播放
video.pause()
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
loadVideos()
})
onUnmounted(() => {
@@ -651,6 +734,20 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.video-cover-img {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.video-cover-img video {
width: 100%;
height: 100%;
object-fit: cover;
}
.figure {
@@ -673,6 +770,17 @@ onUnmounted(() => {
border-radius: 50%;
}
.empty-works {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #999;
}
.empty-text {
font-size: 16px;
}
.text-overlay {
position: absolute;
top: 20px;