- 修复BCryptPasswordEncoder密码验证问题 - 实现密码设置提示弹窗功能(仅对无密码用户显示一次) - 优化修改密码逻辑和验证流程 - 更新Welcome页面背景样式 - 清理临时SQL文件和测试代码 - 移动数据库备份文件到database/backups目录 - 删除不必要的MD文档和临时文件
1513 lines
37 KiB
Vue
1513 lines
37 KiB
Vue
<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
|
||
// 如果已经是完整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: 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>
|