Sfoglia il codice sorgente

fix: 完成重置密码页面,新增错误校验功能。

yuanmingze 8 mesi fa
parent
commit
a107ec8dfb

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "@dcloudio/uni-mp-xhs": "3.0.0-4080420251103001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-4080420251103001",
     "@dcloudio/uni-ui": "^1.5.11",
+    "async-validator": "^4.2.5",
     "dayjs": "^1.11.19",
     "lodash-es": "^4.17.22",
     "luch-request": "^3.1.1",

+ 8 - 0
pnpm-lock.yaml

@@ -59,6 +59,9 @@ importers:
       '@dcloudio/uni-ui':
         specifier: ^1.5.11
         version: 1.5.11
+      async-validator:
+        specifier: ^4.2.5
+        version: 4.2.5
       dayjs:
         specifier: ^1.11.19
         version: 1.11.19
@@ -2060,6 +2063,9 @@ packages:
   array-flatten@1.1.1:
     resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
 
+  async-validator@4.2.5:
+    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
+
   asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
 
@@ -7140,6 +7146,8 @@ snapshots:
 
   array-flatten@1.1.1: {}
 
+  async-validator@4.2.5: {}
+
   asynckit@0.4.0: {}
 
   autoprefixer@10.4.20(postcss@8.5.6):

+ 69 - 0
src/composables/useFormValidator.ts

@@ -0,0 +1,69 @@
+import { ref } from 'vue'
+import Schema, { type Rules, type RuleItem, type ValidateError } from 'async-validator'
+
+export function useFormValidator<T extends Record<string, any>>(rules: Rules, model: T) {
+  const schema = new Schema(rules)
+  const errors = ref<Partial<Record<keyof T, string>>>({})
+
+  /** 全量校验 */
+  const validate = async () => {
+    // ⭐ 核心:每次全量校验前,清空全部错误
+    errors.value = {}
+
+    try {
+      await schema.validate(model)
+      return true
+    } catch (e) {
+      applyErrors(e)
+      return false
+    }
+  }
+
+  /** 单字段校验(获取验证码 / blur 等) */
+  const validateField = async (field: keyof T) => {
+    // ⭐ 核心:每次字段校验前,只清空该字段错误
+    delete errors.value[field]
+
+    try {
+      const fieldRules = rules[field as string] as RuleItem | RuleItem[] | undefined
+      if (!fieldRules) return true
+
+      const fieldSchema = new Schema({ [field as string]: fieldRules })
+      await fieldSchema.validate({ [field as string]: model[field] })
+
+      return true
+    } catch (e) {
+      applyErrors(e, field)
+      return false
+    }
+  }
+
+  const clearErrors = () => {
+    errors.value = {}
+  }
+
+  /** 只负责“写入错误”,不负责清理 */
+  const applyErrors = (e: unknown, onlyField?: keyof T) => {
+    const err = e as { errors: ValidateError[] }
+    const map = { ...errors.value }
+
+    err.errors.forEach((item) => {
+      const f = item.field as keyof T
+      if (!onlyField || f === onlyField) {
+        // 同一字段只保留第一条错误
+        if (!map[f]) {
+          map[f] = item.message
+        }
+      }
+    })
+
+    errors.value = map
+  }
+
+  return {
+    errors,
+    validate,
+    validateField,
+    clearErrors,
+  }
+}

+ 4 - 2
src/pages/login/index.vue

@@ -59,10 +59,12 @@ const form = reactive({
 })
 
 const forgetPassword = () => {
-  console.log('cc')
+  uni.navigateTo({
+    url: '/pages/reset-password/index',
+  })
 }
 </script>
 
 <style lang="scss" scoped>
-@import '@/styles/modules/auth-page.scss';
+@import '../../styles/modules/auth-page.scss';
 </style>

+ 55 - 18
src/pages/reset-password/index.vue

@@ -1,13 +1,18 @@
 <template>
   <view class="auth-content">
-    <wd-navbar fixed placeholder :title="title" left-arrow safeAreaInsetTop customClass="navbar" />
+    <wd-navbar
+      fixed
+      placeholder
+      title="重置密码"
+      left-arrow
+      safeAreaInsetTop
+      customClass="navbar"
+    />
     <!-- 背景层 -->
     <view class="auth-bg"></view>
     <!-- 内容层 -->
     <view class="auth-body">
-      <view class="auth-title"> 欢迎登录 </view>
-
-      <!-- 表单(内部类名不动) -->
+      <!-- 表单-->
       <view class="auth-form">
         <form>
           <view class="form-item">
@@ -16,32 +21,42 @@
               <input
                 class="form-input-inner"
                 type="number"
-                focus
                 maxlength="11"
                 placeholder="请输入账号"
+                v-model="form.phone"
               />
             </view>
+            <text class="error" v-if="errors.phone">{{ errors.phone }}</text>
           </view>
 
           <view class="form-item">
-            <view class="form-label">码</view>
+            <view class="form-label">验证码</view>
             <view class="form-input">
-              <input class="form-input-inner" password placeholder="请输入密码" />
-              <view class="forget-password" @click="forgetPassword">忘记密码?</view>
+              <input class="form-input-inner" v-model="form.code" placeholder="请输入验证码" />
+              <button class="get-code" @click="sendCode">发送验证码</button>
             </view>
+            <text class="error" v-if="errors.code">{{ errors.code }}</text>
           </view>
 
           <view class="form-item">
-            <view class="form-label">验证码</view>
+            <view class="form-label">码</view>
             <view class="form-input">
-              <input class="form-input-inner" placeholder="请输入验证码" />
-              <button class="get-code">发送验证码</button>
+              <input
+                class="form-input-inner"
+                v-model="form.password"
+                password
+                placeholder="请输入密码"
+              />
             </view>
+            <text class="error" v-if="errors.password">{{ errors.password }}</text>
+          </view>
+          <view class="tips">
+            密码为8~16位包含大小写字母、数字、!#$%^&*@特殊符号,且不能三位连续数字!
           </view>
         </form>
 
         <view class="submit">
-          <button class="submit-btn">确定</button>
+          <button class="submit-btn" @click="submit">确定</button>
         </view>
       </view>
     </view>
@@ -49,20 +64,42 @@
 </template>
 
 <script setup lang="ts">
-import projectConfig from '@/config/presets/index'
-const { title } = projectConfig
+import { reactive } from 'vue'
+import { useFormValidator } from '@/composables/useFormValidator'
+import { resetPasswordRules } from '@/validators/resetPassword'
 
+/** 表单模型 */
 const form = reactive({
   phone: '',
-  password: '',
   code: '',
+  password: '',
 })
 
-const forgetPassword = () => {
-  console.log('cc')
+/** 表单校验 Hook */
+const { errors, validate, validateField } = useFormValidator(resetPasswordRules, form)
+
+/** 提交 */
+const submit = async () => {
+  const ok = await validate()
+  console.log('ok', ok)
+  console.log('form', form)
+
+  if (!ok) return
+
+  console.log('校验通过', form)
+  // TODO: 提交重置密码接口
+}
+
+/** 获取验证码前校验 */
+const sendCode = async () => {
+  const ok = await validateField('phone')
+  if (!ok) return
+
+  console.log('发送验证码', form.phone)
+  // TODO: 调用发送验证码接口
 }
 </script>
 
 <style lang="scss" scoped>
-@import '@/styles/modules/auth-page.scss';
+@import '../../styles/modules/auth-page.scss';
 </style>

+ 11 - 0
src/styles/modules/auth-page.scss

@@ -87,6 +87,17 @@
           }
         }
       }
+      .error {
+        color: var(--form-error-font-color);
+        font-size: 24rpx;
+        margin-top: 8rpx;
+        line-height: 28rpx;
+      }
+      .tips {
+        margin-top: 30rpx;
+        font-size: 24rpx;
+        color: var(--font-color-666);
+      }
 
       .submit {
         display: flex;

+ 1 - 0
src/styles/variables/vars-base.scss

@@ -2,6 +2,7 @@
 page {
   --main-color: #3fa3f1;
   --main-color-rgb: 63, 163, 241;
+  --form-error-font-color: #ff4d4f;
   --font-color-fff: #ffffff;
   --font-color-333: #333333;
   --font-color-666: #666666;

+ 65 - 0
src/validators/resetPassword.ts

@@ -0,0 +1,65 @@
+// validators/resetPassword.ts
+import type { Rules, RuleItem } from 'async-validator'
+
+/** 是否开发环境 */
+const isDev = import.meta.env.VITE_MODE === 'development'
+
+/** 三位连续数字校验 */
+function hasThreeConsecutiveDigits(value: string) {
+  for (let i = 0; i <= value.length - 3; i++) {
+    const a = value.charCodeAt(i) - 48
+    const b = value.charCodeAt(i + 1) - 48
+    const c = value.charCodeAt(i + 2) - 48
+    if (a >= 0 && a <= 9 && b >= 0 && b <= 9 && c >= 0 && c <= 9) {
+      if (b === a + 1 && c === b + 1) return true
+    }
+  }
+  return false
+}
+
+/** 生产级密码校验 */
+const strictPasswordValidator: RuleItem['validator'] = (_rule, value, callback) => {
+  const v = String(value ?? '')
+
+  if (v.length < 8 || v.length > 16) {
+    return callback(new Error('密码长度需为 8~16 位'))
+  }
+  if (!/[A-Z]/.test(v)) {
+    return callback(new Error('必须包含大写字母'))
+  }
+  if (!/[a-z]/.test(v)) {
+    return callback(new Error('必须包含小写字母'))
+  }
+  if (!/\d/.test(v)) {
+    return callback(new Error('必须包含数字'))
+  }
+  if (!/[!#$%^&*@]/.test(v)) {
+    return callback(new Error('必须包含特殊符号 !#$%^&*@'))
+  }
+  if (hasThreeConsecutiveDigits(v)) {
+    return callback(new Error('不能包含三位连续数字'))
+  }
+
+  callback()
+}
+
+/** 根据环境生成 password 规则 */
+function createPasswordRules(): RuleItem[] {
+  // 开发环境:只校验是否存在
+  if (isDev) {
+    return [{ required: true, message: '密码必填' }]
+  }
+
+  // 非开发环境:完整校验
+  return [{ required: true, message: '密码必填' }, { validator: strictPasswordValidator }]
+}
+
+/** 重置密码表单校验规则 */
+export const resetPasswordRules: Rules = {
+  phone: [
+    { required: true, message: '账号必填' },
+    { pattern: /^\d{11}$/, message: '账号必须为 11 位纯数字' },
+  ],
+  code: [{ required: true, message: '验证码必填' }],
+  password: createPasswordRules(),
+}