欢迎有需求的老板进行合作点击合作
文档信息
文档版本:V1.0
适用平台:微信小程序、App(iOS/Android)、H5(全端兼容,基于 uni-app)
开发环境:uni-app + Vue3/Vue2 + uView/ui 组件库(可选)
适用场景:登录密码、支付密码、隐私解锁、设备管理等安全验证场景
一、产品概述
1.1 功能背景
为满足应用内高安全性验证需求,需开发一套独立、美观、交互友好的密码锁按键组件,支持数字密码输入、错误提示、输入限制、安全掩码展示等核心能力,适配 uni-app 全端运行。
1.2 核心目标
实现纯数字 6 位 / 自定义位数安全密码输入
提供标准化数字按键面板,交互符合用户习惯
支持密码掩码展示、输入限制、错误重试、重置等能力
全端样式统一,无兼容问题,支持自定义主题
满足安全合规要求:不明文展示密码、不本地明文存储
二、功能需求
2.1 核心功能清单
2.2 详细功能说明
2.2.1 密码输入框
位数配置:默认 6 位,支持通过参数修改为 4/5/6 位
展示样式:
分隔式输入框(推荐):6 个独立方框,输入后自动填充并跳转下一位
无分隔样式:整体横线式输入
状态定义
空闲态:未输入,展示空框 / 下划线
输入态:当前位高亮 / 展示光标
填充态:已输入位展示掩码
错误态:边框变红 + 整体抖动动画
2.2.2 数字按键面板
布局:3×4 网格布局
数字键:1、2、3、4、5、6、7、8、9、0
功能键:删除(←)、重置(C / 清空)
排序规则
固定排序:默认顺序
随机排序:每次渲染按键位置随机(金融 / 支付场景推荐)
交互规则
点击数字键:追加到密码末尾,最多不超过设定位数
点击删除键:删除最后一位,无内容时不响应
点击重置键:一键清空所有输入内容
反馈效果
视觉反馈:点击时按钮缩放 / 背景变色
触感反馈:移动端开启震动(uni.createVibrateShort)
音效反馈:可选开启按键音效
2.2.3 验证与安全逻辑
自动验证:输满指定位数后,自动触发验证事件
手动验证:支持父组件调用方法主动验证
错误处理
密码错误:输入框抖动 + 底部文字提示,自动清空输入
重试锁定:达到最大错误次数(如 5 次),锁定键盘 N 分钟
安全规则
不暴露密码明文,仅向父组件传递加密 / 原始密码
输入过程中不输出日志、不缓存明文
页面卸载自动清空密码内容
2.2.4 状态与提示
正常提示:请输入密码、请再次输入密码
错误提示:密码错误,请重新输入
锁定提示:尝试次数过多,请 X 分钟后重试
成功状态:验证通过,自动关闭键盘 / 执行回调
三、非功能需求
3.1 性能需求
渲染速度:键盘面板首次渲染≤200ms
响应速度:点击按键响应≤100ms
内存占用:无内存泄漏,页面销毁自动释放资源
3.2 兼容性需求
兼容 uni-app 全端:微信小程序、支付宝小程序、App、H5
兼容竖屏模式,适配全面屏、刘海屏、底部安全区
兼容 Vue2/Vue3 语法
3.3 安全需求
密码输入过程不可截屏录屏(App 端可配置)
不明文存储密码,不明文传输
防止暴力破解:错误次数锁定机制
3.4 视觉需求
风格统一,支持深色 / 浅色模式
支持自定义主题色、圆角、大小
动画流畅无卡顿
四、界面原型与交互流程
4.1 界面结构
┌─────────────────────────┐│ 请输入密码 │ 标题区├─────────────────────────┤│ □ □ □ □ □ □ │ 输入框区(6位)├─────────────────────────┤│ 1 2 3 删除 ││ 4 5 6 重置 │ 按键区│ 7 8 9 确定 ││ 0 │└─────────────────────────┘
4.2 交互流程
打开页面 → 渲染密码输入框 + 数字键盘
点击数字 → 填充掩码,自动跳转到下一位
输满 6 位 → 自动提交验证
验证成功 → 触发成功回调,跳转页面
验证失败 → 输入框抖动 + 提示文字,清空输入
多次失败 → 锁定键盘,显示倒计时
五、接口与组件参数
5.1 组件 Props 参数
5.2 组件事件
5.3 组件方法(ref 调用)
六、开发要求
6.1 技术实现
基于 uni-app 官方组件开发,不依赖复杂原生插件
使用 Vue 响应式数据管理输入状态
CSS 使用 flex 布局,适配全端
动画使用 CSS3 transition/animation
6.2 代码规范
组件化开发,支持全局引用 / 单页引用
注释清晰,便于维护
禁止使用 eval、禁止明文日志输出
6.3 测试要点
输入限制:不能超过指定位数
删除 / 重置功能正常
错误重试与锁定逻辑正常
随机按键每次打开顺序不同
全端样式无错位
七、验收标准
功能完整:所有需求点 100% 实现
交互流畅:点击、输入、动画无卡顿
安全合规:密码不明文展示、不泄露
全端兼容:在小程序、App、H5 均正常运行
错误处理:异常场景不崩溃、提示友好
八、附录
8.1 术语说明
掩码:密码隐藏符号●
自动验证:输满位数后自动提交
锁定:密码错误次数超限,暂时禁止输入
8.2 最终效果如图



代码布局如下
第一步.新建components/SafePasswordLock.vue
<template><viewclass="safe-password-lock"><!-- 标题区 --><viewclass="title-area"><slotname="title"><textclass="title-text">{{ title }}</text></slot></view><!-- 密码输入框区(flex 一排) --><viewclass="input-area":class="{ 'error-shake': shake }"><viewv-for="(_, index) in length":key="index"class="input-box":class="{'filled': index < password.length,'active': !isLocked && index === password.length && !isFull,'error': showErrorBorder}":style="{width: boxWidth,height: boxHeight,borderColor: showErrorBorder ? '#ff4d4f' : (index === password.length && !isLocked && !isFull ? themeColor : '#e0e0e0')}"><textclass="mask-char">{{ index < password.length ? mask : '' }}</text><!-- 模拟光标 --><viewv-if="!isLocked && index === password.length && !isFull"class="cursor":style="{ backgroundColor: themeColor }"></view></view></view><!-- 提示信息区 --><viewclass="message-area"><textclass="message-text":style="{ color: errorMsg ? '#ff4d4f' : '#999' }">{{ errorMsg || tipText }}</text></view><!-- 数字键盘区 --><viewclass="keyboard-area"v-if="!isLocked"><!-- 数字按键区(支持随机排序) --><viewclass="number-keys"><viewv-for="(num, idx) in shuffledNumbers":key="idx"class="key-btn number-btn"hover-class="key-hover"@click="onNumberClick(num)"><text>{{ num }}</text></view></view><!-- 功能键:删除 & 重置 --><viewclass="function-row"><viewclass="key-btn func-btn"hover-class="key-hover" @click="onDeleteClick"><text>⌫</text></view><viewclass="key-btn func-btn"hover-class="key-hover" @click="onResetClick"><text>C</text></view></view><!-- 可选内部确认按钮(默认隐藏,由父组件控制) --><viewclass="confirm-row"v-if="showConfirmBtn"><viewclass="key-btn confirm-btn"hover-class="key-hover" @click="onConfirmClick":style="{ backgroundColor: themeColor, color: '#fff' }"><text>确 认</text></view></view></view><!-- 锁定状态展示 --><viewv-elseclass="lock-area"><textclass="lock-text">尝试次数过多,请 {{ lockRemainSeconds }} 秒后重试</text></view></view></template><scriptsetup>import { ref, computed, watch, onMounted, onUnmounted } from 'vue'// 定义 props(符合 uni-app 规范)const props = defineProps({// 密码位数length: {type: Number,default: 6},// 是否随机排序按键randomKey: {type: Boolean,default: false},// 最大重试次数maxRetry: {type: Number,default: 5},// 锁定时间(秒)lockTime: {type: Number,default: 60},// 密码掩码字符mask: {type: String,default: '●'},// 主题色themeColor: {type: String,default: '#1677ff'},// 是否开启震动vibrate: {type: Boolean,default: true},// 标题文案title: {type: String,default: '请输入密码'},// 提示文案(正常时显示)tipText: {type: String,default: ' '},// 是否显示组件内部的确认按钮showConfirmBtn: {type: Boolean,default: false},// 密码框宽度(rpx)boxWidth: {type: String,default: '88rpx'},// 密码框高度(rpx)boxHeight: {type: String,default: '88rpx'}})// 定义事件(kebab-case 命名)const emit = defineEmits(['verify', 'success', 'error', 'lock'])// 内部状态const password = ref('')const errorMsg = ref('')const shake = ref(false)const retryCount = ref(0)const isLocked = ref(false)const lockEndTime = ref(0)let lockTimer = nullconst shuffledNumbers = ref(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'])// 计算属性const isFull = computed(() => password.value.length === props.length)const showErrorBorder = computed(() => !!errorMsg.value)const lockRemainSeconds = computed(() => {if (!isLocked.value || !lockEndTime.value) return 0const remain = Math.ceil((lockEndTime.value - Date.now()) / 1000)return remain > 0 ? remain : 0})// 震动反馈(使用 uni API)const doVibrate = () => {if (props.vibrate && uni && uni.vibrateShort) {uni.vibrateShort({})}}// 抖动动画const triggerShake = () => {shake.value = truesetTimeout(() => {shake.value = false}, 300)}const clearPassword = () => {password.value = ''errorMsg.value = ''}const resetAllState = () => {clearLockTimer()isLocked.value = falseretryCount.value = 0errorMsg.value = ''lockEndTime.value = 0clearPassword()}const handleError = (customMsg = '密码错误,请重新输入') => {retryCount.value += 1errorMsg.value = customMsgtriggerShake()clearPassword()emit('error', { message: customMsg, retryCount: retryCount.value })if (retryCount.value >= props.maxRetry) {startLock()}}const startLock = () => {isLocked.value = truelockEndTime.value = Date.now() + props.lockTime * 1000emit('lock', { remainSeconds: props.lockTime })lockTimer = setInterval(() => {if (Date.now() >= lockEndTime.value) {clearLockTimer()isLocked.value = falseretryCount.value = 0errorMsg.value = ''lockEndTime.value = 0}}, 1000)}const clearLockTimer = () => {if (lockTimer) {clearInterval(lockTimer)lockTimer = null}}// 验证密码(触发 verify 事件)const verifyPassword = () => {if (isLocked.value) {errorMsg.value = '键盘已锁定,请稍后重试'return}if (password.value.length !== props.length) {errorMsg.value = `请输入{data.remainSeconds}秒`,icon: 'none'})}</script><stylescoped>.demo-page {padding: 40rpx;}.mask {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(0, 0, 0, 0.5);display: flex;align-items: center;justify-content: center;z-index: 999;}.lock-container {background-color: #fff;border-radius: 32rpx;padding: 40rpx 30rpx;width: 600rpx;}.custom-confirm-btn {background-color: #1677ff;color: #fff;text-align: center;padding: 24rpx 0;border-radius: 60rpx;margin-top: 30rpx;font-size: 32rpx;font-weight: 500;}</style>
夜雨聆风