This commit is contained in:
aaron 2025-05-24 15:33:31 +08:00
parent fec3d35af2
commit ea7c5e56a3
2 changed files with 315 additions and 1 deletions

View File

@ -5,7 +5,7 @@ services:
build:
context: .
dockerfile: Dockerfile
image: tradus-web:1.3.15
image: tradus-web:1.3.16
container_name: tradus-web
ports:
- '6000:80'

View File

@ -13,6 +13,7 @@ const userInfo = computed(() => userStore.userInfo)
const showMobileMenu = ref(false)
const showUserMenu = ref(false)
const showLoginModal = ref(false)
const showResetPasswordModal = ref(false)
const loginMode = ref('login') // 'login' 'register'
//
@ -69,6 +70,13 @@ const formData = ref({
verificationCode: '',
})
//
const resetPasswordData = ref({
email: '',
verificationCode: '',
newPassword: '',
})
//
const formErrors = ref({
email: '',
@ -76,15 +84,25 @@ const formErrors = ref({
verificationCode: '',
})
//
const resetPasswordErrors = ref({
email: '',
verificationCode: '',
newPassword: '',
})
//
const showPassword = ref(false)
const showNewPassword = ref(false)
//
const sendingCode = ref(false)
const countdown = ref(0)
const resetCountdown = ref(0)
//
const isLoading = ref(false)
const isResetLoading = ref(false)
//
const sendVerificationCode = async () => {
@ -116,6 +134,50 @@ const sendVerificationCode = async () => {
}
}
//
const sendResetVerificationCode = async () => {
if (!resetPasswordData.value.email) {
resetPasswordErrors.value.email = '请输入邮箱'
return
}
try {
sendingCode.value = true
resetPasswordErrors.value.email = ''
//
const response = await fetch(
`${import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'}/user/send-verification-code`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ mail: resetPasswordData.value.email }),
},
)
if (!response.ok) {
throw new Error('发送验证码失败,请重试')
}
resetCountdown.value = 60
//
const timer = setInterval(() => {
resetCountdown.value--
if (resetCountdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
} catch (err) {
resetPasswordErrors.value.email = err instanceof Error ? err.message : '发送验证码失败,请重试'
console.error('发送重置密码验证码失败:', err)
} finally {
sendingCode.value = false
}
}
//
const validateForm = () => {
let isValid = true
@ -154,6 +216,83 @@ const validateForm = () => {
return isValid
}
//
const validateResetPasswordForm = () => {
let isValid = true
resetPasswordErrors.value = {
email: '',
verificationCode: '',
newPassword: '',
}
//
if (!resetPasswordData.value.email) {
resetPasswordErrors.value.email = '请输入邮箱'
isValid = false
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(resetPasswordData.value.email)) {
resetPasswordErrors.value.email = '请输入有效的邮箱地址'
isValid = false
}
//
if (!resetPasswordData.value.verificationCode) {
resetPasswordErrors.value.verificationCode = '请输入验证码'
isValid = false
}
//
if (!resetPasswordData.value.newPassword) {
resetPasswordErrors.value.newPassword = '请输入新密码'
isValid = false
} else if (resetPasswordData.value.newPassword.length < 6) {
resetPasswordErrors.value.newPassword = '密码长度不能小于6位'
isValid = false
}
return isValid
}
//
const handleResetPasswordSubmit = async () => {
if (!validateResetPasswordForm()) return
isResetLoading.value = true
try {
//
const response = await fetch(
`${import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'}/user/reset_password`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mail: resetPasswordData.value.email,
verification_code: resetPasswordData.value.verificationCode,
new_password: resetPasswordData.value.newPassword,
}),
},
)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || '重置密码失败')
}
//
closeResetPasswordModal()
//
alert('密码重置成功,请使用新密码登录')
openAuthModal('login')
} catch (error) {
console.error('重置密码失败:', error)
resetPasswordErrors.value.email =
error instanceof Error ? error.message : '重置密码失败,请检查输入信息'
} finally {
isResetLoading.value = false
}
}
//
const handleSubmit = async () => {
if (!validateForm()) return
@ -242,6 +381,28 @@ const openAuthModal = (mode: 'login' | 'register') => {
showLoginModal.value = true
}
//
const closeResetPasswordModal = () => {
showResetPasswordModal.value = false
//
resetPasswordData.value = {
email: '',
verificationCode: '',
newPassword: '',
}
resetPasswordErrors.value = {
email: '',
verificationCode: '',
newPassword: '',
}
}
//
const openResetPasswordModal = () => {
closeAuthModal() //
showResetPasswordModal.value = true
}
onMounted(() => {
document.addEventListener('click', closeMenus)
if (isAuthenticated.value) {
@ -590,6 +751,12 @@ onUnmounted(() => {
<span v-else class="loading-spinner"></span>
</button>
</form>
<!-- 忘记密码链接 -->
<div class="forgot-password" v-if="loginMode === 'login'">
<button class="forgot-password-link" @click="openResetPasswordModal">忘记密码</button>
</div>
<div class="auth-switch">
{{ loginMode === 'login' ? '还没有账号?' : '已有账号?' }}
<button
@ -602,6 +769,132 @@ onUnmounted(() => {
</div>
</div>
</div>
<!-- 忘记密码模态框 -->
<div class="modal-overlay" v-if="showResetPasswordModal" @click="closeResetPasswordModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h2>重置密码</h2>
<button class="close-button" @click="closeResetPasswordModal">
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-content">
<form class="auth-form" @submit.prevent="handleResetPasswordSubmit">
<div class="form-group">
<label for="reset-email">邮箱</label>
<div class="input-container">
<input
type="email"
id="reset-email"
v-model="resetPasswordData.email"
:class="{ error: resetPasswordErrors.email }"
placeholder="请输入邮箱"
/>
<button
type="button"
class="code-button"
@click="sendResetVerificationCode"
:disabled="sendingCode || resetCountdown > 0"
>
{{
resetCountdown > 0
? `${resetCountdown}秒后重试`
: sendingCode
? '发送中...'
: '获取验证码'
}}
</button>
</div>
<span class="error-message" v-if="resetPasswordErrors.email">{{
resetPasswordErrors.email
}}</span>
</div>
<div class="form-group">
<label for="reset-verification-code">邮件验证码</label>
<input
type="text"
id="reset-verification-code"
v-model="resetPasswordData.verificationCode"
:class="{ error: resetPasswordErrors.verificationCode }"
placeholder="请输入验证码"
/>
<span class="error-message" v-if="resetPasswordErrors.verificationCode">
{{ resetPasswordErrors.verificationCode }}
</span>
</div>
<div class="form-group">
<label for="new-password">新密码</label>
<div class="password-input-container">
<input
:type="showNewPassword ? 'text' : 'password'"
id="new-password"
v-model="resetPasswordData.newPassword"
:class="{ error: resetPasswordErrors.newPassword }"
placeholder="请输入新密码"
/>
<button
type="button"
class="toggle-password-button"
@click="showNewPassword = !showNewPassword"
>
<svg
v-if="!showNewPassword"
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
<svg
v-else
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
stroke-width="2"
fill="none"
>
<path
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
/>
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
</button>
</div>
<span class="error-message" v-if="resetPasswordErrors.newPassword">
{{ resetPasswordErrors.newPassword }}
</span>
</div>
<button type="submit" class="submit-button" :disabled="isResetLoading">
<span v-if="!isResetLoading">重置密码</span>
<span v-else class="loading-spinner"></span>
</button>
</form>
<div class="auth-switch">
<button class="switch-button" @click="closeResetPasswordModal()">关闭</button>
</div>
</div>
</div>
</div>
</div>
</template>
@ -1738,4 +2031,25 @@ html {
border-color: var(--color-border);
color: var(--color-text-secondary);
}
/* 忘记密码链接样式 */
.forgot-password {
text-align: right;
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.forgot-password-link {
background: none;
border: none;
color: var(--color-accent);
font-size: 0.9rem;
cursor: pointer;
padding: 0;
text-decoration: none;
}
.forgot-password-link:hover {
text-decoration: underline;
}
</style>