Files
yunzerwebsiteallinone/uniapp/pages/login/forget.vue
T
2026-07-15 22:38:33 +08:00

482 lines
11 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.
<template>
<view class="auth-page">
<view class="page-inner">
<view class="nav-back" @tap="goBack">
<FaIcon name="chevron-left" color="#303133" :size="18" />
<text class="nav-text">返回</text>
</view>
<view class="header">
<text class="title">忘记密码</text>
<text class="subtitle">
{{ currentStep === 1 ? '验证您的账号' : currentStep === 2 ? '通过手机号验证身份' : '设置新密码' }}
</text>
</view>
<view class="form-card">
<!-- 第一步验证租户和账号 -->
<view v-if="currentStep === 1" class="form">
<view class="field">
<u-input
v-model="form.tenant_name"
placeholder="租户名称"
border="none"
color="#303133"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
</view>
<view class="field">
<u-input
v-model="form.account"
placeholder="账号"
border="none"
color="#303133"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
</view>
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleVerifyAccount">
{{ loading ? '验证中...' : '下一步' }}
</view>
</view>
<!-- 第二步验证手机号并发送验证码 -->
<view v-if="currentStep === 2" class="form">
<view class="info-box">
<text class="info-label">租户</text>
<text class="info-value">{{ form.tenant_name }}</text>
</view>
<view class="info-box">
<text class="info-label">账号</text>
<text class="info-value">{{ form.account }}</text>
</view>
<view v-if="verifyResult.phone" class="field">
<u-input
v-model="form.phone"
placeholder="手机号"
type="number"
maxlength="11"
border="none"
color="#303133"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
</view>
<view v-else class="empty-contact">
<text>账号未绑定手机号请联系管理员</text>
</view>
<view class="field field-row">
<u-input
v-model="form.sms_code"
placeholder="短信验证码"
type="number"
maxlength="6"
border="none"
color="#303133"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
<text
class="code-link"
:class="{ disabled: countdown > 0 || codeLoading || !verifyResult.phone }"
@tap="handleSendCode"
>{{ countdown > 0 ? `${countdown}s` : (codeLoading ? '发送中' : '获取验证码') }}</text>
</view>
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleVerifyPhone">
{{ loading ? '验证中...' : '下一步' }}
</view>
<view class="action-row">
<text class="link-text" @tap="currentStep = 1">返回上一步</text>
<text class="link-text" @tap="goBack">返回登录</text>
</view>
</view>
<!-- 第三步重置密码 -->
<view v-if="currentStep === 3" class="form">
<view class="info-box">
<text class="info-label">租户</text>
<text class="info-value">{{ form.tenant_name }}</text>
</view>
<view class="info-box">
<text class="info-label">账号</text>
<text class="info-value">{{ form.account }}</text>
</view>
<view class="field">
<u-input
v-model="form.new_password"
placeholder="新密码"
type="password"
border="none"
color="#303133"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
</view>
<view class="field">
<u-input
v-model="form.confirm_password"
placeholder="确认新密码"
type="password"
border="none"
color="#303133"
:customStyle="inputStyle"
:placeholderStyle="placeholderStyle"
/>
</view>
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleResetPassword">
{{ loading ? '提交中...' : '重置密码' }}
</view>
<view class="action-row">
<text class="link-text" @tap="currentStep = 2">返回上一步</text>
<text class="link-text" @tap="goBack">返回登录</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { reactive, ref, onUnmounted } from 'vue'
import { verifyAccount, resetPassword, sendResetCode } from '@/api/auth.js'
import { setTenantName, getTenantName } from '@/utils/auth.js'
const currentStep = ref(1)
const loading = ref(false)
const codeLoading = ref(false)
const countdown = ref(0)
let timer = null
const form = reactive({
tenant_name: getTenantName() || '',
account: '',
phone: '',
sms_code: '',
new_password: '',
confirm_password: ''
})
const verifyResult = reactive({
phone: '',
email: ''
})
const inputStyle = {
backgroundColor: 'transparent',
padding: '0 8rpx',
height: '96rpx',
fontSize: '28rpx'
}
const placeholderStyle = 'color: #909399; font-size: 28rpx'
function startCountdown() {
countdown.value = 60
if (timer) clearInterval(timer)
timer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
}
// 第一步:验证租户和账号
async function handleVerifyAccount() {
if (loading.value) return
if (!form.tenant_name.trim() || !form.account.trim()) {
uni.showToast({ title: '请填写租户和账号', icon: 'none' })
return
}
loading.value = true
try {
const data = await verifyAccount({
tenant_name: form.tenant_name.trim(),
account: form.account.trim()
})
verifyResult.phone = data?.phone || ''
verifyResult.email = data?.email || ''
if (!verifyResult.phone && !verifyResult.email) {
uni.showToast({ title: '账号未绑定验证方式', icon: 'none' })
return
}
form.phone = verifyResult.phone || ''
currentStep.value = 2
} catch (err) {
// error handled by request interceptor
} finally {
loading.value = false
}
}
// 第二步:发送验证码
async function handleSendCode() {
if (codeLoading.value || countdown.value > 0) return
if (!form.phone.trim()) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (!/^1\d{10}$/.test(form.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
codeLoading.value = true
try {
await sendResetCode({
tenant_name: form.tenant_name.trim(),
account: form.account.trim(),
phone: form.phone.trim(),
channel: 'sms'
})
uni.showToast({ title: '验证码已发送', icon: 'success' })
startCountdown()
} catch (err) {
// error handled by request interceptor
} finally {
codeLoading.value = false
}
}
// 第二步验证:验证手机和验证码
async function handleVerifyPhone() {
if (loading.value) return
if (!form.phone.trim()) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (!form.sms_code.trim()) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
loading.value = true
try {
// 后端的验证码验证在重置密码时进行,这里只是提交到第三步
currentStep.value = 3
} catch (err) {
// error handled by request interceptor
} finally {
loading.value = false
}
}
// 第三步:重置密码
async function handleResetPassword() {
if (loading.value) return
if (!form.tenant_name.trim() || !form.account.trim() || !form.phone.trim()) {
uni.showToast({ title: '请填写租户、账号和手机号', icon: 'none' })
return
}
if (!form.new_password) {
uni.showToast({ title: '请输入新密码', icon: 'none' })
return
}
if (form.new_password !== form.confirm_password) {
uni.showToast({ title: '两次密码不一致', icon: 'none' })
return
}
if (form.new_password.length < 6) {
uni.showToast({ title: '密码长度不能少于6个字符', icon: 'none' })
return
}
if (!form.sms_code.trim()) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
loading.value = true
try {
await resetPassword({
tenant_name: form.tenant_name.trim(),
account: form.account.trim(),
phone: form.phone.trim(),
sms_code: form.sms_code.trim(),
new_password: form.new_password,
confirm_password: form.confirm_password
})
setTenantName(form.tenant_name.trim())
uni.showToast({ title: '重置成功,请登录', icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' })
}, 500)
} catch (err) {
// error handled by request interceptor
} finally {
loading.value = false
}
}
function goBack() {
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/login/login' }) })
}
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.auth-page {
min-height: 100vh;
background: $color-bg-page;
}
.page-inner {
padding: 0 48rpx;
padding-top: calc(var(--status-bar-height, 44px) + 24rpx);
padding-bottom: 60rpx;
}
.nav-back {
display: flex;
align-items: center;
gap: 4rpx;
margin-bottom: 32rpx;
padding: 8rpx 0;
}
.nav-text {
font-size: 28rpx;
color: $color-text;
}
.header {
margin-bottom: 40rpx;
}
.title {
display: block;
font-size: 44rpx;
font-weight: 600;
color: $color-text;
}
.subtitle {
display: block;
font-size: 26rpx;
color: $color-text-muted;
margin-top: 12rpx;
}
.form-card {
@include card;
padding: 32rpx 28rpx;
}
.form {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.field {
background: $color-bg-page;
border-radius: $radius-md;
padding: 0 24rpx;
overflow: hidden;
}
.field-row {
display: flex;
align-items: center;
}
.code-link {
flex-shrink: 0;
font-size: 26rpx;
color: $color-primary;
padding-left: 16rpx;
white-space: nowrap;
&.disabled {
color: $color-text-muted;
}
}
.info-box {
background: $color-bg-page;
border-radius: $radius-md;
padding: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.info-label {
font-size: 26rpx;
color: $color-text-muted;
}
.info-value {
font-size: 26rpx;
color: $color-text;
font-weight: 500;
}
.empty-contact {
background: $color-bg-page;
border-radius: $radius-md;
padding: 24rpx;
text-align: center;
font-size: 26rpx;
color: $color-text-muted;
}
.btn-primary {
height: 96rpx;
background: $color-primary;
border-radius: $radius-md;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
color: #fff;
margin-top: 12rpx;
&:active {
background: $color-primary-dark;
}
&.disabled {
opacity: 0.7;
}
}
.action-row {
display: flex;
justify-content: space-between;
gap: 16rpx;
margin-top: 8rpx;
}
.link-text {
flex: 1;
text-align: center;
font-size: 26rpx;
color: $color-primary;
padding: 12rpx 0;
}
.footer-link {
text-align: center;
font-size: 26rpx;
color: $color-primary;
padding: 16rpx 0 4rpx;
}
</style>