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

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

View File

@@ -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
// 如果已经是完整URLhttp/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>