feat: 完成管理员密码登录修复和项目清理

- 修复BCryptPasswordEncoder密码验证问题
- 实现密码设置提示弹窗功能(仅对无密码用户显示一次)
- 优化修改密码逻辑和验证流程
- 更新Welcome页面背景样式
- 清理临时SQL文件和测试代码
- 移动数据库备份文件到database/backups目录
- 删除不必要的MD文档和临时文件
This commit is contained in:
AIGC Developer
2025-11-21 16:10:00 +08:00
parent 2961d2b0d0
commit dbd06435cb
384 changed files with 8064 additions and 5080 deletions

View File

@@ -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'))
}
// 点击外部关闭菜单