Files
AIGC/demo/frontend/src/views/Profile.vue
AIGC Developer dbd06435cb feat: 完成管理员密码登录修复和项目清理
- 修复BCryptPasswordEncoder密码验证问题
- 实现密码设置提示弹窗功能(仅对无密码用户显示一次)
- 优化修改密码逻辑和验证流程
- 更新Welcome页面背景样式
- 清理临时SQL文件和测试代码
- 移动数据库备份文件到database/backups目录
- 删除不必要的MD文档和临时文件
2025-11-21 16:10:00 +08:00

1513 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="profile-page">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<!-- Logo -->
<div class="logo">
<img src="/images/backgrounds/logo.svg" alt="Logo" />
</div>
<!-- 导航菜单 -->
<nav class="nav-menu">
<div class="nav-item active">
<el-icon><User /></el-icon>
<span>{{ t('profile.title') }}</span>
</div>
<div class="nav-item">
<el-icon><Compass /></el-icon>
<span @click="goToSubscription">{{ t('profile.subscription') }}</span>
</div>
<div class="nav-item">
<el-icon><Document /></el-icon>
<span @click="goToMyWorks">{{ t('profile.myWorks') }}</span>
</div>
</nav>
<!-- 工具分隔线 -->
<div class="divider">
<span>{{ t('profile.tools') }}</span>
</div>
<!-- 工具菜单 -->
<nav class="tools-menu">
<div class="nav-item">
<el-icon><VideoPlay /></el-icon>
<span @click="goToTextToVideo">{{ t('home.textToVideo') }}</span>
</div>
<div class="nav-item">
<el-icon><Picture /></el-icon>
<span @click="goToImageToVideo">{{ t('home.imageToVideo') }}</span>
</div>
<div class="nav-item">
<el-icon><Film /></el-icon>
<span @click="goToStoryboardVideo">{{ t('home.storyboardVideo') }}</span>
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
</div>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部栏 -->
<header class="top-header">
<div class="header-right">
<div class="points">
<div class="points-icon">
<el-icon><Star /></el-icon>
</div>
<span class="points-number">{{ userStore.availablePoints }}</span>
</div>
<LanguageSwitcher />
<div class="user-status" @click="showUserMenu = !showUserMenu" ref="userStatusRef">
<img src="/images/backgrounds/avatar-default.svg" :alt="t('dashboard.userAvatar')" class="status-icon" />
</div>
</div>
</header>
<!-- 用户资料区域 -->
<section class="profile-section">
<div class="profile-info">
<div class="avatar">
<img src="/images/backgrounds/avatar-default.svg" :alt="t('dashboard.userAvatar')" class="avatar-image" />
</div>
<div class="user-details">
<h2 class="username">{{ userInfo.nickname || userInfo.username || t('profile.noUsername') }}</h2>
<p class="profile-status" v-if="userInfo.bio">{{ userInfo.bio }}</p>
<p class="user-id">{{ t('profile.userId') }} {{ userInfo.id || t('common.loading') }}</p>
</div>
</div>
</section>
<!-- 已发布内容 -->
<section class="published-section">
<h3 class="section-title">{{ t('profile.published') }}</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="openDetail(video)">
<div class="thumbnail-image">
<!-- 如果是视频类型且有视频URL使用video元素显示首帧 -->
<video
v-if="video.type === 'video' && video.resultUrl"
:src="video.resultUrl"
class="video-cover-img"
muted
preload="metadata"
@loadedmetadata="onVideoLoaded"
></video>
<!-- 如果有封面图thumbnailUrl使用图片 -->
<img
v-else-if="video.cover && video.cover !== video.resultUrl"
:src="video.cover"
:alt="video.title"
class="video-cover-img"
/>
<!-- 否则使用占位符 -->
<div v-else class="figure"></div>
</div>
<div class="video-action">
<el-button v-if="index === 0" type="primary" size="small" @click.stop="createSimilar(video)">{{ t('profile.createSimilar') }}</el-button>
<span v-else class="director-text">DIRECTED BY VANNOCENT</span>
</div>
</div>
</div>
<div v-if="!loading && videos.length === 0" class="empty-works">
<div class="empty-text">{{ t('profile.noWorksYet') }}</div>
</div>
</div>
</section>
</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
>
{{ t('profile.browserNotSupport') }}
</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">
<img src="/images/backgrounds/avatar-default.svg" :alt="t('dashboard.userAvatar')" class="avatar-image" />
</div>
<div class="username">{{ (selectedItem && selectedItem.username) || t('profile.anonymousUser') }}</div>
</div>
</div>
<div class="tabs">
<div class="tab" :class="{ active: activeDetailTab === 'detail' }" @click="activeDetailTab = 'detail'">{{ t('profile.workDetail') }}</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">{{ t('video.prompt') }}</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">{{ t('profile.inputDetails') }}</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">{{ t('video.prompt') }}</h3>
<p class="description-text">图1在图2中奔跑视频</p>
</div>
</div>
<!-- 其他分类的内容 -->
<div class="description-section" v-if="activeDetailTab === 'category' && selectedItem.category !== '参考图'">
<h3 class="section-title">{{ t('video.prompt') }}</h3>
<p class="description-text">{{ getDescription(selectedItem) }}</p>
</div>
<div class="metadata-section">
<div class="metadata-item">
<span class="label">{{ t('profile.createTime') }}</span>
<span class="value">{{ selectedItem.createTime }}</span>
</div>
<div class="metadata-item">
<span class="label">{{ t('profile.workId') }}</span>
<span class="value">{{ selectedItem.id }}</span>
</div>
<div class="metadata-item">
<span class="label">{{ t('profile.date') }}</span>
<span class="value">{{ selectedItem.date }}</span>
</div>
<div class="metadata-item">
<span class="label">{{ t('profile.category') }}</span>
<span class="value">{{ selectedItem.category }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">{{ t('profile.duration') }}</span>
<span class="value">{{ formatDuration(selectedItem.duration) || t('profile.unknown') }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">{{ t('profile.quality') }}</span>
<span class="value">{{ selectedItem.quality || t('profile.unknown') }}</span>
</div>
<div class="metadata-item" v-if="selectedItem.type === 'video'">
<span class="label">{{ t('profile.aspectRatio') }}</span>
<span class="value">{{ selectedItem.aspectRatio || t('profile.unknown') }}</span>
</div>
</div>
<div class="action-section">
<button class="create-similar-btn" @click="createSimilar(selectedItem)">
{{ t('profile.createSimilar') }}
</button>
</div>
</div>
</div>
</el-dialog>
<!-- 用户菜单下拉 - 使用Teleport渲染到body -->
<Teleport to="body">
<div v-if="showUserMenu" class="user-menu-teleport" :style="menuStyle">
<!-- 管理员功能 -->
<template v-if="userStore.isAdmin">
<div class="menu-item" @click="goToDashboard">
<el-icon><User /></el-icon>
<span>{{ t('profile.dashboard') }}</span>
</div>
<div class="menu-item" @click="goToOrders">
<el-icon><Document /></el-icon>
<span>{{ t('profile.orderManagement') }}</span>
</div>
<div class="menu-item" @click="goToMembers">
<el-icon><User /></el-icon>
<span>{{ t('profile.memberManagement') }}</span>
</div>
<div class="menu-item" @click="goToSettings">
<el-icon><Setting /></el-icon>
<span>{{ t('profile.systemSettings') }}</span>
</div>
</template>
<!-- 修改密码所有登录用户可见 -->
<div class="menu-item" @click="openChangePasswordDialog">
<el-icon><Lock /></el-icon>
<span>{{ t('profile.changePassword') }}</span>
</div>
<!-- 退出登录 -->
<div class="menu-item" @click="logout">
<el-icon><User /></el-icon>
<span>{{ t('common.logout') }}</span>
</div>
</div>
</Teleport>
<!-- 修改密码弹窗 -->
<el-dialog
v-model="changePasswordDialogVisible"
:title="t('profile.changePassword')"
width="420px"
>
<el-form :model="changePasswordForm" label-width="90px">
<el-form-item label="当前密码">
<el-input
v-model="changePasswordForm.oldPassword"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="新密码" required>
<el-input
v-model="changePasswordForm.newPassword"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" required>
<el-input
v-model="changePasswordForm.confirmPassword"
type="password"
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="changePasswordDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="changePasswordLoading" @click="submitChangePassword">
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
import {
User,
Document,
Star,
Setting,
Lock,
Compass,
VideoPlay,
Picture,
Film
} from '@element-plus/icons-vue'
import { getMyWorks } from '@/api/userWorks'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
import { getCurrentUser, changePassword } from '@/api/auth'
import { getWorkDetail } from '@/api/userWorks'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const userStore = useUserStore()
const { t } = useI18n()
// 控制用户菜单显示
const showUserMenu = ref(false)
const userStatusRef = ref(null)
// 用户信息
const userInfo = ref({
username: '',
nickname: '',
bio: '',
avatar: '',
id: '',
points: 0,
frozenPoints: 0
})
// 修改密码弹窗
const changePasswordDialogVisible = ref(false)
const changePasswordLoading = ref(false)
const changePasswordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const openChangePasswordDialog = () => {
showUserMenu.value = false
changePasswordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: ''
}
changePasswordDialogVisible.value = true
}
const submitChangePassword = async () => {
if (!changePasswordForm.value.newPassword) {
ElMessage.error('新密码不能为空')
return
}
if (changePasswordForm.value.newPassword.length < 6) {
ElMessage.error('新密码长度不能少于6位')
return
}
if (changePasswordForm.value.newPassword !== changePasswordForm.value.confirmPassword) {
ElMessage.error('两次输入的密码不一致')
return
}
try {
changePasswordLoading.value = true
await changePassword({
oldPassword: changePasswordForm.value.oldPassword,
newPassword: changePasswordForm.value.newPassword
})
ElMessage.success('密码修改成功')
changePasswordDialogVisible.value = false
} catch (error) {
console.error('修改密码失败:', error)
const msg = error?.response?.data?.message || '修改密码失败'
ElMessage.error(msg)
} finally {
changePasswordLoading.value = false
}
}
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 {}
const rect = userStatusRef.value.getBoundingClientRect()
return {
position: 'fixed',
top: `${rect.bottom + 8}px`,
right: `${window.innerWidth - rect.right}px`,
zIndex: 99999
}
})
// 跳转到会员订阅页面
const goToSubscription = () => {
router.push('/subscription')
}
const goToMyWorks = () => {
router.push('/works')
}
const goToTextToVideo = () => {
router.push('/text-to-video/create')
}
const goToImageToVideo = () => {
router.push('/image-to-video/create')
}
const goToStoryboardVideo = () => {
router.push('/storyboard-video/create')
}
// 跳转到数据仪表盘
const goToDashboard = () => {
showUserMenu.value = false
// 检查用户权限,只有管理员才能访问数据仪表盘
if (userStore.isAdmin) {
router.push('/admin/dashboard')
} else {
ElMessage.warning(t('profile.insufficientPermission'))
}
}
// 跳转到订单管理
const goToOrders = () => {
showUserMenu.value = false
router.push('/admin/orders')
}
// 跳转到会员管理
const goToMembers = () => {
showUserMenu.value = false
// 检查用户权限,只有管理员才能访问会员管理
if (userStore.isAdmin) {
router.push('/member-management')
} else {
ElMessage.warning(t('profile.insufficientPermission'))
}
}
// 跳转到系统设置
const goToSettings = () => {
showUserMenu.value = false
// 检查用户权限,只有管理员才能访问系统设置
if (userStore.isAdmin) {
router.push('/system-settings')
} else {
ElMessage.warning(t('profile.insufficientPermission'))
}
}
// 退出登录
const logout = async () => {
try {
showUserMenu.value = false
// 清除用户数据
await userStore.logoutUser()
// 清除其他可能的本地存储
localStorage.removeItem('user')
localStorage.removeItem('token')
ElMessage.success(t('profile.logoutSuccess'))
// 跳转到登录页
router.push('/login')
} catch (error) {
console.error('退出登录失败:', error)
ElMessage.error(t('profile.logoutFailed'))
}
}
// 打开作品详情
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(t('profile.loadDetailFailed'))
}
} catch (error) {
console.error('加载作品详情失败:', error)
ElMessage.error(t('profile.loadDetailFailed') + ': ' + (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 t('profile.noPrompt')
}
return t('profile.noPrompt')
}
// 格式化时长
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: 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' ? '分镜视频' : work.workType === 'STORYBOARD_IMAGE' ? '分镜图' : '未知',
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',
}
}
// 加载用户信息
const loadUserInfo = async () => {
userLoading.value = true
try {
const response = await getCurrentUser()
console.log('获取用户信息响应:', response)
if (response && response.data && response.data.success && response.data.data) {
const user = response.data.data
console.log('用户数据:', user)
userInfo.value = {
username: user.username || '',
nickname: user.nickname || user.username || '',
bio: user.bio || '',
avatar: user.avatar || '',
id: user.id ? String(user.id) : '',
points: user.points || 0,
frozenPoints: user.frozenPoints || 0
}
console.log('设置后的用户信息:', userInfo.value)
// 检查用户是否需要设置密码(只有数据库中真正没有密码时才弹窗,且只弹一次)
const needSetPasswordFlag = sessionStorage.getItem('needSetPassword') === '1'
const hasShownPasswordDialog = sessionStorage.getItem('hasShownPasswordDialog') === '1'
// 检查后端返回的用户密码状态
const hasNoPassword = !user.passwordHash || String(user.passwordHash).trim() === ''
// 只有在以下情况下才弹出修改密码弹窗:
// 1. 用户数据库中确实没有密码hasNoPassword = true
// 2. 有 needSetPassword 标记(来自登录流程)
// 3. 还没有显示过弹窗(避免重复显示)
if (hasNoPassword && needSetPasswordFlag && !hasShownPasswordDialog) {
console.log('检测到用户没有设置密码,弹出修改密码弹窗')
openChangePasswordDialog()
// 清除标记,确保只弹一次
sessionStorage.removeItem('needSetPassword')
sessionStorage.setItem('hasShownPasswordDialog', '1')
} else if (needSetPasswordFlag && !hasNoPassword) {
// 如果有标记但用户已经有密码了,清除标记
console.log('用户已有密码,清除 needSetPassword 标记')
sessionStorage.removeItem('needSetPassword')
}
} else {
console.error('获取用户信息失败:', response?.data?.message || '未知错误')
ElMessage.error(t('profile.loadUserInfoFailed'))
}
} catch (error) {
console.error('加载用户信息失败:', error)
ElMessage.error(t('profile.loadUserInfoFailed') + ': ' + (error.message || '未知错误'))
} finally {
userLoading.value = false
}
}
// 加载用户作品列表
const loadVideos = async () => {
loading.value = true
try {
const response = await getMyWorks({
page: 0,
size: 6 // 只加载前6个作品
})
console.log('获取作品列表响应:', response)
if (response && response.data && response.data.success) {
const data = response.data.data || []
console.log('作品数据:', data)
// 转换数据格式
videos.value = data.map(transformWorkData)
console.log('转换后的作品列表:', videos.value)
} else {
console.error('获取作品列表失败:', response?.data?.message || '未知错误')
}
} catch (error) {
console.error('加载作品列表失败:', error)
ElMessage.error(t('profile.loadWorksFailed') + ': ' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
// 编辑个人资料
const editProfile = () => {
// TODO: 可以跳转到编辑页面或打开编辑对话框
ElMessage.info(t('profile.profileEditDevMsg'))
}
// 点击外部关闭菜单
const handleClickOutside = (event) => {
const userStatus = event.target.closest('.user-status')
if (!userStatus) {
showUserMenu.value = false
}
}
// 个人主页“已发布”区不跳转,保持静态展示,与“我的作品”点击打开详情逻辑不同
// 视频加载元数据后,跳转到第一帧(但不播放)
const onVideoLoaded = (event) => {
const video = event.target
if (video && video.duration) {
// 跳转到第一帧0秒
video.currentTime = 0.1
// 确保视频不播放
video.pause()
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
loadUserInfo()
loadVideos()
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.profile-page {
min-height: 100vh;
background: #0a0a0a;
color: white;
display: flex;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: visible;
position: relative;
}
/* 页面特殊效果 */
.profile-page::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 10% 20%, rgba(64, 158, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 90% 80%, rgba(103, 194, 58, 0.1) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(230, 162, 60, 0.05) 0%, transparent 50%);
animation: profileGlow 6s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes profileGlow {
0% { opacity: 0.3; }
100% { opacity: 0.6; }
}
/* 内容层级 */
.profile-page > * {
position: relative;
z-index: 1;
}
/* 左侧导航栏 */
.sidebar {
width: 280px !important;
background: #000000 !important;
padding: 24px 0 !important;
border-right: 1px solid #1a1a1a !important;
flex-shrink: 0 !important;
z-index: 100 !important;
display: block !important;
position: relative !important;
}
.logo {
padding: 0 24px 32px;
display: flex;
align-items: center;
justify-content: center;
}
.logo img {
height: 40px;
width: auto;
}
.nav-menu, .tools-menu {
padding: 0 24px;
}
.nav-item {
display: flex;
align-items: center;
padding: 14px 18px;
margin-bottom: 4px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.nav-item:hover {
background: #2a2a2a;
}
.nav-item.active {
background: #1e3a8a;
}
.nav-item .el-icon {
margin-right: 14px;
font-size: 20px;
}
.nav-item span {
font-size: 15px;
flex: 1;
}
.sora-tag {
margin-left: 8px;
font-size: 10px;
padding: 2px 6px;
}
.divider {
margin: 30px 20px 20px;
padding: 0 16px;
color: #666;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
}
/* 主内容区域 */
.main-content {
flex: 1;
padding: 0;
}
/* 顶部栏 */
.top-header {
height: 80px;
padding: 0 30px;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
justify-content: flex-end;
position: relative;
z-index: 99999;
background: #0a0a0a;
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
}
.points {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(64, 158, 255, 0.1);
border-radius: 20px;
border: 1px solid rgba(64, 158, 255, 0.3);
}
.points-icon {
width: 20px;
height: 20px;
background: #409EFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.points-number {
color: #409EFF;
font-size: 14px;
font-weight: 600;
}
.user-status {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
z-index: 100000;
transition: all 0.3s ease;
overflow: hidden;
}
.user-status:hover {
transform: scale(1.05);
}
.status-icon {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
/* 用户菜单样式 */
.user-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 160px;
z-index: 99999;
overflow: hidden;
}
/* Teleport菜单样式 */
.user-menu-teleport {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 160px;
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s ease;
color: white;
font-size: 14px;
}
.menu-item:hover {
background: #2a2a2a;
}
.menu-item .el-icon {
margin-right: 8px;
font-size: 16px;
}
.menu-item:not(:last-child) {
border-bottom: 1px solid #333;
}
/* 用户资料区域 */
.profile-section {
padding: 30px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
margin: 20px 30px;
border-radius: 12px;
border: 1px solid #333;
position: relative;
z-index: 1;
}
.profile-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(64, 158, 255, 0.1) 0%, transparent 70%);
border-radius: 12px;
pointer-events: none;
}
.profile-info {
display: flex;
align-items: center;
gap: 20px;
position: relative;
z-index: 1;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: #409EFF;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.avatar-icon {
width: 40px;
height: 40px;
background: #1a1a2e;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-icon::before {
content: '';
width: 16px;
height: 16px;
background: white;
border-radius: 2px;
}
.avatar-image {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.user-details {
flex: 1;
}
.username {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
color: white;
}
.profile-status {
font-size: 14px;
color: #999;
margin: 0 0 4px 0;
}
.user-id {
font-size: 12px;
color: #666;
margin: 0;
}
.edit-btn {
background: #2a2a2a;
border: 1px solid #444;
color: white;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
}
.edit-btn:hover {
background: #3a3a3a;
border-color: #555;
}
/* 已发布内容 */
.published-section {
padding: 0 30px 30px;
}
.section-title {
font-size: 20px;
font-weight: 600;
margin: 0 0 20px 0;
color: white;
position: relative;
padding-bottom: 8px;
}
.section-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 3px;
background: #1e3a8a;
border-radius: 2px;
}
.video-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.video-item {
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
border: 1px solid #333;
transition: all 0.3s ease;
}
.video-item:hover {
transform: translateY(-2px);
border-color: #444;
}
.video-thumbnail {
position: relative;
}
.thumbnail-image {
height: 200px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.video-cover-img {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.video-cover-img video {
width: 100%;
height: 100%;
object-fit: cover;
}
.figure {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
position: relative;
}
.figure::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.empty-works {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #999;
}
.empty-text {
font-size: 16px;
}
.text-overlay {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
color: white;
font-size: 14px;
font-weight: 500;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.video-action {
padding: 15px;
text-align: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.video-item:hover .video-action {
opacity: 1;
}
.director-text {
font-size: 12px;
color: #999;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.video-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.profile-page {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu, .tools-menu {
display: flex;
overflow-x: auto;
padding: 0 20px;
}
.nav-item {
white-space: nowrap;
margin-right: 10px;
}
.video-grid {
grid-template-columns: 1fr;
}
.profile-info {
flex-direction: column;
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;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.detail-right .avatar .avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.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>