feat: 完成管理员密码登录修复和项目清理
- 修复BCryptPasswordEncoder密码验证问题 - 实现密码设置提示弹窗功能(仅对无密码用户显示一次) - 优化修改密码逻辑和验证流程 - 更新Welcome页面背景样式 - 清理临时SQL文件和测试代码 - 移动数据库备份文件到database/backups目录 - 删除不必要的MD文档和临时文件
This commit is contained in:
@@ -11,36 +11,36 @@
|
||||
<nav class="nav-menu">
|
||||
<div class="nav-item active">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>个人主页</span>
|
||||
<span>{{ t('profile.title') }}</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<el-icon><Compass /></el-icon>
|
||||
<span @click="goToSubscription">会员订阅</span>
|
||||
<span @click="goToSubscription">{{ t('profile.subscription') }}</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span @click="goToMyWorks">我的作品</span>
|
||||
<span @click="goToMyWorks">{{ t('profile.myWorks') }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- 工具分隔线 -->
|
||||
<div class="divider">
|
||||
<span>工具</span>
|
||||
<span>{{ t('profile.tools') }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 工具菜单 -->
|
||||
<nav class="tools-menu">
|
||||
<div class="nav-item">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
<span @click="goToTextToVideo">文生视频</span>
|
||||
<span @click="goToTextToVideo">{{ t('home.textToVideo') }}</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span @click="goToImageToVideo">图生视频</span>
|
||||
<span @click="goToImageToVideo">{{ t('home.imageToVideo') }}</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<el-icon><Film /></el-icon>
|
||||
<span @click="goToStoryboardVideo">分镜视频</span>
|
||||
<span @click="goToStoryboardVideo">{{ t('home.storyboardVideo') }}</span>
|
||||
<el-tag size="small" type="primary" class="sora-tag">Sora2.0</el-tag>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
<div class="user-status" @click="showUserMenu = !showUserMenu" ref="userStatusRef">
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="status-icon" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" :alt="t('dashboard.userAvatar')" class="status-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -68,19 +68,19 @@
|
||||
<section class="profile-section">
|
||||
<div class="profile-info">
|
||||
<div class="avatar">
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="avatar" class="avatar-image" />
|
||||
<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 || '未设置用户名' }}</h2>
|
||||
<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">ID {{ userInfo.id || '加载中...' }}</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">已发布</h3>
|
||||
<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)">
|
||||
@@ -97,21 +97,21 @@
|
||||
<!-- 如果有封面图(thumbnailUrl),使用图片 -->
|
||||
<img
|
||||
v-else-if="video.cover && video.cover !== video.resultUrl"
|
||||
:src="video.cover"
|
||||
:alt="video.title"
|
||||
class="video-cover-img"
|
||||
: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)">做同款</el-button>
|
||||
<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">暂无作品,开始创作吧!</div>
|
||||
<div class="empty-text">{{ t('profile.noWorksYet') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -139,7 +139,7 @@
|
||||
:poster="selectedItem.cover"
|
||||
controls
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
{{ t('profile.browserNotSupport') }}
|
||||
</video>
|
||||
<img
|
||||
v-else
|
||||
@@ -154,26 +154,26 @@
|
||||
<div class="detail-header">
|
||||
<div class="user-info">
|
||||
<div class="avatar">
|
||||
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" class="avatar-image" />
|
||||
<img src="/images/backgrounds/avatar-default.svg" :alt="t('dashboard.userAvatar')" class="avatar-image" />
|
||||
</div>
|
||||
<div class="username">{{ (selectedItem && selectedItem.username) || '匿名用户' }}</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'">作品详情</div>
|
||||
<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">提示词</h3>
|
||||
<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">输入详情</h3>
|
||||
<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" />
|
||||
@@ -183,53 +183,53 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="description-section">
|
||||
<h3 class="section-title">提示词</h3>
|
||||
<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">提示词</h3>
|
||||
<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">创建时间</span>
|
||||
<span class="label">{{ t('profile.createTime') }}</span>
|
||||
<span class="value">{{ selectedItem.createTime }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="label">作品 ID</span>
|
||||
<span class="label">{{ t('profile.workId') }}</span>
|
||||
<span class="value">{{ selectedItem.id }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="label">日期</span>
|
||||
<span class="label">{{ t('profile.date') }}</span>
|
||||
<span class="value">{{ selectedItem.date }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="label">分类</span>
|
||||
<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">时长</span>
|
||||
<span class="value">{{ formatDuration(selectedItem.duration) || '未知' }}</span>
|
||||
<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">清晰度</span>
|
||||
<span class="value">{{ selectedItem.quality || '未知' }}</span>
|
||||
<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">宽高比</span>
|
||||
<span class="value">{{ selectedItem.aspectRatio || '未知' }}</span>
|
||||
<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>
|
||||
@@ -243,28 +243,72 @@
|
||||
<template v-if="userStore.isAdmin">
|
||||
<div class="menu-item" @click="goToDashboard">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>数据仪表盘</span>
|
||||
<span>{{ t('profile.dashboard') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="goToOrders">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>订单管理</span>
|
||||
<span>{{ t('profile.orderManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="goToMembers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>会员管理</span>
|
||||
<span>{{ t('profile.memberManagement') }}</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="goToSettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
<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>退出登录</span>
|
||||
<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>
|
||||
@@ -277,6 +321,7 @@ import {
|
||||
Document,
|
||||
Star,
|
||||
Setting,
|
||||
Lock,
|
||||
Compass,
|
||||
VideoPlay,
|
||||
Picture,
|
||||
@@ -284,11 +329,13 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getMyWorks } from '@/api/userWorks'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
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)
|
||||
@@ -304,6 +351,56 @@ const userInfo = ref({
|
||||
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)
|
||||
|
||||
// 视频数据
|
||||
@@ -356,14 +453,14 @@ const goToDashboard = () => {
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/admin/dashboard')
|
||||
} else {
|
||||
ElMessage.warning('权限不足,只有管理员才能访问数据仪表盘')
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到订单管理
|
||||
const goToOrders = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/orders')
|
||||
router.push('/admin/orders')
|
||||
}
|
||||
|
||||
// 跳转到会员管理
|
||||
@@ -373,7 +470,7 @@ const goToMembers = () => {
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/member-management')
|
||||
} else {
|
||||
ElMessage.warning('权限不足,只有管理员才能访问会员管理')
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +481,7 @@ const goToSettings = () => {
|
||||
if (userStore.isAdmin) {
|
||||
router.push('/system-settings')
|
||||
} else {
|
||||
ElMessage.warning('权限不足,只有管理员才能访问系统设置')
|
||||
ElMessage.warning(t('profile.insufficientPermission'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,21 +489,21 @@ const goToSettings = () => {
|
||||
const logout = async () => {
|
||||
try {
|
||||
showUserMenu.value = false
|
||||
|
||||
|
||||
// 清除用户数据
|
||||
await userStore.logoutUser()
|
||||
|
||||
|
||||
// 清除其他可能的本地存储
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('token')
|
||||
|
||||
ElMessage.success('已退出登录')
|
||||
|
||||
|
||||
ElMessage.success(t('profile.logoutSuccess'))
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
ElMessage.error('退出登录失败')
|
||||
ElMessage.error(t('profile.logoutFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,11 +520,11 @@ const openDetail = async (item) => {
|
||||
selectedItem.value = transformWorkData(work)
|
||||
} else {
|
||||
console.error('获取作品详情失败:', response?.data?.message || '未知错误')
|
||||
ElMessage.error('获取作品详情失败')
|
||||
ElMessage.error(t('profile.loadDetailFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载作品详情失败:', error)
|
||||
ElMessage.error('加载作品详情失败: ' + (error.message || '未知错误'))
|
||||
ElMessage.error(t('profile.loadDetailFailed') + ': ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,9 +542,9 @@ const getDescription = (item) => {
|
||||
if (desc) return desc
|
||||
// 回退文案
|
||||
if (item.type === 'video') {
|
||||
return '暂无提示词'
|
||||
return t('profile.noPrompt')
|
||||
}
|
||||
return '暂无提示词'
|
||||
return t('profile.noPrompt')
|
||||
}
|
||||
|
||||
// 格式化时长
|
||||
@@ -531,13 +628,36 @@ const loadUserInfo = async () => {
|
||||
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('获取用户信息失败')
|
||||
ElMessage.error(t('profile.loadUserInfoFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败:', error)
|
||||
ElMessage.error('加载用户信息失败: ' + (error.message || '未知错误'))
|
||||
ElMessage.error(t('profile.loadUserInfoFailed') + ': ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
userLoading.value = false
|
||||
}
|
||||
@@ -564,7 +684,7 @@ const loadVideos = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载作品列表失败:', error)
|
||||
ElMessage.error('加载作品列表失败: ' + (error.message || '未知错误'))
|
||||
ElMessage.error(t('profile.loadWorksFailed') + ': ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -573,7 +693,7 @@ const loadVideos = async () => {
|
||||
// 编辑个人资料
|
||||
const editProfile = () => {
|
||||
// TODO: 可以跳转到编辑页面或打开编辑对话框
|
||||
ElMessage.info('个人简介编辑功能待实现')
|
||||
ElMessage.info(t('profile.profileEditDevMsg'))
|
||||
}
|
||||
|
||||
// 点击外部关闭菜单
|
||||
|
||||
Reference in New Issue
Block a user