forgotPassword.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <route lang="json5" type="page">
  2. {
  3. layout: 'default',
  4. style: {
  5. navigationStyle: 'custom',
  6. },
  7. }
  8. </route>
  9. <script lang="ts" setup>
  10. import { getCode, updateUserPassword } from '@/api/login'
  11. import { t } from '@/locale'
  12. import { useUserStore } from '@/store'
  13. import { goBack as goBackUtil, toPage } from '@/utils/page'
  14. import { toast } from '@/utils/toast'
  15. defineOptions({
  16. name: 'ForgotPassword', // 忘记密码
  17. })
  18. const isLogined = computed(() => {
  19. const userStore = useUserStore()
  20. return !!userStore.token
  21. })
  22. // 获取屏幕边界到安全区域距离
  23. const systemInfo = uni.getSystemInfoSync()
  24. const safeAreaInsets = systemInfo.safeAreaInsets
  25. // 步骤控制:1-输入手机号,2-重置密码
  26. const step = ref(1)
  27. // 表单数据
  28. const formData = ref({
  29. phone: '',
  30. verifyCode: '',
  31. newPwd: '',
  32. confirmPwd: '',
  33. })
  34. // 验证码倒计时
  35. const countdown = ref(0)
  36. const countdownTimer = ref<any>(null)
  37. // 获取验证码
  38. async function getVerificationCode() {
  39. // 验证手机号
  40. if (!formData.value.phone.trim()) {
  41. toast.error(t('auth.forgotPassword.error.emptyPhone'))
  42. return
  43. }
  44. // 孟加拉手机号校验:可接受本地格式 01xxxxxxxxx (11 位) 或去掉前导 0 的 1xxxxxxxxx (10 位)
  45. const phoneDigits = formData.value.phone.replace(/\D/g, '')
  46. const bdPhoneRegex = /^0?1\d{9}$/
  47. if (!bdPhoneRegex.test(phoneDigits)) {
  48. toast.error(t('auth.register.error.invalidPhone'))
  49. return
  50. }
  51. // 防止重复点击
  52. if (countdown.value > 0) {
  53. return
  54. }
  55. try {
  56. // 显示加载状态
  57. uni.showLoading({
  58. title: t('common.loading'),
  59. mask: true,
  60. })
  61. // 调用获取验证码接口
  62. await getCode(formData.value.phone)
  63. uni.hideLoading()
  64. toast.success(t('auth.forgotPassword.success.codeSent'))
  65. // 开始倒计时
  66. countdown.value = 60
  67. countdownTimer.value = setInterval(() => {
  68. countdown.value--
  69. if (countdown.value <= 0) {
  70. clearInterval(countdownTimer.value!)
  71. countdownTimer.value = null
  72. }
  73. }, 1000)
  74. }
  75. catch (error) {
  76. uni.hideLoading()
  77. toast.error(error.message || t('auth.forgotPassword.error.sendCodeFailed'))
  78. }
  79. }
  80. // 第一步:验证手机号
  81. function handleStep1() {
  82. // 验证手机号
  83. if (!formData.value.phone.trim()) {
  84. toast.error(t('auth.forgotPassword.error.emptyPhone'))
  85. return
  86. }
  87. // 孟加拉手机号校验:可接受本地格式 01xxxxxxxxx (11 位) 或去掉前导 0 的 1xxxxxxxxx (10 位)
  88. const phoneDigits = formData.value.phone.replace(/\D/g, '')
  89. const bdPhoneRegex = /^0?1\d{9}$/
  90. if (!bdPhoneRegex.test(phoneDigits)) {
  91. toast.error(t('auth.register.error.invalidPhone'))
  92. return
  93. }
  94. // 跳转到第二步
  95. step.value = 2
  96. }
  97. // 第二步:重置密码
  98. async function handleResetPassword() {
  99. try {
  100. // 表单验证
  101. const isValid = await validateResetForm()
  102. if (!isValid)
  103. return
  104. // 显示加载状态
  105. uni.showLoading({
  106. title: t('common.saving'),
  107. mask: true,
  108. })
  109. // 调用重置密码接口
  110. const resetData = {
  111. phoneNo: formData.value.phone,
  112. newPwd: formData.value.newPwd,
  113. verifyCode: formData.value.verifyCode,
  114. }
  115. await updateUserPassword(resetData)
  116. uni.hideLoading()
  117. toast.success(t('auth.forgotPassword.success.passwordReset'))
  118. // 跳转到登录页
  119. setTimeout(() => {
  120. toPage({ url: '/pages/login/login', isReLaunch: true })
  121. }, 1500)
  122. }
  123. catch (error) {
  124. uni.hideLoading()
  125. toast.error(error.message || t('auth.forgotPassword.error.resetFailed'))
  126. }
  127. }
  128. // 重置密码表单验证
  129. function validateResetForm() {
  130. return new Promise((resolve) => {
  131. // 验证验证码
  132. if (!formData.value.verifyCode.trim()) {
  133. toast.error(t('auth.forgotPassword.error.emptyVerifyCode'))
  134. resolve(false)
  135. return
  136. }
  137. // 验证新密码
  138. if (!formData.value.newPwd.trim()) {
  139. toast.error(t('auth.forgotPassword.error.emptyNewPassword'))
  140. resolve(false)
  141. return
  142. }
  143. // 验证密码长度
  144. if (formData.value.newPwd.length < 6 || formData.value.newPwd.length > 20) {
  145. toast.error(t('auth.forgotPassword.error.passwordLength'))
  146. resolve(false)
  147. return
  148. }
  149. // 验证确认密码
  150. if (!formData.value.confirmPwd.trim()) {
  151. toast.error(t('auth.forgotPassword.error.emptyConfirmPassword'))
  152. resolve(false)
  153. return
  154. }
  155. // 验证两次密码是否一致
  156. if (formData.value.newPwd !== formData.value.confirmPwd) {
  157. toast.error(t('auth.forgotPassword.error.passwordMismatch'))
  158. resolve(false)
  159. return
  160. }
  161. resolve(true)
  162. })
  163. }
  164. // 返回上一页
  165. function goBack() {
  166. if (step.value === 2) {
  167. step.value = 1
  168. }
  169. else {
  170. goBackUtil()
  171. }
  172. }
  173. // 页面卸载时清理定时器
  174. onUnmounted(() => {
  175. if (countdownTimer.value) {
  176. clearInterval(countdownTimer.value)
  177. }
  178. })
  179. </script>
  180. <template>
  181. <view class="forgot-password-page relative min-h-screen bg-white">
  182. <!-- 背景图片区域 -->
  183. <view class="auth-bg-section relative">
  184. <!-- 自定义导航栏 -->
  185. <view :style="{ paddingTop: `${safeAreaInsets?.top}px` }">
  186. <view class="h-88rpx flex items-center px-24rpx">
  187. <wd-icon name="thin-arrow-left" size="32rpx" @click="() => goBack()" />
  188. </view>
  189. </view>
  190. <!-- Logo和标语 -->
  191. <view class="pb-40rpx pt-134rpx text-center">
  192. <view class="mb-20rpx flex flex-col items-center justify-center">
  193. <image src="/static/login-logo.png" class="mb-18rpx h-56rpx w-350.48rpx" />
  194. <view>{{ $t('login.slogan') }}</view>
  195. </view>
  196. </view>
  197. </view>
  198. <!-- 表单内容区域 -->
  199. <view class="flex flex-col px-20rpx">
  200. <view class="mb-40rpx" />
  201. <!-- 第一步:输入手机号 -->
  202. <view v-if="step === 1">
  203. <wd-form ref="form" :model="formData">
  204. <view class="bandhu-auth-input-field phone-input-wrapper mb-60rpx" style="border: none; display:flex;align-items:center;">
  205. <view class="phone-area-code" style="padding:0 8rpx;font-size:28rpx;color:#333">
  206. +88
  207. </view>
  208. <wd-input
  209. v-model="formData.phone"
  210. prop="phone"
  211. :placeholder="t('auth.forgotPassword.phone.placeholder')"
  212. no-border
  213. type="number"
  214. custom-class="flex-1"
  215. />
  216. </view>
  217. <!-- 重置密码按钮 -->
  218. <wd-button
  219. size="large"
  220. block
  221. custom-class="mb-40rpx"
  222. @click="handleStep1"
  223. >
  224. {{ $t('auth.forgotPassword.button') }}
  225. </wd-button>
  226. </wd-form>
  227. </view>
  228. <!-- 第二步:重置密码 -->
  229. <view v-else>
  230. <wd-form ref="form" :model="formData">
  231. <view class="mb-40rpx space-y-32rpx">
  232. <view class="bandhu-auth-input-field phone-input-wrapper" style="border: none; display:flex;align-items:center;">
  233. <view class="phone-area-code" style="padding:0 8rpx;font-size:28rpx;color:#333">
  234. +88
  235. </view>
  236. <wd-input
  237. v-model="formData.phone"
  238. prop="phone"
  239. no-border
  240. readonly
  241. type="number"
  242. custom-class="flex-1"
  243. />
  244. </view>
  245. <view class="flex items-center gap-20rpx">
  246. <wd-input
  247. v-model="formData.verifyCode"
  248. prop="verifyCode"
  249. :placeholder="t('auth.forgotPassword.verifyCode.placeholder')"
  250. no-border
  251. type="number"
  252. custom-class="flex-1 bandhu-auth-input-field"
  253. />
  254. <wd-button
  255. plain
  256. :disabled="countdown > 0"
  257. custom-class="bandhu-auth-secondary-btn"
  258. @click="getVerificationCode"
  259. >
  260. {{ countdown > 0 ? `${countdown}s` : t('auth.forgotPassword.getCode') }}
  261. </wd-button>
  262. </view>
  263. <wd-input
  264. v-model="formData.newPwd"
  265. prop="newPwd"
  266. :placeholder="t('auth.forgotPassword.newPassword.placeholder')"
  267. no-border show-password
  268. custom-class="bandhu-auth-input-field"
  269. />
  270. <wd-input
  271. v-model="formData.confirmPwd"
  272. prop="confirmPwd"
  273. show-password
  274. :placeholder="t('auth.forgotPassword.confirmPassword.placeholder')"
  275. no-border
  276. custom-class="bandhu-auth-input-field"
  277. />
  278. </view>
  279. <!-- 密码提示 -->
  280. <view class="mb-28rpx px-20rpx text-center text-#5C5C5C line-height-56rpx">
  281. {{ $t('auth.forgotPassword.passwordHint') }}
  282. </view>
  283. <!-- 重置密码按钮 -->
  284. <wd-button
  285. size="large"
  286. block
  287. custom-class="mb-40rpx"
  288. @click="handleResetPassword"
  289. >
  290. {{ $t('auth.forgotPassword.button') }}
  291. </wd-button>
  292. </wd-form>
  293. </view>
  294. <!-- 登录提示 -->
  295. <view v-if="!isLogined" class="absolute bottom-20rpx left-0 w-full text-center" :style="{ bottom: `${safeAreaInsets?.bottom + 20}px` }">
  296. <text class="text-28rpx text-#5C5C5C">
  297. {{ $t('auth.forgotPassword.hasAccount') }}
  298. </text>
  299. <text class="ml-10rpx text-28rpx text-[var(--wot-color-theme)]" @click="toPage({ url: '/pages/login/login' })">
  300. {{ $t('auth.forgotPassword.loginNow') }}
  301. </text>
  302. </view>
  303. </view>
  304. </view>
  305. </template>
  306. <style lang="scss" scoped>
  307. // 忘记密码页面特有样式(如果有的话)
  308. </style>