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:
@@ -3,7 +3,9 @@
|
||||
<!-- 左侧导航栏 -->
|
||||
<aside class="sidebar">
|
||||
<!-- Logo -->
|
||||
<div class="logo">logo</div>
|
||||
<div class="logo">
|
||||
<img src="/images/backgrounds/logo.svg" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="nav-menu">
|
||||
@@ -67,8 +69,7 @@
|
||||
<section class="profile-section">
|
||||
<div class="profile-info">
|
||||
<div class="avatar">
|
||||
<img v-if="userInfo.avatar" :src="userInfo.avatar" alt="avatar" class="avatar-image" />
|
||||
<div v-else class="avatar-icon"></div>
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="avatar" class="avatar-image" />
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<h2 class="username">{{ userInfo.nickname || userInfo.username || '未设置用户名' }}</h2>
|
||||
@@ -83,7 +84,7 @@
|
||||
<h3 class="section-title">已发布</h3>
|
||||
<div class="video-grid">
|
||||
<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">
|
||||
<!-- 如果是视频类型且有视频URL,使用video元素显示首帧 -->
|
||||
<video
|
||||
@@ -103,10 +104,9 @@
|
||||
/>
|
||||
<!-- 否则使用占位符 -->
|
||||
<div v-else class="figure"></div>
|
||||
<div class="text-overlay" v-if="video.text">{{ video.text }}</div>
|
||||
</div>
|
||||
<div class="video-action">
|
||||
<el-button v-if="index === 0" type="primary" size="small" @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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,6 +119,124 @@
|
||||
</main>
|
||||
</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 to="body">
|
||||
<div v-if="showUserMenu" class="user-menu-teleport" :style="menuStyle">
|
||||
@@ -168,6 +286,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getMyWorks } from '@/api/userWorks'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
import { getWorkDetail } from '@/api/userWorks'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -192,6 +311,11 @@ const userLoading = ref(false)
|
||||
const videos = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 详情弹窗
|
||||
const detailDialogVisible = ref(false)
|
||||
const selectedItem = ref(null)
|
||||
const activeDetailTab = ref('detail')
|
||||
|
||||
// 计算菜单位置
|
||||
const menuStyle = computed(() => {
|
||||
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数据转换为前端需要的格式
|
||||
const transformWorkData = (work) => {
|
||||
const resultUrl = processUrl(work.resultUrl)
|
||||
const thumbnailUrl = processUrl(work.thumbnailUrl)
|
||||
|
||||
return {
|
||||
id: work.id?.toString() || work.taskId || '',
|
||||
title: work.title || work.prompt || '未命名作品',
|
||||
cover: work.thumbnailUrl || work.resultUrl || '/images/backgrounds/welcome.jpg',
|
||||
resultUrl: work.resultUrl || '',
|
||||
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' ? 'video' : 'image',
|
||||
text: work.prompt || '',
|
||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : '未知',
|
||||
size: work.fileSize || '未知大小',
|
||||
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : ''
|
||||
cover: thumbnailUrl || resultUrl || '/images/backgrounds/welcome.jpg',
|
||||
resultUrl: resultUrl || '',
|
||||
type: work.workType === 'TEXT_TO_VIDEO' || work.workType === 'IMAGE_TO_VIDEO' || work.workType === 'STORYBOARD_VIDEO' ? 'video' : 'image',
|
||||
category: work.workType === 'TEXT_TO_VIDEO' ? '文生视频' : work.workType === 'IMAGE_TO_VIDEO' ? '图生视频' : work.workType === 'STORYBOARD_VIDEO' ? '分镜视频' : '未知',
|
||||
sizeText: work.fileSize || '未知大小',
|
||||
createTime: work.createdAt ? new Date(work.createdAt).toLocaleString('zh-CN') : '',
|
||||
date: work.createdAt ? new Date(work.createdAt).toLocaleDateString('zh-CN') : '',
|
||||
description: work.description || work.prompt || '',
|
||||
prompt: work.prompt || '',
|
||||
duration: work.duration || 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) => {
|
||||
@@ -453,7 +655,7 @@ onUnmounted(() => {
|
||||
/* 左侧导航栏 */
|
||||
.sidebar {
|
||||
width: 280px !important;
|
||||
background: #1a1a1a !important;
|
||||
background: #000000 !important;
|
||||
padding: 24px 0 !important;
|
||||
border-right: 1px solid #1a1a1a !important;
|
||||
flex-shrink: 0 !important;
|
||||
@@ -464,9 +666,14 @@ onUnmounted(() => {
|
||||
|
||||
.logo {
|
||||
padding: 0 24px 32px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-menu, .tools-menu {
|
||||
@@ -909,4 +1116,268 @@ onUnmounted(() => {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user