frontend: Profile detail-dialog background and modal adjustments (disable modal overlay); align detail layout; fix muted playback for history videos in 3 create pages

This commit is contained in:
AIGC Developer
2025-11-11 19:34:19 +08:00
parent ef379bcca6
commit 83bf064bb2
4 changed files with 2345 additions and 132 deletions

View File

@@ -117,8 +117,9 @@
<div class="setting-item">
<label>时长</label>
<select v-model="duration" class="setting-select">
<option value="5">5s</option>
<option value="10">10s</option>
<option value="15">15s</option>
<option value="25">25s</option>
</select>
</div>
@@ -139,10 +140,14 @@
<button
class="generate-btn"
@click="handleGenerateClick"
:disabled="inProgress || (currentStep === 'generate' && !hasUploadedImages && !inputText.trim()) || (currentStep === 'video' && !generatedImageUrl && !hasUploadedImages)"
:disabled="!isAuthenticated || inProgress || (currentStep === 'generate' && !hasUploadedImages && !inputText.trim()) || (currentStep === 'video' && !generatedImageUrl && !hasUploadedImages)"
>
{{ getButtonText() }}
{{ isAuthenticated ? getButtonText() : '请先登录' }}
</button>
<div v-if="!isAuthenticated" class="login-tip-floating">
<p>需要登录后才能提交任务</p>
<button class="login-link-btn" @click="goToLogin">立即登录</button>
</div>
</div>
<!-- 右侧预览区域 -->
@@ -189,6 +194,66 @@
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
</div>
</div>
<!-- 历史记录区域 -->
<div class="history-section" v-if="historyTasks.length > 0">
<div class="history-list">
<div
v-for="task in historyTasks"
:key="task.taskId"
class="history-item"
>
<!-- 顶部状态复选框 -->
<div class="history-status-checkbox" v-if="task.status === 'PENDING' || task.status === 'PROCESSING'">
<input type="checkbox" :checked="true" disabled>
<label>进行中</label>
</div>
<!-- 头部信息 -->
<div class="history-item-header">
<span class="history-type">分镜视频</span>
<span class="history-date">{{ formatDate(task.createdAt) }}</span>
</div>
<!-- 描述文字 -->
<div class="history-prompt">{{ task.prompt || '无描述' }}</div>
<!-- 预览区域 -->
<div class="history-preview">
<div v-if="task.status === 'PENDING' || task.status === 'PROCESSING'" class="history-placeholder">
<div class="queue-text">排队中</div>
<div class="queue-link">订阅套餐以提升生成速度</div>
<button class="cancel-btn" @click="cancelTask(task.taskId)">取消</button>
</div>
<div v-else-if="task.status === 'COMPLETED' && task.resultUrl && !task.resultUrl.startsWith('data:image')" class="history-video-thumbnail" @click="toggleHistoryVideo(task)">
<video
:ref="el => setVideoRef(task.taskId, el)"
:src="processHistoryUrl(task.resultUrl)"
muted
preload="metadata"
@loadedmetadata="handleVideoLoaded"
@error="handleVideoError"
@click.stop="toggleHistoryVideo(task)"
></video>
<div class="play-overlay" v-if="!playingVideos[task.taskId]">
<div class="play-icon"></div>
</div>
</div>
<div v-else-if="task.resultUrl && task.resultUrl.startsWith('data:image')" class="history-image-thumbnail">
<img :src="task.resultUrl" alt="分镜图" />
</div>
<div v-else class="history-placeholder">
<div class="no-result-text">暂无结果</div>
</div>
</div>
<!-- 做同款按钮 -->
<div class="history-actions">
<button class="similar-btn" @click="createSimilarFromHistory(task)">做同款</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -196,19 +261,25 @@
</template>
<script setup>
import { ref, computed, onBeforeUnmount } from 'vue'
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { createStoryboardTask, getStoryboardTask } from '@/api/storyboardVideo'
import { ElMessage, ElLoading } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { createStoryboardTask, getStoryboardTask, getUserStoryboardTasks } from '@/api/storyboardVideo'
import { imageToVideoApi } from '@/api/imageToVideo'
import { optimizePrompt } from '@/api/promptOptimizer'
import { getProcessingWorks } from '@/api/userWorks'
const router = useRouter()
const userStore = useUserStore()
// 计算是否已登录
const isAuthenticated = computed(() => userStore.isAuthenticated)
// 表单数据
const inputText = ref('')
const aspectRatio = ref('16:9')
const duration = ref('5')
const duration = ref('10')
const hdMode = ref(false)
const inProgress = ref(false)
const currentStep = ref('generate') // 'generate' 或 'video'
@@ -239,12 +310,23 @@ const videoPollIntervalId = ref(null) // 视频任务轮询定时器ID
const videoTaskStatus = ref('') // 视频任务状态PROCESSING, COMPLETED, FAILED
const videoResultUrl = ref('') // 视频结果URL
const videoProgress = ref(0) // 视频生成进度
const historyTasks = ref([]) // 历史记录
const playingVideos = ref({}) // 正在播放的视频
const videoRefs = ref({}) // 视频元素引用
// 导航函数
const goBack = () => {
router.back()
}
// 跳转到登录页面
const goToLogin = () => {
router.push({
path: '/login',
query: { redirect: router.currentRoute.value.fullPath }
})
}
const goToTextToVideo = () => {
router.push('/text-to-video/create')
}
@@ -295,6 +377,13 @@ const getButtonText = () => {
const handleGenerateClick = () => {
console.log('handleGenerateClick 被调用,当前步骤:', currentStep.value)
// 检查登录状态
if (!userStore.isAuthenticated) {
ElMessage.warning('请先登录后再提交任务')
goToLogin()
return
}
// 如果已经切换到视频步骤,直接生成视频
if (currentStep.value === 'video') {
startVideoGenerate()
@@ -473,6 +562,8 @@ const optimizePromptHandler = async () => {
}
} else if (error.request) {
errorMessage = '网络错误,请检查网络连接'
} else if (error.code === 'ERR_NETWORK') {
errorMessage = '网络连接错误,请检查您的网络'
} else {
errorMessage = error.message || '优化失败'
}
@@ -487,6 +578,13 @@ const optimizePromptHandler = async () => {
const startGenerate = async () => {
console.log('startGenerate 被调用')
// 检查登录状态
if (!userStore.isAuthenticated) {
ElMessage.warning('请先登录后再提交任务')
goToLogin()
return
}
if (!inputText.value.trim()) {
ElMessage.warning('请输入描述文字')
return
@@ -746,6 +844,13 @@ const urlToFile = async (url, filename) => {
const startVideoGenerate = async () => {
console.log('startVideoGenerate 被调用')
// 检查登录状态
if (!userStore.isAuthenticated) {
ElMessage.warning('请先登录后再提交任务')
goToLogin()
return
}
// 如果有分镜图任务ID从分镜图生成使用新的API
if (taskId.value && generatedImageUrl.value) {
try {
@@ -945,6 +1050,237 @@ const pollVideoTaskStatus = async (taskId) => {
videoPollIntervalId.value = setTimeout(poll, pollInterval)
}
// 处理历史记录URL
const processHistoryUrl = (url) => {
if (!url) return ''
// 如果是相对路径,确保格式正确
if (url.startsWith('/') || !url.startsWith('http')) {
if (!url.startsWith('/uploads/') && !url.startsWith('/api/')) {
return url.startsWith('/') ? url : `/${url}`
}
}
return url
}
// 加载历史记录
const loadHistory = async () => {
// 只有登录用户才能查看历史记录
if (!userStore.isAuthenticated) {
historyTasks.value = []
return
}
try {
const response = await getUserStoryboardTasks(0, 10)
console.log('分镜视频历史记录API响应:', response)
if (response.data && response.data.success) {
// 只显示已完成的任务,排队中和处理中的任务在主创作区显示
const tasks = (response.data.data || []).filter(task =>
task.status === 'COMPLETED'
).slice(0, 10)
console.log('获取到的任务列表:', tasks)
// 处理URL确保相对路径正确但保留Base64图片
historyTasks.value = tasks.map(task => ({
...task,
resultUrl: task.resultUrl && !task.resultUrl.startsWith('data:image')
? processHistoryUrl(task.resultUrl)
: task.resultUrl
}))
console.log('历史记录加载成功:', historyTasks.value.length, '条')
} else {
console.warn('分镜视频历史记录API返回失败:', response.data)
historyTasks.value = []
}
} catch (error) {
console.error('加载历史记录失败:', error)
console.error('错误详情:', error.response || error.message)
historyTasks.value = []
}
}
// 从历史记录创建同款
const createSimilarFromHistory = (task) => {
if (task.prompt) {
inputText.value = task.prompt
}
if (task.aspectRatio) {
aspectRatio.value = task.aspectRatio
}
if (task.hdMode !== undefined) {
hdMode.value = task.hdMode
}
// 如果有分镜图URL尝试加载
if (task.resultUrl && task.resultUrl.startsWith('data:image')) {
generatedImageUrl.value = task.resultUrl
uploadedImages.value = [{
url: task.resultUrl,
file: null,
name: '分镜图'
}]
}
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' })
ElMessage.success('已填充历史记录参数,可以开始生成')
}
// 取消任务
const cancelTask = async (taskId) => {
try {
ElMessage.info('取消功能待实现')
// TODO: 实现取消任务API
} catch (error) {
console.error('取消任务失败:', error)
}
}
// 处理视频加载
const handleVideoLoaded = (event) => {
// 视频元数据加载完成,可以显示缩略图
const video = event.target
video.currentTime = 1 // 跳转到第1秒作为缩略图
// 监听播放状态
video.addEventListener('play', () => {
const taskId = Object.keys(videoRefs.value).find(id => videoRefs.value[id] === video)
if (taskId) {
playingVideos.value[taskId] = true
}
})
video.addEventListener('pause', () => {
const taskId = Object.keys(videoRefs.value).find(id => videoRefs.value[id] === video)
if (taskId) {
playingVideos.value[taskId] = false
}
})
video.addEventListener('ended', () => {
const taskId = Object.keys(videoRefs.value).find(id => videoRefs.value[id] === video)
if (taskId) {
playingVideos.value[taskId] = false
video.currentTime = 1 // 播放结束后回到第1秒作为缩略图
}
})
}
// 处理视频加载错误
const handleVideoError = (event) => {
console.error('历史记录视频加载失败:', event.target.src)
// 可以在这里添加错误处理逻辑,比如显示占位图
}
// 设置视频引用
const setVideoRef = (taskId, el) => {
if (el) {
videoRefs.value[taskId] = el
}
}
// 切换历史记录视频播放
const toggleHistoryVideo = (task) => {
const video = videoRefs.value[task.taskId]
if (!video) return
if (playingVideos.value[task.taskId]) {
video.pause()
playingVideos.value[task.taskId] = false
} else {
// 解除静音并设置音量,确保有声音
try {
video.muted = false
video.volume = 1
video.play()
} catch (_) {
video.play()
}
playingVideos.value[task.taskId] = true
}
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}${month}${day}${hours}:${minutes}`
}
// 组件挂载时加载历史记录
// 监听登录状态变化,登录后自动加载历史记录
watch(() => userStore.isAuthenticated, (isAuth) => {
if (isAuth) {
loadHistory()
restoreProcessingTask()
} else {
historyTasks.value = []
}
})
// 恢复正在进行中的任务
const restoreProcessingTask = async () => {
if (!userStore.isAuthenticated) {
return
}
try {
const response = await getProcessingWorks()
if (response.data && response.data.success && response.data.data) {
const works = response.data.data
// 只恢复分镜视频类型的任务(包括 PROCESSING 和 PENDING 状态)
const storyboardWorks = works.filter(work => work.workType === 'STORYBOARD_VIDEO')
if (storyboardWorks.length > 0) {
// 取最新的一个任务
const work = storyboardWorks[0]
// 恢复任务状态
currentTask.value = {
taskId: work.taskId,
prompt: work.prompt,
aspectRatio: work.aspectRatio,
duration: work.duration,
resultUrl: work.resultUrl,
createdAt: work.createdAt
}
taskId.value = work.taskId
// 恢复输入参数
if (work.prompt) {
inputText.value = work.prompt
}
if (work.aspectRatio) {
aspectRatio.value = work.aspectRatio
}
if (work.duration) {
duration.value = work.duration || '10'
}
inProgress.value = true
taskStatus.value = work.status || 'PROCESSING'
console.log('恢复正在进行中的任务:', work.taskId, '状态:', work.status)
ElMessage.info('检测到未完成的任务,继续处理中...')
// 开始轮询任务状态
pollStoryboardTask(work.taskId)
}
}
} catch (error) {
console.error('恢复任务失败:', error)
}
}
onMounted(() => {
loadHistory()
restoreProcessingTask()
})
// 组件卸载时清理轮询
onBeforeUnmount(() => {
if (pollIntervalId.value) {
@@ -1576,12 +1912,65 @@ onBeforeUnmount(() => {
box-shadow: none;
}
.login-tip-floating {
margin-top: 12px;
text-align: center;
padding: 12px;
background: rgba(59, 130, 246, 0.1);
border-radius: 8px;
border: 1px solid rgba(59, 130, 246, 0.3);
}
.login-tip-floating p {
color: #9ca3af;
font-size: 13px;
margin: 0 0 8px 0;
}
.login-link-btn {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.login-link-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
/* 右侧面板 */
.right-panel {
background: #0a0a0a;
padding: 32px;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
/* 自定义右侧面板滚动条样式 */
.right-panel::-webkit-scrollbar {
width: 8px;
}
.right-panel::-webkit-scrollbar-track {
background: #1a1a1a;
border-radius: 4px;
}
.right-panel::-webkit-scrollbar-thumb {
background: #3b82f6;
border-radius: 4px;
}
.right-panel::-webkit-scrollbar-thumb:hover {
background: #2563eb;
}
.preview-area {
@@ -1589,6 +1978,7 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
gap: 20px;
min-height: min-content;
}
.status-checkbox {
@@ -1624,6 +2014,11 @@ onBeforeUnmount(() => {
overflow: hidden; /* 防止内容溢出 */
min-height: 0; /* 允许flex子项缩小 */
position: relative; /* 为绝对定位的子元素提供定位参考 */
width: 80%;
max-width: 1000px;
margin-left: 0;
margin-right: auto;
align-self: flex-start;
}
.preview-content:hover {
@@ -1631,15 +2026,21 @@ onBeforeUnmount(() => {
}
.preview-placeholder {
text-align: center;
padding: 40px;
width: 100%;
min-height: 400px;
background: #1a1a1a;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
padding: 60px 40px;
}
.placeholder-text {
font-size: 18px;
color: #6b7280;
font-size: 20px;
color: #9ca3af;
font-weight: 500;
line-height: 1.5;
line-height: 1.6;
}
/* 预览图片 */
@@ -1838,6 +2239,223 @@ onBeforeUnmount(() => {
text-align: left;
}
}
/* 历史记录区域 */
.history-section {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #2a2a2a;
}
.history-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.history-item {
background: transparent;
border: none;
padding: 0;
transition: all 0.2s ease;
width: 80%;
max-width: 1000px;
margin-left: 0;
margin-right: auto;
}
.history-status-checkbox {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.history-status-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: default;
accent-color: #3b82f6;
}
.history-status-checkbox label {
color: #e5e7eb;
font-size: 14px;
font-weight: 500;
cursor: default;
}
.history-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.history-type {
color: #3b82f6;
font-size: 14px;
font-weight: 600;
}
.history-date {
color: #9ca3af;
font-size: 13px;
}
.history-prompt {
color: #e5e7eb;
font-size: 14px;
line-height: 1.6;
margin-bottom: 16px;
white-space: pre-wrap;
word-wrap: break-word;
}
.history-preview {
width: 100%;
aspect-ratio: 16/9;
background: #000;
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
position: relative;
}
.history-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #000;
color: #fff;
gap: 12px;
}
.queue-text {
font-size: 18px;
font-weight: 600;
color: #fff;
}
.queue-link {
font-size: 14px;
color: #3b82f6;
cursor: pointer;
text-decoration: none;
}
.queue-link:hover {
color: #60a5fa;
text-decoration: underline;
}
.cancel-btn {
padding: 8px 24px;
background: transparent;
color: #fff;
border: 1px solid #fff;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
margin-top: 8px;
transition: all 0.2s ease;
}
.cancel-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.no-result-text {
font-size: 14px;
color: #6b7280;
}
.history-video-thumbnail {
width: 100%;
height: 100%;
position: relative;
cursor: pointer;
overflow: hidden;
}
.history-video-thumbnail video {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
display: block;
}
.history-image-thumbnail {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.history-image-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.history-video-thumbnail:hover .play-overlay {
opacity: 1;
}
.play-icon {
width: 48px;
height: 48px;
background: rgba(59, 130, 246, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
padding-left: 4px;
}
.history-actions {
display: flex;
justify-content: flex-start;
margin-top: 0;
}
.similar-btn {
padding: 10px 20px;
background: #2a2a2a;
color: #e5e7eb;
border: 1px solid #3a3a3a;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.similar-btn:hover {
background: #3a3a3a;
border-color: #4a4a4a;
}
</style>