|
|
@@ -0,0 +1,265 @@
|
|
|
+<template>
|
|
|
+ <view class="reset-page">
|
|
|
+ <text class="hint">通过手机号与短信验证码重置密码,无需旧密码。</text>
|
|
|
+
|
|
|
+ <view class="form">
|
|
|
+ <input class="input" v-model="mobile" placeholder="手机号" type="number" />
|
|
|
+
|
|
|
+ <view class="captcha-row">
|
|
|
+ <input
|
|
|
+ class="input captcha-input"
|
|
|
+ v-model="captchaCode"
|
|
|
+ placeholder="图形验证码"
|
|
|
+ maxlength="8"
|
|
|
+ />
|
|
|
+ <view class="captcha-img-wrap" @click="refreshCaptcha">
|
|
|
+ <image v-if="captchaImageSrc" class="captcha-img" :src="captchaImageSrc" mode="aspectFit" />
|
|
|
+ <text v-else class="captcha-placeholder">{{ captchaLoading ? '加载中' : '点击刷新' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="sms-row">
|
|
|
+ <input class="input sms-input" v-model="smsCode" placeholder="短信验证码" type="number" maxlength="8" />
|
|
|
+ <view class="sms-btn" :class="{ disabled: sending || countdown > 0 }" @click="onSendSms">
|
|
|
+ <text>{{ countdown > 0 ? countdown + 's' : (sending ? '发送中' : '获取验证码') }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <input class="input" v-model="newPassword" placeholder="新密码(须含字母与数字)" password />
|
|
|
+ <input class="input" v-model="confirmPassword" placeholder="确认新密码" password />
|
|
|
+
|
|
|
+ <view class="submit" :class="{ disabled: loading }" @click="onSubmit">
|
|
|
+ <text>{{ loading ? '提交中...' : '重置密码' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import { getCaptcha, sendOpenSms, resetPasswordOpen } from '../../utils/api'
|
|
|
+
|
|
|
+ function passwordMeetsRule(pwd) {
|
|
|
+ const s = String(pwd || '')
|
|
|
+ return /[A-Za-z]/.test(s) && /\d/.test(s)
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeCaptchaImageSrc(image) {
|
|
|
+ if (!image) return ''
|
|
|
+ const s = String(image)
|
|
|
+ if (s.startsWith('data:')) return s
|
|
|
+ return `data:image/png;base64,${s}`
|
|
|
+ }
|
|
|
+
|
|
|
+ export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ mobile: '',
|
|
|
+ captchaId: '',
|
|
|
+ captchaCode: '',
|
|
|
+ captchaImageSrc: '',
|
|
|
+ captchaLoading: false,
|
|
|
+ smsCode: '',
|
|
|
+ newPassword: '',
|
|
|
+ confirmPassword: '',
|
|
|
+ sending: false,
|
|
|
+ countdown: 0,
|
|
|
+ timer: null,
|
|
|
+ loading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+ this.refreshCaptcha()
|
|
|
+ },
|
|
|
+ onUnload() {
|
|
|
+ if (this.timer) clearInterval(this.timer)
|
|
|
+ this.timer = null
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async refreshCaptcha() {
|
|
|
+ this.captchaLoading = true
|
|
|
+ this.captchaCode = ''
|
|
|
+ try {
|
|
|
+ const data = await getCaptcha()
|
|
|
+ this.captchaId = data.captcha_id != null ? String(data.captcha_id) : ''
|
|
|
+ this.captchaImageSrc = normalizeCaptchaImageSrc(data.image)
|
|
|
+ } catch (e) {
|
|
|
+ this.captchaImageSrc = ''
|
|
|
+ uni.showToast({ title: e.message || '图形码加载失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ this.captchaLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async onSendSms() {
|
|
|
+ if (this.sending || this.countdown > 0) return
|
|
|
+ const m = String(this.mobile || '').trim()
|
|
|
+ if (!m) {
|
|
|
+ uni.showToast({ title: '请输入手机号', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!this.captchaId) {
|
|
|
+ uni.showToast({ title: '请等待图形验证码加载', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const cc = String(this.captchaCode || '').trim()
|
|
|
+ if (!cc) {
|
|
|
+ uni.showToast({ title: '请输入图形验证码', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.sending = true
|
|
|
+ try {
|
|
|
+ await sendOpenSms(m, this.captchaId, cc)
|
|
|
+ uni.showToast({ title: '短信已发送', icon: 'none' })
|
|
|
+ this.countdown = 60
|
|
|
+ this.timer = setInterval(() => {
|
|
|
+ this.countdown -= 1
|
|
|
+ if (this.countdown <= 0) {
|
|
|
+ clearInterval(this.timer)
|
|
|
+ this.timer = null
|
|
|
+ this.countdown = 0
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '发送失败', icon: 'none' })
|
|
|
+ this.refreshCaptcha()
|
|
|
+ } finally {
|
|
|
+ this.sending = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async onSubmit() {
|
|
|
+ if (this.loading) return
|
|
|
+ const m = String(this.mobile || '').trim()
|
|
|
+ if (!m) {
|
|
|
+ uni.showToast({ title: '请输入手机号', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const code = String(this.smsCode || '').trim()
|
|
|
+ if (!code) {
|
|
|
+ uni.showToast({ title: '请输入短信验证码', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const pwd = String(this.newPassword || '')
|
|
|
+ const confirm = String(this.confirmPassword || '')
|
|
|
+ if (!pwd) {
|
|
|
+ uni.showToast({ title: '请输入新密码', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!passwordMeetsRule(pwd)) {
|
|
|
+ uni.showToast({ title: '新密码须同时包含字母与数字', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (pwd !== confirm) {
|
|
|
+ uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ await resetPasswordOpen(m, code, pwd)
|
|
|
+ uni.showToast({ title: '密码重置成功', icon: 'success' })
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.reLaunch({ url: '/pages/login/index' })
|
|
|
+ }, 400)
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '重置失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .reset-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #fff;
|
|
|
+ padding: 32rpx 48rpx 64rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+ .hint {
|
|
|
+ display: block;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #888;
|
|
|
+ line-height: 1.5;
|
|
|
+ margin-bottom: 32rpx;
|
|
|
+ }
|
|
|
+ .form {
|
|
|
+ margin-top: 8rpx;
|
|
|
+ }
|
|
|
+ .input {
|
|
|
+ height: 88rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ background: #f7f7f7;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ }
|
|
|
+ .captcha-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ }
|
|
|
+ .captcha-input {
|
|
|
+ flex: 1;
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ .captcha-img-wrap {
|
|
|
+ width: 220rpx;
|
|
|
+ height: 88rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ background: #f0f0f0;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .captcha-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ .captcha-placeholder {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+ padding: 0 8rpx;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .sms-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ }
|
|
|
+ .sms-input {
|
|
|
+ flex: 1;
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ .sms-btn {
|
|
|
+ height: 88rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #259653;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 26rpx;
|
|
|
+ }
|
|
|
+ .sms-btn.disabled {
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
+ .submit {
|
|
|
+ margin-top: 32rpx;
|
|
|
+ height: 92rpx;
|
|
|
+ border-radius: 18rpx;
|
|
|
+ background: #259653;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 30rpx;
|
|
|
+ }
|
|
|
+ .submit.disabled {
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+</style>
|