登录后修改密码🐱

This commit is contained in:
Kunagisa 2025-07-13 21:40:32 +08:00
parent bcf943d9f1
commit 46fa0a7668
10 changed files with 1226 additions and 20 deletions

View File

@ -182,3 +182,18 @@ export const resetPassword = async (token, password) => {
}
}
/**
* 忘记密码
* @param {string} qq - QQ号
* @param {string} new_password - 新密码
* @param {string} token - 验证码token
* @param {string} captcha - 用户输入的验证码
*/
export const forgetPassword = async (qq, new_password, token, captcha) => {
try {
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,207 @@
<template>
<div v-if="visible" class="password-dialog-overlay" @click.self="handleClose">
<div class="password-dialog">
<div class="password-dialog-content">
<div class="dialog-title">修改密码</div>
<div class="confirm-content">
<p class="confirm-message">是否要修改密码</p>
</div>
</div>
<div class="password-dialog-footer">
<button class="cancel-button" @click="handleClose" :disabled="loading">取消</button>
<button
class="confirm-button"
@click="handleSubmit"
:disabled="loading"
>
{{ loading ? '修改中...' : '确定' }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue'
import { requestResetPassword, resetPassword } from '@/api/login.js'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'success', 'error'])
const loading = ref(false)
//
watch(() => props.visible, (visible) => {
if (visible) {
loading.value = false
}
})
const handleClose = () => {
if (!loading.value) {
emit('close')
}
}
const handleSubmit = async () => {
if (loading.value) {
return
}
loading.value = true
try {
//
await requestResetPassword()
emit('success', '密码修改请求已发送!')
emit('close')
} catch (error) {
console.error('请求修改密码失败:', error)
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || '请求修改密码失败,请重试'
emit('error', errorMessage)
} finally {
loading.value = false
}
}
</script>
<style scoped>
.password-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.password-dialog {
background: white;
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
animation: dialog-fade-in 0.3s ease;
}
.password-dialog-content {
padding: 20px;
text-align: center;
}
.dialog-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
}
.confirm-content {
margin-bottom: 0;
}
.confirm-message {
font-size: 16px;
color: #666;
margin: 0;
line-height: 1.5;
}
.password-dialog-footer {
padding: 10px 20px 20px;
text-align: center;
display: flex;
justify-content: center;
gap: 12px;
}
.cancel-button, .confirm-button {
padding: 8px 24px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.cancel-button {
background-color: #909399;
color: white;
}
.cancel-button:hover:not(:disabled) {
background-color: #a6a9ad;
}
.confirm-button {
background-color: #67c23a;
color: white;
}
.confirm-button:hover:not(:disabled) {
background-color: #85ce61;
}
.confirm-button:disabled {
background-color: #c0c4cc;
cursor: not-allowed;
}
.cancel-button:disabled {
background-color: #c0c4cc;
cursor: not-allowed;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 移动端适配 */
@media screen and (max-width: 480px) {
.password-dialog {
width: 85%;
}
.dialog-title {
font-size: 16px;
}
.password-dialog-content {
padding: 16px;
}
.confirm-message {
font-size: 14px;
}
.password-dialog-footer {
flex-direction: column;
gap: 8px;
}
.cancel-button, .confirm-button {
width: 100%;
padding: 10px;
font-size: 13px;
}
}
</style>

View File

@ -47,7 +47,7 @@ const handleClose = () => {
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
z-index: 2000;
}
.error-dialog {

View File

@ -0,0 +1,498 @@
<template>
<div class="login-form">
<div>忘记密码</div>
<form class="login-form-container" @submit.prevent="handleForgetPassword">
<div class="input-container">
<label for="username">QQ号</label>
<input
type="text"
id="username"
v-model="username"
placeholder="请输入QQ号"
:class="{ 'error': usernameError }"
/>
<span class="error-message" v-if="usernameError">{{ usernameError }}</span>
</div>
<div class="input-container">
<label for="newPassword">新密码</label>
<input
type="password"
id="newPassword"
v-model="newPassword"
placeholder="请输入新密码"
:class="{ 'error': newPasswordError }"
/>
<span class="error-message" v-if="newPasswordError">{{ newPasswordError }}</span>
</div>
<div class="input-container">
<label for="confirmPassword">确认新密码</label>
<input
type="password"
id="confirmPassword"
v-model="confirmPassword"
placeholder="请再次输入新密码"
:class="{ 'error': confirmPasswordError }"
/>
<span class="error-message" v-if="confirmPasswordError">{{ confirmPasswordError }}</span>
</div>
<div class="input-container captcha-container">
<label for="captcha">验证码</label>
<div class="captcha-wrapper">
<input
type="text"
id="captcha"
v-model="captcha"
placeholder="请输入验证码"
:class="{ 'error': captchaError }"
/>
<img
v-if="captchaImage"
:src="captchaImage"
alt="验证码"
class="captcha-image"
@click="refreshCaptcha"
/>
</div>
<span class="error-message" v-if="captchaError">{{ captchaError }}</span>
</div>
<div class="login-button">
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '重置密码' }}
</button>
</div>
<div class="register-link">
<a @click.prevent="$emit('login')">返回登录</a>
</div>
</form>
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
<SuccessDialog
:visible="showSuccess"
:title="successTitle"
:message="successMessage"
@close="handleSuccessClose"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getCaptcha, forgetPassword } from '../api/login'
import ErrorDialog from './ErrorDialog.vue'
import SuccessDialog from './SuccessDialog.vue'
const username = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const captcha = ref('')
const captchaImage = ref('')
const captchaToken = ref('')
//
const usernameError = ref('')
const newPasswordError = ref('')
const confirmPasswordError = ref('')
const captchaError = ref('')
//
const isSubmitting = ref(false)
//
const showError = ref(false)
const errorTitle = ref('错误提示')
const errorMessage = ref('')
//
const showSuccess = ref(false)
const successTitle = ref('成功')
const successMessage = ref('密码重置成功,请使用新密码登录')
const showErrorMessage = (message, title = '错误提示') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
const showSuccessMessage = (message, title = '成功') => {
successMessage.value = message
successTitle.value = title
showSuccess.value = true
}
const refreshCaptcha = async () => {
try {
const response = await getCaptcha()
captchaImage.value = `data:image/png;base64,${response.img}`
captchaToken.value = response.token
captcha.value = '' //
} catch (error) {
console.error('获取验证码失败:', error)
const apiError = error.response?.data?.detail || error.response?.data?.message
if (apiError) {
showErrorMessage(apiError)
} else {
showErrorMessage('获取验证码失败,请刷新页面重试')
}
}
}
const handleForgetPassword = async () => {
try {
//
if (!validateForm()) {
return
}
isSubmitting.value = true
// API
await forgetPassword(
username.value,
newPassword.value,
captchaToken.value,
captcha.value
)
//
showSuccessMessage('密码重置成功,请使用新密码登录')
} catch (error) {
console.error('重置密码失败:', error)
if (error.response) {
const apiError = error.response?.data?.detail || error.response?.data?.message
if (apiError) {
showErrorMessage(apiError)
} else {
showErrorMessage('重置密码失败,请稍后重试')
}
} else if (error.message === 'Network Error') {
showErrorMessage('无法连接服务器,请稍后重试')
} else {
showErrorMessage(error.message || '重置密码失败,请稍后重试')
}
//
refreshCaptcha()
} finally {
isSubmitting.value = false
}
}
const validateForm = () => {
//
usernameError.value = ''
newPasswordError.value = ''
confirmPasswordError.value = ''
captchaError.value = ''
//
if (!username.value) {
usernameError.value = '请输入QQ号码'
return false
}
//
if (!newPassword.value) {
newPasswordError.value = '请输入新密码'
return false
}
if (newPassword.value.length < 4) {
newPasswordError.value = '密码长度不能小于4个字符'
return false
}
if (newPassword.value.length > 20) {
newPasswordError.value = '密码长度不能超过20个字符'
return false
}
//
if (!confirmPassword.value) {
confirmPasswordError.value = '请确认新密码'
return false
}
if (newPassword.value !== confirmPassword.value) {
confirmPasswordError.value = '两次输入的密码不一致'
return false
}
//
if (!captcha.value) {
captchaError.value = '请输入验证码'
return false
}
if (captcha.value.length !== 4) {
captchaError.value = '验证码长度不正确'
return false
}
return true
}
const handleSuccessClose = () => {
showSuccess.value = false
//
resetForm()
//
$emit('login')
}
//
const resetForm = () => {
username.value = ''
newPassword.value = ''
confirmPassword.value = ''
captcha.value = ''
usernameError.value = ''
newPasswordError.value = ''
confirmPasswordError.value = ''
captchaError.value = ''
refreshCaptcha()
}
// resetForm
defineExpose({
resetForm
})
onMounted(() => {
refreshCaptcha()
})
</script>
<style scoped>
.login-form {
width: 340px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.10);
padding: 36px 32px 28px 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.login-form>div:first-child {
font-size: 26px;
font-weight: bold;
color: #222;
margin-bottom: 28px;
letter-spacing: 2px;
}
.login-form-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 18px;
}
.input-container {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-container label {
font-size: 14px;
color: #666;
margin-bottom: 2px;
}
.input-container input {
height: 40px;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 0 12px;
font-size: 15px;
background: #f7fbfd;
transition: border 0.2s;
}
.input-container input:focus {
border: 1.5px solid #409eff;
outline: none;
background: #fff;
}
.input-container input.error {
border-color: #f56c6c;
}
.error-message {
color: #f56c6c;
font-size: 12px;
margin-top: 2px;
}
.login-button {
margin-top: 10px;
}
.login-button button {
width: 100%;
height: 42px;
background: linear-gradient(90deg, #409eff 0%, #6dd5fa 100%);
color: #fff;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.10);
transition: background 0.2s;
}
.login-button button:hover {
background: linear-gradient(90deg, #66b1ff 0%, #6dd5fa 100%);
}
.login-button button:disabled {
background: #a0cfff;
cursor: not-allowed;
}
.register-link {
margin-top: 12px;
text-align: right;
}
.register-link a {
color: #409eff;
font-size: 14px;
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.register-link a:hover {
color: #1a73e8;
text-decoration: underline;
}
/* VAPTCHA 相关样式 */
.VAPTCHA-init-main {
display: table;
width: 100%;
height: 100%;
background-color: #f7fbfd;
border-radius: 6px;
border: 1px solid #d0d7de;
}
.VAPTCHA-init-loading {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.VAPTCHA-init-loading>a {
display: inline-block;
width: 18px;
height: 18px;
border: none;
}
.VAPTCHA-init-loading .VAPTCHA-text {
font-family: sans-serif;
font-size: 12px;
color: #666;
vertical-align: middle;
}
.captcha-container {
margin-bottom: 16px;
width: 100%;
}
.captcha-wrapper {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
position: relative;
}
.captcha-wrapper input {
flex: 1;
height: 40px;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 0 12px;
font-size: 15px;
background: #f7fbfd;
transition: border 0.2s;
width: calc(100% - 120px); /* 减去验证码图片的宽度和间距 */
}
.captcha-wrapper input:focus {
border: 1.5px solid #409eff;
outline: none;
background: #fff;
}
.captcha-image {
width: 110px;
height: 40px;
border-radius: 6px;
cursor: pointer;
object-fit: cover;
border: 1px solid #d0d7de;
transition: all 0.3s;
}
.captcha-image:hover {
opacity: 0.8;
}
/* 移动端适配 */
@media screen and (max-width: 480px) {
.login-form {
width: 90%;
max-width: 340px;
padding: 24px 20px 20px 20px;
}
.captcha-wrapper {
gap: 6px;
}
.captcha-wrapper input {
width: calc(100% - 100px);
}
.captcha-image {
width: 90px;
}
.input-container input,
.captcha-wrapper input {
height: 38px;
font-size: 14px;
}
.login-button button {
height: 40px;
font-size: 15px;
}
}
/* 超小屏幕适配 */
@media screen and (max-width: 320px) {
.login-form {
padding: 20px 16px 16px 16px;
}
.captcha-wrapper input {
width: calc(100% - 90px);
padding: 0 8px;
}
.captcha-image {
width: 80px;
}
}
</style>

View File

@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -47,7 +47,10 @@
<div class="login-button">
<button type="submit">登录</button>
</div>
<div class="forget-password">
</div>
<div class="register-link">
<a @click.prevent="$emit('forget')">忘记密码</a>
<a @click.prevent="$emit('register')">注册账号</a>
</div>
</form>
@ -296,8 +299,9 @@ onMounted(() => {
}
.register-link {
display: flex;
justify-content: space-between;
margin-top: 12px;
text-align: right;
}
.register-link a {
@ -313,6 +317,7 @@ onMounted(() => {
text-decoration: underline;
}
/* VAPTCHA 相关样式 */
.VAPTCHA-init-main {
display: table;

View File

@ -119,6 +119,11 @@ const routes = [
meta: { requiresAuth: true }
}
]
},
{
path: '/user/resetpassword/:token',
name: 'ResetPassword',
component: () => import('@/views/ResetPassword.vue')
}
]

435
src/views/ResetPassword.vue Normal file
View File

@ -0,0 +1,435 @@
<template>
<div class="reset-password-container">
<div class="bg-container">
<img :src="bgImg" alt="重置密码背景" />
</div>
<div class="bottom-text">
<p>© Byz解忧杂货铺</p>
</div>
<div class="content-container">
<div class="form-content">
<div class="reset-form">
<div class="form-title">重置密码</div>
<form class="reset-form-container" @submit.prevent="handleResetPassword">
<div class="input-container">
<label for="newPassword">新密码</label>
<div class="password-input-wrapper">
<input
v-model="newPassword"
:type="showPassword ? 'text' : 'password'"
id="newPassword"
placeholder="请输入新密码"
:disabled="loading"
@keyup.enter="handleResetPassword"
/>
<i
:class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"
class="password-toggle"
@click="showPassword = !showPassword"
></i>
</div>
</div>
<div class="input-container">
<label for="confirmPassword">确认新密码</label>
<div class="password-input-wrapper">
<input
v-model="confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
id="confirmPassword"
placeholder="请再次输入新密码"
:disabled="loading"
@keyup.enter="handleResetPassword"
/>
<i
:class="showConfirmPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"
class="password-toggle"
@click="showConfirmPassword = !showConfirmPassword"
></i>
</div>
</div>
<div class="password-tips">
<p>密码长度4-20个字符</p>
</div>
<div class="reset-button">
<button type="submit" :disabled="loading || !isFormValid">
{{ loading ? '重置中...' : '重置密码' }}
</button>
</div>
<div class="back-link">
<a @click.prevent="goToHome">返回首页</a>
</div>
</form>
</div>
</div>
</div>
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
<SuccessDialog
:visible="showSuccess"
:title="successTitle"
:message="successMessage"
@close="handleSuccessClose"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { resetPassword } from '@/api/login.js'
import ErrorDialog from '@/components/ErrorDialog.vue'
import SuccessDialog from '@/components/SuccessDialog.vue'
import loginBg from '@/assets/login_1.jpg'
import loginBg1 from '@/assets/login_2.jpg'
import loginBg3 from '@/assets/login_3.jpg'
const route = useRoute()
const router = useRouter()
const images = [loginBg, loginBg1, loginBg3]
const randomIndex = Math.floor(Math.random() * images.length)
const bgImg = ref(images[randomIndex])
const newPassword = ref('')
const confirmPassword = ref('')
const showPassword = ref(false)
const showConfirmPassword = ref(false)
const loading = ref(false)
//
const showError = ref(false)
const errorTitle = ref('错误提示')
const errorMessage = ref('')
//
const showSuccess = ref(false)
const successTitle = ref('成功')
const successMessage = ref('密码重置成功!')
const showErrorMessage = (message, title = '错误提示') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
const showSuccessMessage = (message, title = '成功') => {
successMessage.value = message
successTitle.value = title
showSuccess.value = true
}
//
const isFormValid = computed(() => {
return newPassword.value.trim() &&
confirmPassword.value.trim() &&
newPassword.value === confirmPassword.value &&
newPassword.value.length >= 4 &&
newPassword.value.length <= 20
})
const handleResetPassword = async () => {
if (!isFormValid.value || loading.value) {
return
}
// token
const token = route.params.token
if (!token) {
showErrorMessage('无效的重置链接')
return
}
loading.value = true
try {
// API
await resetPassword(token, newPassword.value)
showSuccessMessage('密码重置成功!请使用新密码登录。')
} catch (error) {
console.error('重置密码失败:', error)
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || '重置密码失败,请重试'
showErrorMessage(errorMessage)
} finally {
loading.value = false
}
}
const handleSuccessClose = () => {
showSuccess.value = false
goToHome()
}
const goToHome = () => {
router.push('/')
}
onMounted(() => {
// token
const token = route.params.token
if (!token) {
showErrorMessage('无效的重置链接')
}
})
</script>
<style scoped>
.reset-password-container {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}
.bg-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.bg-container img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: left;
}
.content-container {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
z-index: 2;
padding: 2rem;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.form-content {
width: 100%;
margin-top: 48px;
display: flex;
flex-direction: column;
align-items: center;
}
.reset-form {
width: 340px;
margin: 0 auto;
background: rgba(255,255,255,0.95);
border-radius: 16px;
box-shadow: 0 4px 32px rgba(0,0,0,0.10);
padding: 36px 32px 28px 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.form-title {
font-size: 26px;
font-weight: bold;
color: #222;
margin-bottom: 28px;
letter-spacing: 2px;
}
.reset-form-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 18px;
}
.input-container {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-container label {
font-size: 14px;
color: #666;
margin-bottom: 2px;
}
.password-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-input-wrapper input {
width: 100%;
padding: 12px 16px;
padding-right: 40px;
border: 1px solid #d0d7de;
border-radius: 6px;
font-size: 15px;
background: #f7fbfd;
transition: border 0.2s;
outline: none;
box-sizing: border-box;
}
.password-input-wrapper input:focus {
border: 1.5px solid #409eff;
background: #fff;
}
.password-input-wrapper input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #666;
font-size: 16px;
transition: color 0.3s;
z-index: 1;
}
.password-toggle:hover {
color: #409eff;
}
.password-tips {
margin-top: 12px;
text-align: left;
}
.password-tips p {
margin: 0;
font-size: 12px;
color: #666;
}
.reset-button {
margin-top: 10px;
}
.reset-button button {
width: 100%;
height: 42px;
background: linear-gradient(90deg, #409eff 0%, #6dd5fa 100%);
color: #fff;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 8px rgba(64,158,255,0.10);
transition: background 0.2s;
}
.reset-button button:hover:not(:disabled) {
background: linear-gradient(90deg, #66b1ff 0%, #6dd5fa 100%);
}
.reset-button button:disabled {
background: #a0cfff;
cursor: not-allowed;
}
.back-link {
margin-top: 12px;
text-align: center;
}
.back-link a {
color: #409eff;
font-size: 14px;
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.back-link a:hover {
color: #1a73e8;
text-decoration: underline;
}
.bottom-text {
position: absolute;
left: 24px;
bottom: 24px;
z-index: 2;
color: #fff;
font-size: 16px;
font-weight: 300;
letter-spacing: 1px;
text-shadow: 0 2px 8px rgba(0,0,0,0.25);
background: rgba(0, 0, 0, 0.18);
border-radius: 8px;
padding: 6px 18px;
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
user-select: none;
transition: background 0.3s;
}
.bottom-text p {
margin: 0;
opacity: 0.92;
}
@media (max-width: 600px) {
.content-container {
width: 100vw;
height: 100vh;
min-width: 0;
left: 0;
top: 0;
right: 0;
bottom: 0;
transform: none;
border-radius: 0;
background: rgba(0,0,0,0.18);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
padding: 0 8px;
justify-content: flex-start;
}
.form-content {
margin-top: 64px;
width: 100%;
align-items: center;
}
.reset-form {
width: 90%;
max-width: 340px;
padding: 24px 20px 20px 20px;
}
.password-input-wrapper input {
height: 38px;
font-size: 14px;
}
.reset-button button {
height: 40px;
font-size: 15px;
}
}
</style>

View File

@ -11,8 +11,20 @@
返回主界面
</button>
<div class="form-content">
<LoginModule ref="loginModuleRef" v-if="!showRegister" @register="showRegister = true" />
<RegisterModule v-else @login="handleSwitchToLogin" />
<LoginModule
ref="loginModuleRef"
v-if="!showRegister && !showForget"
@register="showRegister = true"
@forget="handleSwitchToForget"
/>
<RegisterModule
v-else-if="showRegister"
@login="handleSwitchToLogin"
/>
<ForgetModule
v-else-if="showForget"
@login="handleSwitchToLogin"
/>
</div>
</div>
</div>
@ -25,6 +37,7 @@ import { hasValidToken } from '@/utils/jwt'
import loginBg from '@/assets/login_1.jpg'
import loginBg1 from '@/assets/login_2.jpg'
import loginBg3 from '@/assets/login_3.jpg'
import ForgetModule from "@/components/forget_module.vue";
import LoginModule from '@/components/login_module.vue'
import RegisterModule from '@/components/register_module.vue'
@ -34,6 +47,7 @@ const bgImg = ref(images[randomIndex])
const router = useRouter()
const showRegister = ref(false)
const showForget = ref(false)
const loginModuleRef = ref(null)
const handleBack = () => {
@ -43,6 +57,7 @@ const handleBack = () => {
//
const handleSwitchToLogin = () => {
showRegister.value = false
showForget.value = false
// tick
setTimeout(() => {
if (loginModuleRef.value) {
@ -51,6 +66,12 @@ const handleSwitchToLogin = () => {
}, 0)
}
//
const handleSwitchToForget = () => {
showForget.value = true
showRegister.value = false
}
//
onMounted(() => {
if (hasValidToken()) {

View File

@ -9,6 +9,7 @@ const PrivilegeRequestDialog = defineAsyncComponent(() => import('@/components/P
const SuccessDialog = defineAsyncComponent(() => import('@/components/SuccessDialog.vue'))
const ErrorDialog = defineAsyncComponent(() => import('@/components/ErrorDialog.vue'))
const ChangeUsernameDialog = defineAsyncComponent(() => import('@/components/ChangeUsernameDialog.vue'))
const ChangePasswordDialog = defineAsyncComponent(() => import('@/components/ChangePasswordDialog.vue'))
const isLoggedIn = computed(() => {
return !!localStorage.getItem('access_token') && !!currentUserData.value
@ -210,6 +211,7 @@ const privilegeDialogName = ref('')
const privilegeDialogKey = ref('')
const successDialog = ref({ visible: false, message: '' })
const changeUsernameDialogVisible = ref(false)
const changePasswordDialogVisible = ref(false)
const privilegeDisplayNames = {
'lv-admin': '管理员',
@ -284,6 +286,12 @@ function showChangeUsernameDialog() {
showDropdown.value = false
}
//
function showChangePasswordDialog() {
changePasswordDialogVisible.value = true
showDropdown.value = false
}
//
function handleUsernameChangeSuccess(newUsername) {
if (currentUserData.value) {
@ -297,6 +305,19 @@ function handleUsernameChangeError(errorMessage) {
errorDialogMessage.value = errorMessage
errorDialogVisible.value = true
}
//
function handlePasswordChangeSuccess(message) {
successDialog.value = { visible: true, message: message }
}
//
function handlePasswordChangeError(errorMessage) {
errorDialogMessage.value = errorMessage
errorDialogVisible.value = true
//
changePasswordDialogVisible.value = false
}
</script>
<template>
@ -392,6 +413,10 @@ function handleUsernameChangeError(errorMessage) {
<i class="fas fa-edit"></i>
修改用户名
</div>
<div class="dropdown-item" @click.stop="showChangePasswordDialog">
<i class="fas fa-lock"></i>
修改密码
</div>
<div v-if="isAdmin" class="dropdown-item" @click.stop="router.push('/backend/dashboard'); showDropdown = false">
<i class="fas fa-cog"></i>
管理后台
@ -423,6 +448,11 @@ function handleUsernameChangeError(errorMessage) {
</p>
</div>
</footer>
<ErrorDialog
:visible="errorDialogVisible"
:message="errorDialogMessage"
@close="errorDialogVisible = false"
/>
<PrivilegeRequestDialog
:visible="privilegeDialogVisible"
:privilegeName="privilegeDialogName"
@ -434,11 +464,6 @@ function handleUsernameChangeError(errorMessage) {
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialogVisible"
:message="errorDialogMessage"
@close="errorDialogVisible = false"
/>
<ChangeUsernameDialog
:visible="changeUsernameDialogVisible"
:currentUsername="currentUserData?.username || ''"
@ -446,6 +471,12 @@ function handleUsernameChangeError(errorMessage) {
@success="handleUsernameChangeSuccess"
@error="handleUsernameChangeError"
/>
<ChangePasswordDialog
:visible="changePasswordDialogVisible"
@close="changePasswordDialogVisible = false"
@success="handlePasswordChangeSuccess"
@error="handlePasswordChangeError"
/>
</div>
</template>