Files
jiapuapp/pages/auth/a05-reset-password.vue
T
2026-07-16 07:58:53 +08:00

330 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 页面编号A-05用途通过手机号验证后重设登录密码 -->
<template>
<view class="auth-page reset-page">
<view class="page-canvas">
<image class="page-backdrop" src="/static/assets/modules/auth/opaque/a01-red-hall-ink-backdrop-v1.png" mode="scaleToFill" />
<image class="brand-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
<view class="reset-content">
<view class="page-heading">
<view class="back-button" hover-class="tap-fade" @click="goBack">
<image class="back-button__icon" src="/static/assets/foundation/transparent/chevron-right.png" mode="aspectFit" />
</view>
<text class="page-title">重设密码</text>
<text class="page-subtitle">验证手机号后设置新的登录密码</text>
<image class="reset-divider" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
</view>
<view class="form-content">
<view class="field-block" :class="{ 'field-block--error': fieldErrors.phone }">
<view class="input-row">
<text class="input-label">手机号</text>
<input v-model.trim="phone" class="auth-input" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="placeholder" @input="clearFieldError('phone')" />
</view>
<text v-if="fieldErrors.phone" class="field-error">{{ fieldErrors.phone }}</text>
</view>
<view class="field-block" :class="{ 'field-block--error': fieldErrors.verificationCode }">
<view class="input-row code-row">
<text class="input-label">验证码</text>
<input v-model.trim="verificationCode" class="auth-input" type="number" maxlength="6" placeholder="请输入验证码" placeholder-class="placeholder" @input="clearFieldError('verificationCode')" />
<view class="get-code" hover-class="tap-fade" @click="prepareGetCode">{{ codeRequested ? '重新获取' : '获取验证码' }}</view>
</view>
<text v-if="fieldErrors.verificationCode" class="field-error">{{ fieldErrors.verificationCode }}</text>
</view>
<view class="field-block" :class="{ 'field-block--error': fieldErrors.password }">
<view class="input-row">
<text class="input-label">新密码</text>
<input v-model="password" class="auth-input" password maxlength="32" placeholder="请设置新密码" placeholder-class="placeholder" @input="clearFieldError('password')" />
</view>
<text v-if="fieldErrors.password" class="field-error">{{ fieldErrors.password }}</text>
</view>
<view class="field-block" :class="{ 'field-block--error': fieldErrors.confirmPassword }">
<view class="input-row">
<text class="input-label">确认新密码</text>
<input v-model="confirmPassword" class="auth-input" password maxlength="32" placeholder="请再次输入新密码" placeholder-class="placeholder" @input="clearFieldError('confirmPassword')" />
</view>
<text v-if="fieldErrors.confirmPassword" class="field-error">{{ fieldErrors.confirmPassword }}</text>
</view>
</view>
<view class="reset-submit" hover-class="button-hover" @click="submitReset">
<image class="reset-submit__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text class="reset-submit__content">确认重设</text>
</view>
<text class="reset-tip">重设成功后请使用新密码登录</text>
<view class="login-entry">
<text>想起密码了</text>
<view class="login-entry__link" hover-class="tap-fade" @click="prepareLogin">返回登录</view>
</view>
</view>
</view>
<!-- 发送短信验证码前进行行为验证关闭时不清空表单 -->
<view v-if="verificationVisible" class="verification-layer" @click="closeVerification">
<view class="verification-dialog" @click.stop>
<image class="verification-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy">发送验证码前需要完成行为验证</text>
<text class="verification-note">验证数据将在接口接入后启用</text>
<view class="verification-actions">
<view class="verification-action" hover-class="tap-fade" @click="closeVerification">取消</view>
<view class="verification-action verification-action--primary" hover-class="tap-fade" @click="confirmVerification">完成验证</view>
</view>
</view>
</view>
</view>
<view v-if="successVisible" class="success-layer">
<view class="success-dialog">
<image class="success-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<view class="success-dialog__content">
<text class="success-mark"></text>
<text class="success-title">密码已重设</text>
<text class="success-copy">请返回登录页使用新密码登录</text>
<view class="success-action" hover-class="button-hover" @click="prepareLogin">
<image class="success-action__skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text class="success-action__copy">返回登录</text>
</view>
</view>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<image class="feedback-toast__skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onUnload } from '@dcloudio/uni-app'
const phone = ref('')
const verificationCode = ref('')
const password = ref('')
const confirmPassword = ref('')
const codeRequested = ref(false)
const verificationVisible = ref(false)
const successVisible = ref(false)
const feedbackVisible = ref(false)
const feedbackMessage = ref('')
const fieldErrors = ref({
phone: '',
verificationCode: '',
password: '',
confirmPassword: ''
})
let feedbackTimer = null
onUnload(() => {
if (feedbackTimer) clearTimeout(feedbackTimer)
})
const goBack = () => uni.navigateBack()
// 返回登录时替换当前认证页,避免登录与重设页反复叠加。
const prepareLogin = () => uni.redirectTo({ url: '/pages/auth/a01-entry' })
const showFeedback = (message) => {
feedbackMessage.value = message
feedbackVisible.value = true
if (feedbackTimer) clearTimeout(feedbackTimer)
feedbackTimer = setTimeout(() => {
feedbackVisible.value = false
feedbackTimer = null
}, 2200)
}
const clearFieldError = (field) => {
fieldErrors.value[field] = ''
}
const prepareGetCode = () => {
if (!/^1\d{10}$/.test(phone.value)) {
fieldErrors.value.phone = '请输入正确手机号'
return
}
fieldErrors.value.phone = ''
verificationVisible.value = true
}
const closeVerification = () => {
verificationVisible.value = false
}
const confirmVerification = () => {
closeVerification()
codeRequested.value = true
showFeedback('验证码已发送')
}
const validateForm = () => {
const nextErrors = { phone: '', verificationCode: '', password: '', confirmPassword: '' }
if (!/^1\d{10}$/.test(phone.value)) nextErrors.phone = '请输入正确手机号'
if (!/^\d{6}$/.test(verificationCode.value)) nextErrors.verificationCode = '请输入 6 位验证码'
if (!password.value) nextErrors.password = '请设置新密码'
if (!confirmPassword.value) nextErrors.confirmPassword = '请再次输入新密码'
else if (password.value !== confirmPassword.value) nextErrors.confirmPassword = '两次密码输入不一致'
fieldErrors.value = nextErrors
return !Object.values(nextErrors).some(Boolean)
}
const submitReset = () => {
if (!validateForm()) return
successVisible.value = true
}
</script>
<style scoped lang="scss">
.auth-page {
position: relative;
width: 100%;
height: 100dvh;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
background: #f7f0e4;
color: #493323;
}
.page-canvas {
position: relative;
box-sizing: border-box;
width: 100%;
min-height: calc(1665rpx + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
}
.page-backdrop {
position: absolute;
inset: 0 0 auto;
width: 100%;
height: 1665rpx;
pointer-events: none;
}
.brand-seal {
position: absolute;
z-index: 1;
top: 64rpx;
left: 50%;
width: 220rpx;
height: 264rpx;
transform: translateX(-50%);
pointer-events: none;
}
.reset-content {
position: relative;
z-index: 2;
box-sizing: border-box;
padding: 430rpx 64rpx 46rpx;
}
.page-heading {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
min-height: 154rpx;
}
.back-button {
position: absolute;
top: 10rpx;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
}
.back-button__icon { width: 42rpx; height: 42rpx; transform: scaleX(-1); }
.page-title { color: #9f170f; font-family: STSong, SimSun, serif; font-size: 66rpx; font-weight: 700; letter-spacing: 7rpx; }
.page-subtitle { margin-top: 12rpx; color: #806c58; font-size: 25rpx; letter-spacing: 1rpx; }
.reset-divider { width: 154rpx; height: 34rpx; margin-top: 10rpx; }
.form-content { margin-top: 8rpx; }
.field-block { min-height: 114rpx; }
.input-row {
display: flex;
align-items: center;
min-height: 84rpx;
border-bottom: 1rpx solid #cfaa70;
}
.input-label { flex: 0 0 162rpx; color: #674b35; font-family: STSong, SimSun, serif; font-size: 29rpx; }
.auth-input { flex: 1; min-width: 0; height: 82rpx; color: #3f2c1d; font-size: 27rpx; }
.placeholder { color: #a79e94; }
.get-code { flex: 0 0 auto; padding-left: 14rpx; border-left: 1rpx solid #d7bd94; color: #a7160c; font-size: 23rpx; }
.field-error { display: block; padding-top: 4rpx; color: #b42318; font-size: 21rpx; line-height: 28rpx; text-align: right; }
.field-block--error .input-row { border-bottom-color: #b42318; }
.reset-submit {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 104rpx;
margin-top: 26rpx;
overflow: hidden;
}
.reset-submit__skin,
.success-action__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.reset-submit__content { position: relative; z-index: 1; color: #fffaf0; font-family: STSong, SimSun, serif; font-size: 35rpx; letter-spacing: 6rpx; }
.reset-tip { display: block; margin-top: 22rpx; color: #806c58; font-size: 22rpx; text-align: center; }
.login-entry { display: flex; align-items: center; justify-content: center; min-height: 78rpx; color: #6b5a4a; font-size: 24rpx; }
.login-entry__link { display: flex; align-items: center; min-height: 78rpx; margin-left: 8rpx; color: #a7160c; }
.verification-layer,
.success-layer {
position: fixed;
z-index: 20;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 42rpx;
background: rgba(35, 18, 10, 0.62);
}
.verification-dialog,
.success-dialog { position: relative; width: 100%; max-width: 670rpx; min-height: 500rpx; }
.verification-dialog__skin,
.success-dialog__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.verification-dialog__content,
.success-dialog__content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; min-height: 500rpx; padding: 92rpx 70rpx 64rpx; text-align: center; }
.verification-title,
.success-title { color: #8f160f; font-family: STSong, SimSun, serif; font-size: 45rpx; font-weight: 700; letter-spacing: 4rpx; }
.verification-copy,
.success-copy { margin-top: 32rpx; color: #513a28; font-size: 27rpx; line-height: 42rpx; }
.verification-note { margin-top: 14rpx; color: #8d7965; font-size: 22rpx; }
.verification-actions { display: flex; width: 100%; margin-top: 56rpx; border-top: 1rpx solid #d6bb91; }
.verification-action { display: flex; flex: 1; align-items: center; justify-content: center; min-height: 86rpx; color: #73533a; font-size: 27rpx; }
.verification-action--primary { border-left: 1rpx solid #d6bb91; color: #a7160c; }
.success-mark { display: flex; align-items: center; justify-content: center; width: 82rpx; height: 82rpx; margin-bottom: 20rpx; border: 2rpx solid #a7160c; border-radius: 50%; color: #a7160c; font-size: 48rpx; }
.success-action { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 92rpx; margin-top: 38rpx; overflow: hidden; }
.success-action__copy { position: relative; z-index: 1; color: #fffaf0; font-family: STSong, SimSun, serif; font-size: 30rpx; letter-spacing: 4rpx; }
.feedback-toast { position: fixed; z-index: 30; top: calc(40rpx + env(safe-area-inset-top)); left: 50%; display: flex; align-items: center; justify-content: center; min-width: 280rpx; min-height: 80rpx; padding: 0 42rpx; transform: translateX(-50%); }
.feedback-toast__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.feedback-toast__copy { position: relative; z-index: 1; color: #5e402b; font-size: 24rpx; }
.tap-fade,
.button-hover { opacity: 0.72; }
@media screen and (max-width: 340px) {
.reset-content { padding-right: 48rpx; padding-left: 48rpx; }
.page-title { font-size: 60rpx; }
.input-label { flex-basis: 150rpx; }
}
</style>