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:
@@ -37,25 +37,14 @@
|
|||||||
<div class="image-input-section">
|
<div class="image-input-section">
|
||||||
<div class="image-upload-area">
|
<div class="image-upload-area">
|
||||||
<div class="upload-box" @click="uploadFirstFrame">
|
<div class="upload-box" @click="uploadFirstFrame">
|
||||||
<div class="upload-icon">+</div>
|
<div v-if="!firstFrameImage" class="upload-placeholder">
|
||||||
<div class="upload-text">首帧</div>
|
<div class="upload-icon">+</div>
|
||||||
</div>
|
<div class="upload-text">首帧</div>
|
||||||
<div class="arrow-icon">↔</div>
|
</div>
|
||||||
<div class="upload-box optional" @click="uploadLastFrame">
|
<div v-else class="upload-preview">
|
||||||
<div class="upload-icon">+</div>
|
<img :src="firstFrameImage" alt="首帧" />
|
||||||
<div class="upload-text">尾帧 (可选)</div>
|
<button class="remove-btn" @click.stop="removeFirstFrame">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 已上传的图片预览 -->
|
|
||||||
<div class="image-preview" v-if="firstFrameImage || lastFrameImage">
|
|
||||||
<div class="preview-item" v-if="firstFrameImage">
|
|
||||||
<img :src="firstFrameImage" alt="首帧" />
|
|
||||||
<button class="remove-btn" @click="removeFirstFrame">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="preview-item" v-if="lastFrameImage">
|
|
||||||
<img :src="lastFrameImage" alt="尾帧" />
|
|
||||||
<button class="remove-btn" @click="removeLastFrame">×</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +81,8 @@
|
|||||||
<label>时长</label>
|
<label>时长</label>
|
||||||
<select v-model="duration" class="setting-select">
|
<select v-model="duration" class="setting-select">
|
||||||
<option value="10">10s</option>
|
<option value="10">10s</option>
|
||||||
|
<option value="15">15s</option>
|
||||||
|
<option value="25">25s</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,9 +97,17 @@
|
|||||||
|
|
||||||
<!-- 生成按钮 -->
|
<!-- 生成按钮 -->
|
||||||
<div class="generate-section">
|
<div class="generate-section">
|
||||||
<button class="generate-btn" @click="startGenerate">
|
<button
|
||||||
开始生成
|
class="generate-btn"
|
||||||
|
@click="startGenerate"
|
||||||
|
:disabled="!isAuthenticated"
|
||||||
|
>
|
||||||
|
{{ isAuthenticated ? '开始生成' : '请先登录' }}
|
||||||
</button>
|
</button>
|
||||||
|
<div v-if="!isAuthenticated" class="login-tip">
|
||||||
|
<p>需要登录后才能提交任务</p>
|
||||||
|
<button class="login-link-btn" @click="goToLogin">立即登录</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,7 +181,6 @@
|
|||||||
<!-- 操作按钮区域 -->
|
<!-- 操作按钮区域 -->
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<button class="action-btn primary" @click="createSimilar">做同款</button>
|
<button class="action-btn primary" @click="createSimilar">做同款</button>
|
||||||
<button class="action-btn primary" @click="submitWork">投稿</button>
|
|
||||||
<div class="action-icons">
|
<div class="action-icons">
|
||||||
<button class="icon-btn" @click="downloadVideo" title="下载视频">
|
<button class="icon-btn" @click="downloadVideo" title="下载视频">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
@@ -230,6 +228,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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" 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.firstFrameUrl" class="history-image-thumbnail">
|
||||||
|
<img :src="task.firstFrameUrl" 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,17 +322,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onUnmounted, computed } from 'vue'
|
import { ref, reactive, onUnmounted, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElLoading } from 'element-plus'
|
import { ElMessage, ElLoading } from 'element-plus'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { User, VideoCamera, Star, Setting, SwitchButton } from '@element-plus/icons-vue'
|
import { User, VideoCamera, Star, Setting, SwitchButton } from '@element-plus/icons-vue'
|
||||||
import { imageToVideoApi } from '@/api/imageToVideo'
|
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||||
import { optimizePrompt } from '@/api/promptOptimizer'
|
import { optimizePrompt } from '@/api/promptOptimizer'
|
||||||
|
import { getProcessingWorks } from '@/api/userWorks'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 计算是否已登录
|
||||||
|
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
const aspectRatio = ref('16:9')
|
const aspectRatio = ref('16:9')
|
||||||
@@ -296,6 +358,9 @@ const stopPolling = ref(null)
|
|||||||
const showInProgress = ref(false)
|
const showInProgress = ref(false)
|
||||||
const watermarkOption = ref('without')
|
const watermarkOption = ref('without')
|
||||||
const optimizingPrompt = ref(false) // 优化提示词状态
|
const optimizingPrompt = ref(false) // 优化提示词状态
|
||||||
|
const historyTasks = ref([]) // 历史记录
|
||||||
|
const playingVideos = ref({}) // 正在播放的视频
|
||||||
|
const videoRefs = ref({}) // 视频元素引用
|
||||||
|
|
||||||
// 用户菜单相关
|
// 用户菜单相关
|
||||||
const showUserMenu = ref(false)
|
const showUserMenu = ref(false)
|
||||||
@@ -319,6 +384,14 @@ const goBack = () => {
|
|||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转到登录页面
|
||||||
|
const goToLogin = () => {
|
||||||
|
router.push({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: router.currentRoute.value.fullPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const goToTextToVideo = () => {
|
const goToTextToVideo = () => {
|
||||||
router.push('/text-to-video/create')
|
router.push('/text-to-video/create')
|
||||||
}
|
}
|
||||||
@@ -433,6 +506,13 @@ const removeLastFrame = () => {
|
|||||||
|
|
||||||
// 开始生成视频
|
// 开始生成视频
|
||||||
const startGenerate = async () => {
|
const startGenerate = async () => {
|
||||||
|
// 检查登录状态
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
ElMessage.warning('请先登录后再提交任务')
|
||||||
|
goToLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否已有任务在进行中
|
// 检查是否已有任务在进行中
|
||||||
if (inProgress.value) {
|
if (inProgress.value) {
|
||||||
ElMessage.warning('已有任务在进行中,请等待完成或取消当前任务')
|
ElMessage.warning('已有任务在进行中,请等待完成或取消当前任务')
|
||||||
@@ -450,11 +530,6 @@ const startGenerate = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证描述文字长度
|
|
||||||
if (inputText.value.trim().length > 500) {
|
|
||||||
ElMessage.error('描述文字不能超过500个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
const loading = ElLoading.service({
|
const loading = ElLoading.service({
|
||||||
@@ -467,7 +542,6 @@ const startGenerate = async () => {
|
|||||||
// 准备请求参数
|
// 准备请求参数
|
||||||
const params = {
|
const params = {
|
||||||
firstFrame: firstFrameFile.value,
|
firstFrame: firstFrameFile.value,
|
||||||
lastFrame: lastFrameFile.value,
|
|
||||||
prompt: inputText.value.trim(),
|
prompt: inputText.value.trim(),
|
||||||
aspectRatio: aspectRatio.value,
|
aspectRatio: aspectRatio.value,
|
||||||
duration: parseInt(duration.value),
|
duration: parseInt(duration.value),
|
||||||
@@ -660,6 +734,8 @@ const optimizePromptHandler = async () => {
|
|||||||
}
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
errorMessage = '网络错误,请检查网络连接'
|
errorMessage = '网络错误,请检查网络连接'
|
||||||
|
} else if (error.code === 'ERR_NETWORK') {
|
||||||
|
errorMessage = '网络连接错误,请检查您的网络'
|
||||||
} else {
|
} else {
|
||||||
errorMessage = error.message || '优化失败'
|
errorMessage = error.message || '优化失败'
|
||||||
}
|
}
|
||||||
@@ -703,17 +779,7 @@ const retryTask = () => {
|
|||||||
startGenerate()
|
startGenerate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投稿功能
|
// 投稿功能(已移除按钮,保留函数已删除)
|
||||||
const submitWork = () => {
|
|
||||||
if (!currentTask.value) {
|
|
||||||
ElMessage.error('没有可投稿的作品')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里可以调用投稿API
|
|
||||||
ElMessage.success('投稿成功!')
|
|
||||||
console.log('投稿作品:', currentTask.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除作品
|
// 删除作品
|
||||||
const deleteWork = () => {
|
const deleteWork = () => {
|
||||||
@@ -737,6 +803,213 @@ const deleteWork = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理历史记录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 imageToVideoApi.getTasks(0, 5)
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
// 只显示已完成的任务,排队中和处理中的任务在主创作区显示
|
||||||
|
const tasks = (response.data.data || []).filter(task =>
|
||||||
|
task.status === 'COMPLETED'
|
||||||
|
).slice(0, 5)
|
||||||
|
|
||||||
|
// 处理URL,确保相对路径正确
|
||||||
|
historyTasks.value = tasks.map(task => ({
|
||||||
|
...task,
|
||||||
|
resultUrl: task.resultUrl ? processHistoryUrl(task.resultUrl) : null,
|
||||||
|
firstFrameUrl: task.firstFrameUrl ? processHistoryUrl(task.firstFrameUrl) : null
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('历史记录加载成功:', historyTasks.value.length, '条')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载历史记录失败:', error)
|
||||||
|
historyTasks.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从历史记录创建同款
|
||||||
|
const createSimilarFromHistory = (task) => {
|
||||||
|
if (task.prompt) {
|
||||||
|
inputText.value = task.prompt
|
||||||
|
}
|
||||||
|
if (task.aspectRatio) {
|
||||||
|
aspectRatio.value = task.aspectRatio
|
||||||
|
}
|
||||||
|
if (task.duration) {
|
||||||
|
duration.value = task.duration
|
||||||
|
}
|
||||||
|
if (task.hdMode !== undefined) {
|
||||||
|
hdMode.value = task.hdMode
|
||||||
|
}
|
||||||
|
// 如果有首帧图片URL,尝试加载
|
||||||
|
if (task.firstFrameUrl) {
|
||||||
|
firstFrameImage.value = task.firstFrameUrl
|
||||||
|
}
|
||||||
|
// 滚动到顶部
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听登录状态变化,登录后自动加载历史记录
|
||||||
|
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 imageToVideoWorks = works.filter(work => work.workType === 'IMAGE_TO_VIDEO')
|
||||||
|
|
||||||
|
if (imageToVideoWorks.length > 0) {
|
||||||
|
// 取最新的一个任务
|
||||||
|
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 = work.duration || '10'
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress.value = true
|
||||||
|
taskStatus.value = work.status || 'PROCESSING'
|
||||||
|
taskProgress.value = 50 // 初始进度设为50%
|
||||||
|
|
||||||
|
console.log('恢复正在进行中的任务:', work.taskId, '状态:', work.status)
|
||||||
|
ElMessage.info('检测到未完成的任务,继续处理中...')
|
||||||
|
|
||||||
|
// 开始轮询任务状态
|
||||||
|
startPollingTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('恢复任务失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载历史记录
|
||||||
|
onMounted(() => {
|
||||||
|
loadHistory()
|
||||||
|
restoreProcessingTask()
|
||||||
|
})
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
// 组件卸载时清理资源
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 停止轮询
|
// 停止轮询
|
||||||
@@ -990,6 +1263,8 @@ onUnmounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-box:hover {
|
.upload-box:hover {
|
||||||
@@ -1001,6 +1276,15 @@ onUnmounted(() => {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-icon {
|
.upload-icon {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
@@ -1014,46 +1298,49 @@ onUnmounted(() => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.arrow-icon {
|
.arrow-icon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-item {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn {
|
.remove-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
width: 24px;
|
width: 28px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
z-index: 10;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.9);
|
||||||
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文本输入区域 */
|
/* 文本输入区域 */
|
||||||
@@ -1208,12 +1495,65 @@ onUnmounted(() => {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-tip {
|
||||||
|
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 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 {
|
.right-panel {
|
||||||
background: #0a0a0a;
|
background: #0a0a0a;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.preview-area {
|
||||||
@@ -1221,6 +1561,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
min-height: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-checkbox {
|
.status-checkbox {
|
||||||
@@ -1253,6 +1594,11 @@ onUnmounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-content:hover {
|
.preview-content:hover {
|
||||||
@@ -1260,15 +1606,22 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-placeholder {
|
.preview-placeholder {
|
||||||
text-align: center;
|
width: 100%;
|
||||||
padding: 40px;
|
min-height: 400px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
color: #6b7280;
|
color: #9ca3af;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1430,6 +1783,10 @@ onUnmounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 生成中状态 */
|
/* 生成中状态 */
|
||||||
@@ -1696,6 +2053,223 @@ onUnmounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 历史记录区域 */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
<!-- 左侧导航栏 -->
|
<!-- 左侧导航栏 -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="logo">logo</div>
|
<div class="logo">
|
||||||
|
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 导航菜单 -->
|
<!-- 导航菜单 -->
|
||||||
<nav class="nav-menu">
|
<nav class="nav-menu">
|
||||||
@@ -67,8 +69,7 @@
|
|||||||
<section class="profile-section">
|
<section class="profile-section">
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<img v-if="userInfo.avatar" :src="userInfo.avatar" alt="avatar" class="avatar-image" />
|
<img src="/images/backgrounds/avatar-default.svg" alt="avatar" class="avatar-image" />
|
||||||
<div v-else class="avatar-icon"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-details">
|
<div class="user-details">
|
||||||
<h2 class="username">{{ userInfo.nickname || userInfo.username || '未设置用户名' }}</h2>
|
<h2 class="username">{{ userInfo.nickname || userInfo.username || '未设置用户名' }}</h2>
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
<h3 class="section-title">已发布</h3>
|
<h3 class="section-title">已发布</h3>
|
||||||
<div class="video-grid">
|
<div class="video-grid">
|
||||||
<div class="video-item" v-for="(video, index) in videos" :key="video.id || index" v-loading="loading">
|
<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="video-thumbnail" @click="openDetail(video)">
|
||||||
<div class="thumbnail-image">
|
<div class="thumbnail-image">
|
||||||
<!-- 如果是视频类型且有视频URL,使用video元素显示首帧 -->
|
<!-- 如果是视频类型且有视频URL,使用video元素显示首帧 -->
|
||||||
<video
|
<video
|
||||||
@@ -103,10 +104,9 @@
|
|||||||
/>
|
/>
|
||||||
<!-- 否则使用占位符 -->
|
<!-- 否则使用占位符 -->
|
||||||
<div v-else class="figure"></div>
|
<div v-else class="figure"></div>
|
||||||
<div class="text-overlay" v-if="video.text">{{ video.text }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="video-action">
|
<div class="video-action">
|
||||||
<el-button v-if="index === 0" type="primary" size="small" @click.stop="goToCreate(video)">做同款</el-button>
|
<el-button v-if="index === 0" type="primary" size="small" @click.stop="createSimilar(video)">做同款</el-button>
|
||||||
<span v-else class="director-text">DIRECTED BY VANNOCENT</span>
|
<span v-else class="director-text">DIRECTED BY VANNOCENT</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,6 +119,124 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 作品详情弹窗(与“我的作品”一致风格的精简版) -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
:title="selectedItem?.title"
|
||||||
|
width="60%"
|
||||||
|
:before-close="handleClose"
|
||||||
|
class="detail-dialog"
|
||||||
|
:modal="false"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:close-on-press-escape="true"
|
||||||
|
>
|
||||||
|
<div class="detail-content" v-if="selectedItem">
|
||||||
|
<div class="detail-left">
|
||||||
|
<div class="video-container">
|
||||||
|
<video
|
||||||
|
v-if="selectedItem.type === 'video'"
|
||||||
|
class="detail-video"
|
||||||
|
:src="selectedItem.resultUrl || selectedItem.cover"
|
||||||
|
:poster="selectedItem.cover"
|
||||||
|
controls
|
||||||
|
>
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="detail-image"
|
||||||
|
:src="selectedItem.cover"
|
||||||
|
:alt="selectedItem.title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-right">
|
||||||
|
<div class="detail-header">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="avatar">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="username">{{ (selectedItem && selectedItem.username) || '匿名用户' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab" :class="{ active: activeDetailTab === 'detail' }" @click="activeDetailTab = 'detail'">作品详情</div>
|
||||||
|
<div class="tab" :class="{ active: activeDetailTab === 'category' }" @click="activeDetailTab = 'category'">{{ selectedItem.category }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section" v-if="activeDetailTab === 'detail'">
|
||||||
|
<h3 class="section-title">提示词</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 参考图特殊内容 -->
|
||||||
|
<div class="reference-content" v-if="activeDetailTab === 'category' && selectedItem.category === '参考图'">
|
||||||
|
<div class="input-details-section">
|
||||||
|
<h3 class="section-title">输入详情</h3>
|
||||||
|
<div class="input-images">
|
||||||
|
<div class="input-image-item">
|
||||||
|
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
|
||||||
|
</div>
|
||||||
|
<div class="input-image-item">
|
||||||
|
<img :src="selectedItem.cover" :alt="selectedItem.title" class="input-thumbnail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section">
|
||||||
|
<h3 class="section-title">提示词</h3>
|
||||||
|
<p class="description-text">图1在图2中奔跑视频</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 其他分类的内容 -->
|
||||||
|
<div class="description-section" v-if="activeDetailTab === 'category' && selectedItem.category !== '参考图'">
|
||||||
|
<h3 class="section-title">提示词</h3>
|
||||||
|
<p class="description-text">{{ getDescription(selectedItem) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metadata-section">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">创建时间</span>
|
||||||
|
<span class="value">{{ selectedItem.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">作品 ID</span>
|
||||||
|
<span class="value">{{ selectedItem.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">日期</span>
|
||||||
|
<span class="value">{{ selectedItem.date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="label">分类</span>
|
||||||
|
<span class="value">{{ selectedItem.category }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||||
|
<span class="label">时长</span>
|
||||||
|
<span class="value">{{ formatDuration(selectedItem.duration) || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||||
|
<span class="label">清晰度</span>
|
||||||
|
<span class="value">{{ selectedItem.quality || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-item" v-if="selectedItem.type === 'video'">
|
||||||
|
<span class="label">宽高比</span>
|
||||||
|
<span class="value">{{ selectedItem.aspectRatio || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-section">
|
||||||
|
<button class="create-similar-btn" @click="createSimilar(selectedItem)">
|
||||||
|
做同款
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 用户菜单下拉 - 使用Teleport渲染到body -->
|
<!-- 用户菜单下拉 - 使用Teleport渲染到body -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="showUserMenu" class="user-menu-teleport" :style="menuStyle">
|
<div v-if="showUserMenu" class="user-menu-teleport" :style="menuStyle">
|
||||||
@@ -168,6 +286,7 @@ import {
|
|||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { getMyWorks } from '@/api/userWorks'
|
import { getMyWorks } from '@/api/userWorks'
|
||||||
import { getCurrentUser } from '@/api/auth'
|
import { getCurrentUser } from '@/api/auth'
|
||||||
|
import { getWorkDetail } from '@/api/userWorks'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -192,6 +311,11 @@ const userLoading = ref(false)
|
|||||||
const videos = ref([])
|
const videos = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
const activeDetailTab = ref('detail')
|
||||||
|
|
||||||
// 计算菜单位置
|
// 计算菜单位置
|
||||||
const menuStyle = computed(() => {
|
const menuStyle = computed(() => {
|
||||||
if (!userStatusRef.value || !showUserMenu.value) return {}
|
if (!userStatusRef.value || !showUserMenu.value) return {}
|
||||||
@@ -287,18 +411,105 @@ const logout = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开作品详情
|
||||||
|
const openDetail = async (item) => {
|
||||||
|
selectedItem.value = item
|
||||||
|
activeDetailTab.value = 'detail'
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getWorkDetail(item.id)
|
||||||
|
if (response && response.data && response.data.success && response.data.data) {
|
||||||
|
const work = response.data.data
|
||||||
|
selectedItem.value = transformWorkData(work)
|
||||||
|
} else {
|
||||||
|
console.error('获取作品详情失败:', response?.data?.message || '未知错误')
|
||||||
|
ElMessage.error('获取作品详情失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载作品详情失败:', error)
|
||||||
|
ElMessage.error('加载作品详情失败: ' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭详情
|
||||||
|
const handleClose = () => {
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
selectedItem.value = null
|
||||||
|
activeDetailTab.value = 'detail'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取作品提示词(优先使用 prompt,其次使用后台 description,最后回退默认文案)
|
||||||
|
const getDescription = (item) => {
|
||||||
|
if (!item) return ''
|
||||||
|
const desc = (item.prompt && item.prompt.trim()) ? item.prompt : (item.description && item.description.trim() ? item.description : '')
|
||||||
|
if (desc) return desc
|
||||||
|
// 回退文案
|
||||||
|
if (item.type === 'video') {
|
||||||
|
return '暂无提示词'
|
||||||
|
}
|
||||||
|
return '暂无提示词'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时长
|
||||||
|
const formatDuration = (dur) => {
|
||||||
|
if (dur === null || dur === undefined || dur === '') return ''
|
||||||
|
if (typeof dur === 'number') return `${dur}s`
|
||||||
|
if (typeof dur === 'string') {
|
||||||
|
const trimmed = dur.trim()
|
||||||
|
if (/^\d+$/.test(trimmed)) return `${trimmed}s`
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
return String(dur)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 做同款
|
||||||
|
const createSimilar = (item) => {
|
||||||
|
if (!item) return
|
||||||
|
if (item.type === 'video') {
|
||||||
|
router.push('/text-to-video/create')
|
||||||
|
} else {
|
||||||
|
router.push('/image-to-video/create')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理URL,确保相对路径正确
|
||||||
|
const processUrl = (url) => {
|
||||||
|
if (!url) return null
|
||||||
|
// 如果已经是完整URL(http/https),直接返回
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
// 如果是相对路径(以/开头),确保以/开头
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
// 否则添加/前缀
|
||||||
|
return '/' + url
|
||||||
|
}
|
||||||
|
|
||||||
// 将后端返回的UserWork数据转换为前端需要的格式
|
// 将后端返回的UserWork数据转换为前端需要的格式
|
||||||
const transformWorkData = (work) => {
|
const transformWorkData = (work) => {
|
||||||
|
const resultUrl = processUrl(work.resultUrl)
|
||||||
|
const thumbnailUrl = processUrl(work.thumbnailUrl)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: work.id?.toString() || work.taskId || '',
|
id: work.id?.toString() || work.taskId || '',
|
||||||
title: work.title || work.prompt || '未命名作品',
|
title: work.title || work.prompt || '未命名作品',
|
||||||
cover: work.thumbnailUrl || work.resultUrl || '/images/backgrounds/welcome.jpg',
|
cover: thumbnailUrl || resultUrl || '/images/backgrounds/welcome.jpg',
|
||||||
resultUrl: work.resultUrl || '',
|
resultUrl: resultUrl || '',
|
||||||
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' ? 'video' : 'image',
|
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' || work.workType === 'STORYBOARD_VIDEO' ? 'video' : 'image',
|
||||||
text: work.prompt || '',
|
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : work.workType === 'STORYBOARD_VIDEO' ? '分镜视频' : '未知',
|
||||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : '未知',
|
sizeText: work.fileSize || '未知大小',
|
||||||
size: work.fileSize || '未知大小',
|
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : '',
|
||||||
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 || work.videoDuration || work.length || '',
|
||||||
|
aspectRatio: work.aspectRatio || work.ratio || work.aspect || '',
|
||||||
|
quality: work.quality || work.resolution || '',
|
||||||
|
username: work.username || work.user?.username || work.creator || work.author || work.owner || '未知用户',
|
||||||
|
status: work.status || 'COMPLETED',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,16 +585,7 @@ 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 onVideoLoaded = (event) => {
|
||||||
@@ -453,7 +655,7 @@ onUnmounted(() => {
|
|||||||
/* 左侧导航栏 */
|
/* 左侧导航栏 */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 280px !important;
|
width: 280px !important;
|
||||||
background: #1a1a1a !important;
|
background: #000000 !important;
|
||||||
padding: 24px 0 !important;
|
padding: 24px 0 !important;
|
||||||
border-right: 1px solid #1a1a1a !important;
|
border-right: 1px solid #1a1a1a !important;
|
||||||
flex-shrink: 0 !important;
|
flex-shrink: 0 !important;
|
||||||
@@ -464,9 +666,14 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
padding: 0 24px 32px;
|
padding: 0 24px 32px;
|
||||||
font-size: 20px;
|
display: flex;
|
||||||
font-weight: 500;
|
align-items: center;
|
||||||
color: white;
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu, .tools-menu {
|
.nav-menu, .tools-menu {
|
||||||
@@ -909,4 +1116,268 @@ onUnmounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 详情弹窗样式(与“我的作品”保持一致) */
|
||||||
|
:deep(.el-dialog.detail-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
/* 强制覆盖 Element Plus 对话框背景变量,避免仍使用默认白色 */
|
||||||
|
--el-dialog-bg-color: #0a0a0a !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog.detail-dialog .el-dialog__header) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog.detail-dialog .el-dialog__title) {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog.detail-dialog .el-dialog__headerbtn) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog.detail-dialog .el-dialog__body) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
height: 50vh;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-left {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-video, .detail-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right {
|
||||||
|
flex: 1;
|
||||||
|
background: #0a0a0a;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right .avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #409eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-right .username {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d1d5db;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reference-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-details-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-images {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-image-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:hover {
|
||||||
|
background: #337ecc;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-similar-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 覆盖全局对话框与遮罩背景,确保弹窗打开时为深色背景 */
|
||||||
|
:deep(.el-dialog__wrapper) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 不全局影响其它对话框,仅本弹窗 */
|
||||||
|
|
||||||
|
/* 仅作用于本弹窗的遮罩类,避免全局影响 */
|
||||||
|
:deep(.dark-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 兜底:确保对话框本体也是深色(覆盖可能的白色默认) */
|
||||||
|
:deep(.dark-overlay .el-overlay-dialog),
|
||||||
|
:deep(.dark-overlay .el-dialog),
|
||||||
|
:deep(.el-overlay .el-dialog.detail-dialog),
|
||||||
|
:deep(.detail-dialog .el-dialog) {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局兜底:仅作用于个人主页详情弹窗 */
|
||||||
|
.el-dialog.detail-dialog {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
--el-dialog-bg-color: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
.el-dialog.detail-dialog .el-dialog__header {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
.el-dialog.detail-dialog .el-dialog__body {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -117,8 +117,9 @@
|
|||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<label>时长</label>
|
<label>时长</label>
|
||||||
<select v-model="duration" class="setting-select">
|
<select v-model="duration" class="setting-select">
|
||||||
<option value="5">5s</option>
|
|
||||||
<option value="10">10s</option>
|
<option value="10">10s</option>
|
||||||
|
<option value="15">15s</option>
|
||||||
|
<option value="25">25s</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -139,10 +140,14 @@
|
|||||||
<button
|
<button
|
||||||
class="generate-btn"
|
class="generate-btn"
|
||||||
@click="handleGenerateClick"
|
@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>
|
</button>
|
||||||
|
<div v-if="!isAuthenticated" class="login-tip-floating">
|
||||||
|
<p>需要登录后才能提交任务</p>
|
||||||
|
<button class="login-link-btn" @click="goToLogin">立即登录</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧预览区域 -->
|
<!-- 右侧预览区域 -->
|
||||||
@@ -189,6 +194,66 @@
|
|||||||
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
|
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,19 +261,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onBeforeUnmount } from 'vue'
|
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElLoading } from 'element-plus'
|
||||||
import { createStoryboardTask, getStoryboardTask } from '@/api/storyboardVideo'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { createStoryboardTask, getStoryboardTask, getUserStoryboardTasks } from '@/api/storyboardVideo'
|
||||||
import { imageToVideoApi } from '@/api/imageToVideo'
|
import { imageToVideoApi } from '@/api/imageToVideo'
|
||||||
import { optimizePrompt } from '@/api/promptOptimizer'
|
import { optimizePrompt } from '@/api/promptOptimizer'
|
||||||
|
import { getProcessingWorks } from '@/api/userWorks'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 计算是否已登录
|
||||||
|
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
const aspectRatio = ref('16:9')
|
const aspectRatio = ref('16:9')
|
||||||
const duration = ref('5')
|
const duration = ref('10')
|
||||||
const hdMode = ref(false)
|
const hdMode = ref(false)
|
||||||
const inProgress = ref(false)
|
const inProgress = ref(false)
|
||||||
const currentStep = ref('generate') // 'generate' 或 'video'
|
const currentStep = ref('generate') // 'generate' 或 'video'
|
||||||
@@ -239,12 +310,23 @@ const videoPollIntervalId = ref(null) // 视频任务轮询定时器ID
|
|||||||
const videoTaskStatus = ref('') // 视频任务状态:PROCESSING, COMPLETED, FAILED
|
const videoTaskStatus = ref('') // 视频任务状态:PROCESSING, COMPLETED, FAILED
|
||||||
const videoResultUrl = ref('') // 视频结果URL
|
const videoResultUrl = ref('') // 视频结果URL
|
||||||
const videoProgress = ref(0) // 视频生成进度
|
const videoProgress = ref(0) // 视频生成进度
|
||||||
|
const historyTasks = ref([]) // 历史记录
|
||||||
|
const playingVideos = ref({}) // 正在播放的视频
|
||||||
|
const videoRefs = ref({}) // 视频元素引用
|
||||||
|
|
||||||
// 导航函数
|
// 导航函数
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转到登录页面
|
||||||
|
const goToLogin = () => {
|
||||||
|
router.push({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: router.currentRoute.value.fullPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const goToTextToVideo = () => {
|
const goToTextToVideo = () => {
|
||||||
router.push('/text-to-video/create')
|
router.push('/text-to-video/create')
|
||||||
}
|
}
|
||||||
@@ -295,6 +377,13 @@ const getButtonText = () => {
|
|||||||
const handleGenerateClick = () => {
|
const handleGenerateClick = () => {
|
||||||
console.log('handleGenerateClick 被调用,当前步骤:', currentStep.value)
|
console.log('handleGenerateClick 被调用,当前步骤:', currentStep.value)
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
ElMessage.warning('请先登录后再提交任务')
|
||||||
|
goToLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 如果已经切换到视频步骤,直接生成视频
|
// 如果已经切换到视频步骤,直接生成视频
|
||||||
if (currentStep.value === 'video') {
|
if (currentStep.value === 'video') {
|
||||||
startVideoGenerate()
|
startVideoGenerate()
|
||||||
@@ -473,6 +562,8 @@ const optimizePromptHandler = async () => {
|
|||||||
}
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
errorMessage = '网络错误,请检查网络连接'
|
errorMessage = '网络错误,请检查网络连接'
|
||||||
|
} else if (error.code === 'ERR_NETWORK') {
|
||||||
|
errorMessage = '网络连接错误,请检查您的网络'
|
||||||
} else {
|
} else {
|
||||||
errorMessage = error.message || '优化失败'
|
errorMessage = error.message || '优化失败'
|
||||||
}
|
}
|
||||||
@@ -487,6 +578,13 @@ const optimizePromptHandler = async () => {
|
|||||||
const startGenerate = async () => {
|
const startGenerate = async () => {
|
||||||
console.log('startGenerate 被调用')
|
console.log('startGenerate 被调用')
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
ElMessage.warning('请先登录后再提交任务')
|
||||||
|
goToLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!inputText.value.trim()) {
|
if (!inputText.value.trim()) {
|
||||||
ElMessage.warning('请输入描述文字')
|
ElMessage.warning('请输入描述文字')
|
||||||
return
|
return
|
||||||
@@ -746,6 +844,13 @@ const urlToFile = async (url, filename) => {
|
|||||||
const startVideoGenerate = async () => {
|
const startVideoGenerate = async () => {
|
||||||
console.log('startVideoGenerate 被调用')
|
console.log('startVideoGenerate 被调用')
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
ElMessage.warning('请先登录后再提交任务')
|
||||||
|
goToLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有分镜图任务ID(从分镜图生成),使用新的API
|
// 如果有分镜图任务ID(从分镜图生成),使用新的API
|
||||||
if (taskId.value && generatedImageUrl.value) {
|
if (taskId.value && generatedImageUrl.value) {
|
||||||
try {
|
try {
|
||||||
@@ -945,6 +1050,237 @@ const pollVideoTaskStatus = async (taskId) => {
|
|||||||
videoPollIntervalId.value = setTimeout(poll, pollInterval)
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
if (pollIntervalId.value) {
|
if (pollIntervalId.value) {
|
||||||
@@ -1576,12 +1912,65 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: none;
|
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 {
|
.right-panel {
|
||||||
background: #0a0a0a;
|
background: #0a0a0a;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.preview-area {
|
||||||
@@ -1589,6 +1978,7 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
min-height: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-checkbox {
|
.status-checkbox {
|
||||||
@@ -1624,6 +2014,11 @@ onBeforeUnmount(() => {
|
|||||||
overflow: hidden; /* 防止内容溢出 */
|
overflow: hidden; /* 防止内容溢出 */
|
||||||
min-height: 0; /* 允许flex子项缩小 */
|
min-height: 0; /* 允许flex子项缩小 */
|
||||||
position: relative; /* 为绝对定位的子元素提供定位参考 */
|
position: relative; /* 为绝对定位的子元素提供定位参考 */
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-content:hover {
|
.preview-content:hover {
|
||||||
@@ -1631,15 +2026,21 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-placeholder {
|
.preview-placeholder {
|
||||||
text-align: center;
|
width: 100%;
|
||||||
padding: 40px;
|
min-height: 400px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
color: #6b7280;
|
color: #9ca3af;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 预览图片 */
|
/* 预览图片 */
|
||||||
@@ -1838,6 +2239,223 @@ onBeforeUnmount(() => {
|
|||||||
text-align: left;
|
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>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,8 +62,9 @@
|
|||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<label>时长</label>
|
<label>时长</label>
|
||||||
<select v-model="duration" class="setting-select">
|
<select v-model="duration" class="setting-select">
|
||||||
<option value="5">5s</option>
|
|
||||||
<option value="10">10s</option>
|
<option value="10">10s</option>
|
||||||
|
<option value="15">15s</option>
|
||||||
|
<option value="25">25s</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,9 +79,17 @@
|
|||||||
|
|
||||||
<!-- 生成按钮 -->
|
<!-- 生成按钮 -->
|
||||||
<div class="generate-section">
|
<div class="generate-section">
|
||||||
<button class="generate-btn" @click="startGenerate">
|
<button
|
||||||
开始生成
|
class="generate-btn"
|
||||||
|
@click="startGenerate"
|
||||||
|
:disabled="!isAuthenticated"
|
||||||
|
>
|
||||||
|
{{ isAuthenticated ? '开始生成' : '请先登录' }}
|
||||||
</button>
|
</button>
|
||||||
|
<div v-if="!isAuthenticated" class="login-tip">
|
||||||
|
<p>需要登录后才能提交任务</p>
|
||||||
|
<button class="login-link-btn" @click="goToLogin">立即登录</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,7 +163,6 @@
|
|||||||
<!-- 操作按钮区域 -->
|
<!-- 操作按钮区域 -->
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<button class="action-btn primary" @click="createSimilar">做同款</button>
|
<button class="action-btn primary" @click="createSimilar">做同款</button>
|
||||||
<button class="action-btn primary" @click="submitWork">投稿</button>
|
|
||||||
<div class="action-icons">
|
<div class="action-icons">
|
||||||
<button class="icon-btn" @click="downloadVideo" title="下载视频">
|
<button class="icon-btn" @click="downloadVideo" title="下载视频">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
@@ -196,6 +204,63 @@
|
|||||||
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
|
<div class="placeholder-text">开始创作您的第一个作品吧!</div>
|
||||||
</div>
|
</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" 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 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,21 +295,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onUnmounted, computed } from 'vue'
|
import { ref, onUnmounted, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { textToVideoApi } from '@/api/textToVideo'
|
import { textToVideoApi } from '@/api/textToVideo'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { User, VideoCamera, Star, Setting, SwitchButton } from '@element-plus/icons-vue'
|
import { User, VideoCamera, Star, Setting, SwitchButton } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElLoading } from 'element-plus'
|
import { ElMessage, ElLoading } from 'element-plus'
|
||||||
import { optimizePrompt } from '@/api/promptOptimizer'
|
import { optimizePrompt } from '@/api/promptOptimizer'
|
||||||
|
import { getProcessingWorks } from '@/api/userWorks'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 计算是否已登录
|
||||||
|
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
const aspectRatio = ref('16:9')
|
const aspectRatio = ref('16:9')
|
||||||
const duration = ref(5)
|
const duration = ref(10)
|
||||||
const hdMode = ref(false)
|
const hdMode = ref(false)
|
||||||
const inProgress = ref(false)
|
const inProgress = ref(false)
|
||||||
const currentTask = ref(null)
|
const currentTask = ref(null)
|
||||||
@@ -254,6 +323,9 @@ const stopPolling = ref(null)
|
|||||||
const showInProgress = ref(false)
|
const showInProgress = ref(false)
|
||||||
const watermarkOption = ref('without')
|
const watermarkOption = ref('without')
|
||||||
const optimizingPrompt = ref(false) // 优化提示词状态
|
const optimizingPrompt = ref(false) // 优化提示词状态
|
||||||
|
const historyTasks = ref([]) // 历史记录
|
||||||
|
const playingVideos = ref({}) // 正在播放的视频
|
||||||
|
const videoRefs = ref({}) // 视频元素引用
|
||||||
|
|
||||||
// 用户菜单相关
|
// 用户菜单相关
|
||||||
const showUserMenu = ref(false)
|
const showUserMenu = ref(false)
|
||||||
@@ -277,6 +349,14 @@ const goBack = () => {
|
|||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转到登录页面
|
||||||
|
const goToLogin = () => {
|
||||||
|
router.push({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: router.currentRoute.value.fullPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const goToImageToVideo = () => {
|
const goToImageToVideo = () => {
|
||||||
router.push('/image-to-video/create')
|
router.push('/image-to-video/create')
|
||||||
}
|
}
|
||||||
@@ -317,6 +397,13 @@ const logout = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startGenerate = async () => {
|
const startGenerate = async () => {
|
||||||
|
// 检查登录状态
|
||||||
|
if (!userStore.isAuthenticated) {
|
||||||
|
ElMessage.warning('请先登录后再提交任务')
|
||||||
|
goToLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否已有任务在进行中
|
// 检查是否已有任务在进行中
|
||||||
if (inProgress.value) {
|
if (inProgress.value) {
|
||||||
ElMessage.warning('已有任务在进行中,请等待完成或取消当前任务')
|
ElMessage.warning('已有任务在进行中,请等待完成或取消当前任务')
|
||||||
@@ -329,11 +416,6 @@ const startGenerate = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证描述文字长度
|
|
||||||
if (inputText.value.trim().length > 1000) {
|
|
||||||
ElMessage.error('文本描述不能超过1000个字符')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
const loading = ElLoading.service({
|
const loading = ElLoading.service({
|
||||||
@@ -516,6 +598,8 @@ const optimizePromptHandler = async () => {
|
|||||||
}
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
errorMessage = '网络错误,请检查网络连接'
|
errorMessage = '网络错误,请检查网络连接'
|
||||||
|
} else if (error.code === 'ERR_NETWORK') {
|
||||||
|
errorMessage = '网络连接错误,请检查您的网络'
|
||||||
} else {
|
} else {
|
||||||
errorMessage = error.message || '优化失败'
|
errorMessage = error.message || '优化失败'
|
||||||
}
|
}
|
||||||
@@ -559,17 +643,7 @@ const retryTask = () => {
|
|||||||
startGenerate()
|
startGenerate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投稿功能
|
// 投稿功能(已移除按钮,保留函数已删除)
|
||||||
const submitWork = () => {
|
|
||||||
if (!currentTask.value) {
|
|
||||||
ElMessage.error('没有可投稿的作品')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里可以调用投稿API
|
|
||||||
ElMessage.success('投稿成功!')
|
|
||||||
console.log('投稿作品:', currentTask.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除作品
|
// 删除作品
|
||||||
const deleteWork = () => {
|
const deleteWork = () => {
|
||||||
@@ -593,6 +667,210 @@ const deleteWork = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理历史记录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 textToVideoApi.getTasks(0, 5)
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
// 只显示已完成的任务,排队中和处理中的任务在主创作区显示
|
||||||
|
const tasks = (response.data.data || []).filter(task =>
|
||||||
|
task.status === 'COMPLETED'
|
||||||
|
).slice(0, 5)
|
||||||
|
|
||||||
|
// 处理URL,确保相对路径正确
|
||||||
|
historyTasks.value = tasks.map(task => ({
|
||||||
|
...task,
|
||||||
|
resultUrl: task.resultUrl ? processHistoryUrl(task.resultUrl) : null
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('历史记录加载成功:', historyTasks.value.length, '条')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载历史记录失败:', error)
|
||||||
|
historyTasks.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从历史记录创建同款
|
||||||
|
const createSimilarFromHistory = (task) => {
|
||||||
|
if (task.prompt) {
|
||||||
|
inputText.value = task.prompt
|
||||||
|
}
|
||||||
|
if (task.aspectRatio) {
|
||||||
|
aspectRatio.value = task.aspectRatio
|
||||||
|
}
|
||||||
|
if (task.duration) {
|
||||||
|
duration.value = task.duration
|
||||||
|
}
|
||||||
|
if (task.hdMode !== undefined) {
|
||||||
|
hdMode.value = task.hdMode
|
||||||
|
}
|
||||||
|
// 滚动到顶部
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载历史记录
|
||||||
|
// 监听登录状态变化,登录后自动加载历史记录
|
||||||
|
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 textToVideoWorks = works.filter(work => work.workType === 'TEXT_TO_VIDEO')
|
||||||
|
|
||||||
|
if (textToVideoWorks.length > 0) {
|
||||||
|
// 取最新的一个任务
|
||||||
|
const work = textToVideoWorks[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 = parseInt(work.duration) || 10
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress.value = true
|
||||||
|
taskStatus.value = work.status || 'PROCESSING'
|
||||||
|
taskProgress.value = 50 // 初始进度设为50%
|
||||||
|
|
||||||
|
console.log('恢复正在进行中的任务:', work.taskId, '状态:', work.status)
|
||||||
|
ElMessage.info('检测到未完成的任务,继续处理中...')
|
||||||
|
|
||||||
|
// 开始轮询任务状态
|
||||||
|
startPollingTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('恢复任务失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadHistory()
|
||||||
|
restoreProcessingTask()
|
||||||
|
})
|
||||||
|
|
||||||
// 组件卸载时清理资源
|
// 组件卸载时清理资源
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 停止轮询
|
// 停止轮询
|
||||||
@@ -973,12 +1251,65 @@ onUnmounted(() => {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-tip {
|
||||||
|
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 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 {
|
.right-panel {
|
||||||
background: #0a0a0a;
|
background: #0a0a0a;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.preview-area {
|
||||||
@@ -986,6 +1317,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
min-height: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-checkbox {
|
.status-checkbox {
|
||||||
@@ -1018,6 +1350,11 @@ onUnmounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-content:hover {
|
.preview-content:hover {
|
||||||
@@ -1025,15 +1362,21 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-placeholder {
|
.preview-placeholder {
|
||||||
text-align: center;
|
width: 100%;
|
||||||
padding: 40px;
|
min-height: 400px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
color: #6b7280;
|
color: #9ca3af;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@@ -1251,6 +1594,10 @@ onUnmounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 生成中状态 */
|
/* 生成中状态 */
|
||||||
@@ -1530,4 +1877,207 @@ onUnmounted(() => {
|
|||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 历史记录区域 */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user