Merge pull request 'feature/login-screen' (#11) from feature/login-screen into master
Reviewed-on: #11
This commit is contained in:
		
						commit
						b7eb16fb2f
					
				@ -154,12 +154,12 @@ export const changeUserName = async (name) => {
 | 
			
		||||
 * 用户请求修改密码(发送重置请求)
 | 
			
		||||
 * 路由: /user/resetpassword
 | 
			
		||||
 * 方法: POST
 | 
			
		||||
 * 需要登录
 | 
			
		||||
 * @param {string} uuid - 要修改的用户的uuid
 | 
			
		||||
 * @returns {Promise<void>} 无返回值,成功即为请求成功
 | 
			
		||||
 */
 | 
			
		||||
export const requestResetPassword = async () => {
 | 
			
		||||
export const requestResetPassword = async (uuid) => {
 | 
			
		||||
  try {
 | 
			
		||||
    await axiosInstance.post('/user/resetpassword');
 | 
			
		||||
    await axiosInstance.post('/user/resetpassword', null, { params: { uuid } });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    throw error;
 | 
			
		||||
  }
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										210
									
								
								src/components/ChangePasswordDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/components/ChangePasswordDialog.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,210 @@
 | 
			
		||||
<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'
 | 
			
		||||
import {getStoredUser} from "@/utils/jwt.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 {
 | 
			
		||||
    const user = getStoredUser()
 | 
			
		||||
    console.log(user.uuid)
 | 
			
		||||
    // 请求修改密码
 | 
			
		||||
    await requestResetPassword(user.uuid)
 | 
			
		||||
    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 {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										564
									
								
								src/components/forget_module.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										564
									
								
								src/components/forget_module.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,564 @@
 | 
			
		||||
<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>-->
 | 
			
		||||
    <form form class="login-form-container" @submit.prevent="uuid_handleForgetPassword">
 | 
			
		||||
<!--      <div class="input-container">-->
 | 
			
		||||
<!--        <label for="username">UUID</label>-->
 | 
			
		||||
<!--        <input-->
 | 
			
		||||
<!--          id="username"-->
 | 
			
		||||
<!--          type="text"-->
 | 
			
		||||
<!--          placeholder="请输入UUID"-->
 | 
			
		||||
<!--          v-model="uuid"-->
 | 
			
		||||
<!--        />-->
 | 
			
		||||
<!--      </div>-->
 | 
			
		||||
      <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="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, requestResetPassword, getUserByInfo} 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 uuid = ref('')
 | 
			
		||||
 | 
			
		||||
// 错误信息
 | 
			
		||||
const usernameError = ref('')
 | 
			
		||||
const newPasswordError = ref('')
 | 
			
		||||
const confirmPasswordError = ref('')
 | 
			
		||||
const captchaError = ref('')
 | 
			
		||||
const uuidError = 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 uuid_handleForgetPassword = async () => {
 | 
			
		||||
  try{
 | 
			
		||||
    if (!uuid_validateForm()) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    //console.log(uuid.value);
 | 
			
		||||
    const user = await getUserByInfo({qq_code:username.value})
 | 
			
		||||
    console.log(user)
 | 
			
		||||
    await requestResetPassword(user.uuid)
 | 
			
		||||
    isSubmitting.value = true
 | 
			
		||||
  }catch ( error){
 | 
			
		||||
    showErrorMessage(error.message || '不是正确的uuid')
 | 
			
		||||
  }finally {
 | 
			
		||||
    isSubmitting.value = false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const uuid_validateForm = () =>{
 | 
			
		||||
  usernameError.value = ''
 | 
			
		||||
  if (!username.value) {
 | 
			
		||||
    usernameError.value = '请输入qq'
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
  uuid_resetForm()
 | 
			
		||||
  // 切换到登录页面
 | 
			
		||||
  $emit('login')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重置表单数据
 | 
			
		||||
const resetForm = () => {
 | 
			
		||||
  username.value = ''
 | 
			
		||||
  newPassword.value = ''
 | 
			
		||||
  confirmPassword.value = ''
 | 
			
		||||
  captcha.value = ''
 | 
			
		||||
  usernameError.value = ''
 | 
			
		||||
  newPasswordError.value = ''
 | 
			
		||||
  confirmPasswordError.value = ''
 | 
			
		||||
  captchaError.value = ''
 | 
			
		||||
  refreshCaptcha()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const uuid_resetForm = () =>{
 | 
			
		||||
  username.value = ''
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 暴露resetForm方法给父组件
 | 
			
		||||
defineExpose({
 | 
			
		||||
  // resetForm
 | 
			
		||||
  uuid_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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,14 @@ import path from 'path'
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [vue()],
 | 
			
		||||
  build: {
 | 
			
		||||
    terserOptions:{
 | 
			
		||||
      compress:{
 | 
			
		||||
        drop_console:true,
 | 
			
		||||
        drop_debugger:true,
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      '@': path.resolve(__dirname, './src')
 | 
			
		||||
@ -13,4 +21,4 @@ export default defineConfig({
 | 
			
		||||
    port: 80,
 | 
			
		||||
    open: true
 | 
			
		||||
  }
 | 
			
		||||
}) 
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user