【山竹记账前端-react】20.数字键盘输入金额和金额显示
大纲链接 §
[toc]
1. 输入金额和金额显示逻辑 ⇧
实现逻辑 ⇧
默认显示
¥ 0.00
- 输入有效数字和小数点
- 初始输入
1-9,显示1-9 - 之后输入任意数字,显示对应数字追加到后一位,但不超过最长允许长度
- 之后输入
.,显示整数部分 +.- 之后输入任意数字,小数部分只可输入最多两位数字
- 之后再输入
.,不往后追加,仍显示原来
- 初始输入
0,显示0 - 之后输入任意数字,去除首位的零,替换为输入的数字
- 之后输入
.,显示0.- 之后输入任意数字,小数部分只可输入最多两位数字
- 之后再输入
.,不往后追加,仍显示原来
- 初始输入
.,显示0. - 之后输入任意数字,小数部分只可输入最多两位数字
- 之后再输入
.,不往后追加,仍显示原来
- 初始输入
- 点击删除按钮
- 一次只能删除最后一位数字或
. - 删除到只剩最后一位数字,再删除,恢复为初始默认显示
¥ 0.00
- 一次只能删除最后一位数字或
- 点击归零按钮,都恢复为初始默认显示
¥ 0.00
使用 zustand 统一管理 useNumpadAmountStore ⇧
@/stores/useNumpadAmountStore.ts
|
|
- 一个数据状态
amount读写金额状态 - 三个逻辑:
appendText- 表驱动 遍历条件修改:
- 输入参数
inputText - 显示金额
amount - 拼接逻辑
set((state) => ({ amount: state.amount + inputText }))
- 输入参数
backspaceTextclearText
- 重构替换
useState拦截器写法
使用 useNumpadButtons 抽象按钮数据:包括点击逻辑,额外样式 ⇧
@/modules/items/input/useNumpadButtons.tsx
|
|
- 引用键盘按钮操作逻辑
const { appendText, clearText, backspaceText } = useNumpadAmountStore() - 注意使用
cn函数拼接类名,必须在TSX中使用,才有样式代码提示 - 可使用
text或者icon作为按钮内容 - 支持单个按钮添加额外样式
extraClass
Numpad 组件使用 ⇧
@/modules/items/input/Numpad.tsx集成useNumpadButtons
|
|
添加事件总线,解耦通知 ⇧
安装事件总线和提示ui的库 ⇧
|
|
创建 useGlobalHintsBus.ts ⇧
@/hooks/useGlobalHintsBus.ts
|
|
- 自定义提示事件类型写在
GlobalHintsEvents中 - 配合使用
sweetalert2提示框,payload传参类型参考SweetAlertOptions
封装组件内通用事件钩子 useNanoEvent.tsx ⇧
@/hooks/useNanoEvent.tsx
- 作为组件生命周期事件订阅逻辑钩子,负责挂载时订阅事件,和卸载时移除事件监听
接受
event和handler1 2 3 4 5 6 7 8 9 10 11 12 13 14import { GlobalHintsEvents, HintPayload, useGlobalHintsBus } from '@/hooks/useGlobalHintsBus' import { useEffect } from 'react' export const useNanoEvent = <K extends keyof GlobalHintsEvents = 'hint'>( event: K, handler: (payload?: HintPayload) => void ) => { const { on } = useGlobalHintsBus() useEffect(() => { const unbind = on(event, handler) return unbind }, [event, handler, on]) }
封装提示处理逻辑 ⇧
@/modules/items/input/numpad/numberLimitListener.tsx
- 在监听函数中写提示逻辑
使用
sweetalert2、sweetalert2-react-content第三方库作为提示UI组件1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25import type { HintPayload } from '@/hooks/useGlobalHintsBus' import SwAl, { SweetAlertOptions } from 'sweetalert2' import withReactContent from 'sweetalert2-react-content' export const Notify = withReactContent(SwAl) const notifyOptions: SweetAlertOptions = { icon: 'warning', position: 'top', text: '输入的数字长度不支持超过10位', toast: true, timer: 2000, timerProgressBar: true, showConfirmButton: false, } export const numberLimitListener = (hint?: HintPayload) => { if (typeof hint === 'string') { Notify.fire({ ...notifyOptions, text: hint }) } else if (hint && typeof hint === 'object') { Notify.fire({ ...notifyOptions, ...hint, icon: hint.type, text: hint.message }) } else { Notify.fire({ ...notifyOptions }) } }
在组件中使用 发布/订阅事件 ⇧
在键盘相关组件中注册
@/modules/items/input/Numpad.tsx
useNanoEvent('hint', numberLimitListener)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17// import ... import { useNanoEvent } from '@/hooks/useNanoEvent' import { numberLimitListener } from '@/modules/items/input/numpad/numberLimitListener' // ... export const Numpad: React.FC<Props> = () => { // ... const { numpadButtons } = useNumpadButtons() useNanoEvent('hint', numberLimitListener) return ( <div className={cn(/* ... */)} style={{/* ... */}}> {numpadButtons.map((buttonItem) => (/* ... */))} </div> ) }
在键盘操作状态文件中,添加触发
hint事件逻辑@/stores/useNumpadAmountStore.ts
|
|
参考
- react 中如何使用 event bus
- Event Bus for React
- Stop Prop Drilling! Create a Custom useEventBus Hook in React
- 2023 再谈 React 组件通信
- 发布/订阅js库 Compare mitt v.s. nanoevents v.s. eventemitter3
- nanoevents
- mitt
- eventemitter3
- github nanoevents
- sweetalert2
- sweetalert
添加防抖 @/lib/helpers/throttledFn.ts ⇧
@/lib/helpers/throttledFn.ts
|
|
@/hooks/useGlobalHintsBus.ts
|
|
- 使用
throttledEmit代替emit
添加移动端按钮震动反馈 ⇧
- 在移动端 Web 开发中,实现按钮点击或输入的震动反馈效果,主要依赖于
MDN Web API的Navigator.vibrate()方法。
核心实现代码
|
|
创建
@/hooks/useVibrate.ts
- 提供
isSupported判断客户端是否支持振动Web Api: navigator.vibrate() - 提供
vibrate核心震动方法:触发单次/传入模式震动 - 提供
start实现间隔循环震动 - 提供
stop停止震动 - 提供
triggerMap创建的触发器- 为每一个
rawTriggerMap的key创建一个震动触发器,然后映射到triggerMap中
- 为每一个
组件卸载时自动清理,防止内存泄漏或后台持续震动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86import { useEffect, useRef, useCallback } from 'react' type RawTriggerMap = Record<string, (...args: any[]) => any> export interface UseVibrateOptions { /** * 是否循环震动 * @default false */ isLoop?: boolean rawTriggerMap?: RawTriggerMap } export const useVibrate = ( pattern: VibratePattern = 15, // 默认轻触反馈 15ms options: UseVibrateOptions = {} ) => { const { isLoop = false, rawTriggerMap } = options const intervalRef = useRef<number | null>(null) // 检查当前浏览器是否支持 const isSupported = typeof navigator !== 'undefined' && 'vibrate' in navigator // 1. 核心震动方法 const trigger = useCallback( (customPattern?: VibratePattern) => { if (!isSupported) return navigator.vibrate(customPattern ?? pattern) }, [isSupported, pattern] ) // 2. 停止震动方法 const stop = useCallback(() => { if (!isSupported) return if (intervalRef.current) { clearInterval(intervalRef.current) intervalRef.current = null } if (!navigator.userActivation.hasBeenActive) return navigator?.vibrate?.(0) // 传入 0 即可立即停止当前所有震动 }, [isSupported]) // 3. 启动循环震动 const start = useCallback(() => { if (!isSupported) return stop() // 启动前先清除上一次的定时器 // 如果是数组,计算一个完整的震动周期总耗时 const duration = Array.isArray(pattern) ? pattern.reduce((acc, cur) => acc + cur, 0) : (pattern as number) trigger() // 先执行第一次 if (isLoop) { intervalRef.current = window.setInterval(() => { trigger() }, duration + 50) // 加 50ms 缓冲,避免硬件响应不过来导致连续震动粘连 } }, [isSupported, pattern, isLoop, trigger, stop]) // 组件卸载时自动清理,防止内存泄漏或后台持续震动 useEffect(() => { return () => stop() }, [stop]) // 为每一个rawTriggerMap的key创建一个震动触发器,然后映射到triggerMap中 const triggerMap = rawTriggerMap ? Object.keys(rawTriggerMap).reduce((acc, key) => { acc[key] = (...args) => { trigger() rawTriggerMap[key](...args) } return acc }, {} as RawTriggerMap) : {} return { isSupported, vibrate: trigger, // 触发单次/传入模式震动 start, // 开始循环(如果配置了 isLoop: true) stop, // 停止震动 triggerMap, // 创建的触发器 } }
在
@/modules/items/input/numpad/useNumpadButtons.tsx组件中使用
|
|
- 之后的任意按钮点击事件都可以使用
useVibrate的rawTriggerMap添加振动效果
参考
- react 中如何使用 event bus
- Event Bus for React
- Stop Prop Drilling! Create a Custom useEventBus Hook in React
- 2023 再谈 React 组件通信
- 发布/订阅js库 Compare mitt v.s. nanoevents v.s. eventemitter3
- nanoevents
- mitt
- eventemitter3
- github nanoevents
- sweetalert2
- sweetalert
- MDN Navigator:vibrate() 方法
- MDN 振动 API
- Navigator:userActivation 属性
- UserActivation:hasBeenActive 属性
- 完美复刻 VueUse:自定义封装 useVibrate for react
- vueuse useVibrate
参考文章
- 初始代码:https://github.com/FrankFang/l3xHutO
- 提交历史(6个提交):https://github.com/FrankFang/aLK6px/commits/master
相关文章
- MDN Navigator:vibrate() 方法
- MDN 振动 API
- Navigator:userActivation 属性
- UserActivation:hasBeenActive 属性
- 完美复刻 VueUse:自定义封装 useVibrate for react
- vueuse useVibrate
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名