index.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <template>
  2. <view class="change-page">
  3. <text class="hint">通过短信验证码修改密码,无需旧密码。修改成功后请使用新密码登录。</text>
  4. <view class="form">
  5. <input class="input" v-model="mobile" placeholder="手机号" type="number" />
  6. <view class="captcha-row">
  7. <input
  8. class="input captcha-input"
  9. v-model="captchaCode"
  10. placeholder="图形验证码"
  11. maxlength="8"
  12. />
  13. <view class="captcha-img-wrap" @click="refreshCaptcha">
  14. <image v-if="captchaImageSrc" class="captcha-img" :src="captchaImageSrc" mode="aspectFit" />
  15. <text v-else class="captcha-placeholder">{{ captchaLoading ? '加载中' : '点击刷新' }}</text>
  16. </view>
  17. </view>
  18. <view class="sms-row">
  19. <input class="input sms-input" v-model="smsCode" placeholder="短信验证码" type="number" maxlength="8" />
  20. <view class="sms-btn" :class="{ disabled: sending || countdown > 0 }" @click="onSendSms">
  21. <text>{{ countdown > 0 ? countdown + 's' : (sending ? '发送中' : '获取验证码') }}</text>
  22. </view>
  23. </view>
  24. <input class="input" v-model="newPassword" placeholder="新密码(须含字母与数字)" password />
  25. <input class="input" v-model="confirmPassword" placeholder="确认新密码" password />
  26. <view class="submit" :class="{ disabled: loading }" @click="onSubmit">
  27. <text>{{ loading ? '提交中...' : '确认修改' }}</text>
  28. </view>
  29. </view>
  30. </view>
  31. </template>
  32. <script>
  33. import {
  34. getToken,
  35. setToken,
  36. getCaptcha,
  37. sendOpenSms,
  38. resetPasswordOpen,
  39. getCurrentUserInfo
  40. } from '../../utils/api'
  41. function passwordMeetsRule(pwd) {
  42. const s = String(pwd || '')
  43. return /[A-Za-z]/.test(s) && /\d/.test(s)
  44. }
  45. function normalizeCaptchaImageSrc(image) {
  46. if (!image) return ''
  47. const s = String(image)
  48. if (s.startsWith('data:')) return s
  49. return `data:image/png;base64,${s}`
  50. }
  51. function extractMobileFromMe(raw) {
  52. if (!raw || typeof raw !== 'object') return ''
  53. const o =
  54. raw.user != null && typeof raw.user === 'object'
  55. ? raw.user
  56. : raw.data != null && typeof raw.data === 'object'
  57. ? raw.data
  58. : raw
  59. const m = o.mobile ?? o.phone ?? o.phone_number
  60. return m != null ? String(m).trim() : ''
  61. }
  62. export default {
  63. data() {
  64. return {
  65. mobile: '',
  66. captchaId: '',
  67. captchaCode: '',
  68. captchaImageSrc: '',
  69. captchaLoading: false,
  70. smsCode: '',
  71. newPassword: '',
  72. confirmPassword: '',
  73. sending: false,
  74. countdown: 0,
  75. timer: null,
  76. loading: false
  77. }
  78. },
  79. async onLoad() {
  80. this.refreshCaptcha()
  81. try {
  82. const raw = uni.getStorageSync('current_user')
  83. const fromStorage = extractMobileFromMe(raw)
  84. if (fromStorage) this.mobile = fromStorage
  85. } catch (e) {}
  86. const token = getToken()
  87. if (!token) return
  88. try {
  89. const me = await getCurrentUserInfo(token)
  90. const m = extractMobileFromMe(me)
  91. if (m) this.mobile = m
  92. } catch (e) {}
  93. },
  94. onShow() {
  95. if (!getToken()) {
  96. uni.showToast({ title: '请先登录', icon: 'none' })
  97. setTimeout(() => {
  98. uni.reLaunch({ url: '/pages/login/index' })
  99. }, 80)
  100. }
  101. },
  102. onUnload() {
  103. if (this.timer) clearInterval(this.timer)
  104. this.timer = null
  105. },
  106. methods: {
  107. async refreshCaptcha() {
  108. this.captchaLoading = true
  109. this.captchaCode = ''
  110. try {
  111. const data = await getCaptcha()
  112. this.captchaId = data.captcha_id != null ? String(data.captcha_id) : ''
  113. this.captchaImageSrc = normalizeCaptchaImageSrc(data.image)
  114. } catch (e) {
  115. this.captchaImageSrc = ''
  116. uni.showToast({ title: e.message || '图形码加载失败', icon: 'none' })
  117. } finally {
  118. this.captchaLoading = false
  119. }
  120. },
  121. async onSendSms() {
  122. if (this.sending || this.countdown > 0) return
  123. const m = String(this.mobile || '').trim()
  124. if (!m) {
  125. uni.showToast({ title: '请输入手机号', icon: 'none' })
  126. return
  127. }
  128. if (!this.captchaId) {
  129. uni.showToast({ title: '请等待图形验证码加载', icon: 'none' })
  130. return
  131. }
  132. const cc = String(this.captchaCode || '').trim()
  133. if (!cc) {
  134. uni.showToast({ title: '请输入图形验证码', icon: 'none' })
  135. return
  136. }
  137. this.sending = true
  138. try {
  139. await sendOpenSms(m, this.captchaId, cc)
  140. uni.showToast({ title: '短信已发送', icon: 'none' })
  141. this.countdown = 60
  142. this.timer = setInterval(() => {
  143. this.countdown -= 1
  144. if (this.countdown <= 0) {
  145. clearInterval(this.timer)
  146. this.timer = null
  147. this.countdown = 0
  148. }
  149. }, 1000)
  150. } catch (e) {
  151. uni.showToast({ title: e.message || '发送失败', icon: 'none' })
  152. this.refreshCaptcha()
  153. } finally {
  154. this.sending = false
  155. }
  156. },
  157. async onSubmit() {
  158. if (this.loading) return
  159. const m = String(this.mobile || '').trim()
  160. if (!m) {
  161. uni.showToast({ title: '请输入手机号', icon: 'none' })
  162. return
  163. }
  164. const code = String(this.smsCode || '').trim()
  165. if (!code) {
  166. uni.showToast({ title: '请输入短信验证码', icon: 'none' })
  167. return
  168. }
  169. const pwd = String(this.newPassword || '')
  170. const confirm = String(this.confirmPassword || '')
  171. if (!pwd) {
  172. uni.showToast({ title: '请输入新密码', icon: 'none' })
  173. return
  174. }
  175. if (!passwordMeetsRule(pwd)) {
  176. uni.showToast({ title: '新密码须同时包含字母与数字', icon: 'none' })
  177. return
  178. }
  179. if (pwd !== confirm) {
  180. uni.showToast({ title: '两次新密码不一致', icon: 'none' })
  181. return
  182. }
  183. this.loading = true
  184. try {
  185. await resetPasswordOpen(m, code, pwd)
  186. uni.showToast({ title: '修改成功,请重新登录', icon: 'success' })
  187. setToken('')
  188. try {
  189. uni.removeStorageSync('current_user')
  190. } catch (e) {}
  191. setTimeout(() => {
  192. uni.reLaunch({ url: '/pages/login/index' })
  193. }, 500)
  194. } catch (e) {
  195. uni.showToast({ title: e.message || '修改失败', icon: 'none' })
  196. } finally {
  197. this.loading = false
  198. }
  199. }
  200. }
  201. }
  202. </script>
  203. <style scoped>
  204. .change-page {
  205. min-height: 100vh;
  206. background: #fff;
  207. padding: 32rpx 48rpx 64rpx;
  208. box-sizing: border-box;
  209. }
  210. .hint {
  211. display: block;
  212. font-size: 26rpx;
  213. color: #888;
  214. line-height: 1.5;
  215. margin-bottom: 32rpx;
  216. }
  217. .form {
  218. margin-top: 8rpx;
  219. }
  220. .input {
  221. height: 88rpx;
  222. padding: 0 24rpx;
  223. background: #f7f7f7;
  224. border-radius: 16rpx;
  225. font-size: 28rpx;
  226. margin-bottom: 20rpx;
  227. }
  228. .captcha-row {
  229. display: flex;
  230. align-items: center;
  231. gap: 16rpx;
  232. margin-bottom: 20rpx;
  233. }
  234. .captcha-input {
  235. flex: 1;
  236. margin-bottom: 0;
  237. }
  238. .captcha-img-wrap {
  239. width: 220rpx;
  240. height: 88rpx;
  241. border-radius: 16rpx;
  242. background: #f0f0f0;
  243. overflow: hidden;
  244. display: flex;
  245. align-items: center;
  246. justify-content: center;
  247. flex-shrink: 0;
  248. }
  249. .captcha-img {
  250. width: 100%;
  251. height: 100%;
  252. }
  253. .captcha-placeholder {
  254. font-size: 22rpx;
  255. color: #999;
  256. padding: 0 8rpx;
  257. text-align: center;
  258. }
  259. .sms-row {
  260. display: flex;
  261. align-items: center;
  262. gap: 16rpx;
  263. margin-bottom: 20rpx;
  264. }
  265. .sms-input {
  266. flex: 1;
  267. margin-bottom: 0;
  268. }
  269. .sms-btn {
  270. height: 88rpx;
  271. padding: 0 24rpx;
  272. border-radius: 16rpx;
  273. display: flex;
  274. align-items: center;
  275. justify-content: center;
  276. background: #259653;
  277. color: #fff;
  278. font-size: 26rpx;
  279. }
  280. .sms-btn.disabled {
  281. opacity: 0.6;
  282. }
  283. .submit {
  284. margin-top: 32rpx;
  285. height: 92rpx;
  286. border-radius: 18rpx;
  287. background: #259653;
  288. display: flex;
  289. align-items: center;
  290. justify-content: center;
  291. color: #fff;
  292. font-size: 30rpx;
  293. }
  294. .submit.disabled {
  295. opacity: 0.7;
  296. }
  297. </style>