登录后修改密码🐱
This commit is contained in:
parent
bcf943d9f1
commit
46fa0a7668
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
207
src/components/ChangePasswordDialog.vue
Normal file
207
src/components/ChangePasswordDialog.vue
Normal 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>
|
@ -47,7 +47,7 @@ const handleClose = () => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.error-dialog {
|
||||
|
498
src/components/forget_module.vue
Normal file
498
src/components/forget_module.vue
Normal 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>
|
@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -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;
|
||||
|
@ -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
435
src/views/ResetPassword.vue
Normal 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>
|
@ -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()) {
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user