forgotPassword.vue 9.3 KB

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