|
|
@@ -1,9 +1,30 @@
|
|
|
<template>
|
|
|
<view class="change-page">
|
|
|
- <text class="hint">修改成功后请使用新密码登录。</text>
|
|
|
+ <text class="hint">通过短信验证码修改密码,无需旧密码。修改成功后请使用新密码登录。</text>
|
|
|
|
|
|
<view class="form">
|
|
|
- <input class="input" v-model="oldPassword" placeholder="当前密码" password />
|
|
|
+ <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 />
|
|
|
|
|
|
@@ -15,22 +36,71 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
- import { getToken, setToken, changePasswordLoggedIn } from '../../utils/api'
|
|
|
+ import {
|
|
|
+ getToken,
|
|
|
+ setToken,
|
|
|
+ getCaptcha,
|
|
|
+ sendOpenSms,
|
|
|
+ resetPasswordOpen,
|
|
|
+ getCurrentUserInfo
|
|
|
+ } 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}`
|
|
|
+ }
|
|
|
+
|
|
|
+ function extractMobileFromMe(raw) {
|
|
|
+ if (!raw || typeof raw !== 'object') return ''
|
|
|
+ const o =
|
|
|
+ raw.user != null && typeof raw.user === 'object'
|
|
|
+ ? raw.user
|
|
|
+ : raw.data != null && typeof raw.data === 'object'
|
|
|
+ ? raw.data
|
|
|
+ : raw
|
|
|
+ const m = o.mobile ?? o.phone ?? o.phone_number
|
|
|
+ return m != null ? String(m).trim() : ''
|
|
|
+ }
|
|
|
+
|
|
|
export default {
|
|
|
data() {
|
|
|
return {
|
|
|
- oldPassword: '',
|
|
|
+ mobile: '',
|
|
|
+ captchaId: '',
|
|
|
+ captchaCode: '',
|
|
|
+ captchaImageSrc: '',
|
|
|
+ captchaLoading: false,
|
|
|
+ smsCode: '',
|
|
|
newPassword: '',
|
|
|
confirmPassword: '',
|
|
|
+ sending: false,
|
|
|
+ countdown: 0,
|
|
|
+ timer: null,
|
|
|
loading: false
|
|
|
}
|
|
|
},
|
|
|
+ async onLoad() {
|
|
|
+ this.refreshCaptcha()
|
|
|
+ try {
|
|
|
+ const raw = uni.getStorageSync('current_user')
|
|
|
+ const fromStorage = extractMobileFromMe(raw)
|
|
|
+ if (fromStorage) this.mobile = fromStorage
|
|
|
+ } catch (e) {}
|
|
|
+ const token = getToken()
|
|
|
+ if (!token) return
|
|
|
+ try {
|
|
|
+ const me = await getCurrentUserInfo(token)
|
|
|
+ const m = extractMobileFromMe(me)
|
|
|
+ if (m) this.mobile = m
|
|
|
+ } catch (e) {}
|
|
|
+ },
|
|
|
onShow() {
|
|
|
if (!getToken()) {
|
|
|
uni.showToast({ title: '请先登录', icon: 'none' })
|
|
|
@@ -39,35 +109,90 @@
|
|
|
}, 80)
|
|
|
}
|
|
|
},
|
|
|
+ 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 oldPwd = String(this.oldPassword || '')
|
|
|
- const newPwd = String(this.newPassword || '')
|
|
|
- const confirm = String(this.confirmPassword || '')
|
|
|
- if (!oldPwd) {
|
|
|
- uni.showToast({ title: '请输入当前密码', icon: 'none' })
|
|
|
+ const m = String(this.mobile || '').trim()
|
|
|
+ if (!m) {
|
|
|
+ uni.showToast({ title: '请输入手机号', icon: 'none' })
|
|
|
return
|
|
|
}
|
|
|
- if (!newPwd) {
|
|
|
+ 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(newPwd)) {
|
|
|
+ if (!passwordMeetsRule(pwd)) {
|
|
|
uni.showToast({ title: '新密码须同时包含字母与数字', icon: 'none' })
|
|
|
return
|
|
|
}
|
|
|
- if (newPwd !== confirm) {
|
|
|
+ if (pwd !== confirm) {
|
|
|
uni.showToast({ title: '两次新密码不一致', icon: 'none' })
|
|
|
return
|
|
|
}
|
|
|
- if (oldPwd === newPwd) {
|
|
|
- uni.showToast({ title: '新密码不能与当前密码相同', icon: 'none' })
|
|
|
- return
|
|
|
- }
|
|
|
this.loading = true
|
|
|
try {
|
|
|
- await changePasswordLoggedIn(oldPwd, newPwd)
|
|
|
+ await resetPasswordOpen(m, code, pwd)
|
|
|
uni.showToast({ title: '修改成功,请重新登录', icon: 'success' })
|
|
|
setToken('')
|
|
|
try {
|
|
|
@@ -111,6 +236,61 @@
|
|
|
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;
|