【山竹记账前端-react】20.数字键盘输入金额和金额显示


大纲链接 §

[toc]


1. 输入金额和金额显示逻辑 ⇧

实现逻辑 ⇧

默认显示 ¥ 0.00

  • 输入有效数字和小数点
    • 初始输入 1-9,显示 1-9
    • 之后输入任意数字,显示对应数字追加到后一位,但不超过最长允许长度
    • 之后输入 .,显示整数部分 + .
      • 之后输入任意数字,小数部分只可输入最多两位数字
      • 之后再输入 .,不往后追加,仍显示原来
    • 初始输入 0,显示 0
    • 之后输入任意数字,去除首位的零,替换为输入的数字
    • 之后输入 .,显示 0.
      • 之后输入任意数字,小数部分只可输入最多两位数字
      • 之后再输入 .,不往后追加,仍显示原来
    • 初始输入 .,显示 0.
    • 之后输入任意数字,小数部分只可输入最多两位数字
    • 之后再输入 .,不往后追加,仍显示原来
  • 点击删除按钮
    • 一次只能删除最后一位数字或 .
    • 删除到只剩最后一位数字,再删除,恢复为初始默认显示 ¥ 0.00
  • 点击归零按钮,都恢复为初始默认显示 ¥ 0.00

使用 zustand 统一管理 useNumpadAmountStore ⇧

@/stores/useNumpadAmountStore.ts

 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
86
87
88
89
90
import { create } from 'zustand'

interface NumpadAmountState {
  amount: string
}
interface NumpadAmountActions {
  appendText: (inputText: string) => void
  backspaceText: () => void
  clearText: () => void
}

export const useNumpadAmountStore = create<NumpadAmountState & NumpadAmountActions>()(
  (set, get) => ({
    amount: '0.00',
    appendText: (inputText: string) => {
      const conditions = [
        {
          ruleHint: `输入时,如果当前金额为'0.00',则清空金额`,
          check: () => get().amount === '0.00',
          action: () => {
            set({ amount: '' })
          },
        },
        {
          ruleHint: `小数点后已经有两位数字,则不能再输入`,
          check: () => /\.\d{2}/.test(get().amount),
          action: () => {
            inputText = ''
          },
        },
        {
          ruleHint: `已经有'.',则不能再输入'.'`,
          check: () => get().amount.includes('.') && inputText === '.',
          action: () => {
            inputText = ''
          },
        },
        {
          ruleHint: `输入'.',判断当前金额是否为空,是则首位补零`,
          check: () => inputText === '.' && get().amount === '',
          action: () => {
            set({ amount: '0' })
          },
        },
        {
          ruleHint: `输入 '0',判断金额是否为'0',是则不输入`,
          check: () => get().amount === '0' && inputText === '0',
          action: () => {
            inputText = ''
          },
        },
        {
          ruleHint: `输入 '1' ~ ' 9',判断当前金额是否为'0',是则替换为 '1' ~ ' 9'`,
          check: () => /[1-9]/g.test(inputText) && get().amount === '0',
          action: () => {
            set({ amount: inputText })
            inputText = ''
          },
        },
        {
          ruleHint: `位数超过10位数字,则不能再输入,并提示`,
          check: () => get().amount.replace('.', '').length >= 10,
          action: () => {
            inputText = ''
            console.warn('输入的数字长度不支持超过10位')
            // TODO: 待使用事件总线解耦通知逻辑
          },
        },
      ]

      conditions?.forEach((condition) => {
        if (condition.check()) {
          condition.action()
        }
      })

      set((state) => ({ amount: state.amount + inputText }))
    },
    backspaceText: () => {
      set((state) => ({ amount: state.amount.slice(0, -1) }))
      // 删空则补零
      if (get().amount === '') {
        set({ amount: '0.00' })
      }
    },
    clearText: () => {
      set({ amount: '0.00' })
    },
  })
)
  • 一个数据状态 amount 读写金额状态
  • 三个逻辑:
    • appendText
    • 表驱动 遍历条件修改:
      • 输入参数 inputText
      • 显示金额 amount
      • 拼接逻辑 set((state) => ({ amount: state.amount + inputText }))
    • backspaceText
    • clearText
  • 重构替换 useState 拦截器写法

使用 useNumpadButtons 抽象按钮数据:包括点击逻辑,额外样式 ⇧

@/modules/items/input/useNumpadButtons.tsx

 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
import { cn } from 'cn'
import { useNumpadAmountStore } from '@/stores/useNumpadAmountStore.ts'

type ButtonItem<T extends boolean = boolean> = {
  id: string
  text: string
  onClick: () => void
  extraClass?: string
} & (T extends true
  ? {
      hasIcon: T
      iconName: string //SvgIconName
    }
  : {
      hasIcon?: false
      iconName?: never
    })

/**
 * @Description: 数字按键列表
 * @Description: 注意使用 cn 函数拼接类名,必须在 TSX 中使用
 */
export const useNumpadButtons = () => {
  const { appendText, clearText, backspaceText } = useNumpadAmountStore()
  const numpadButtons: ButtonItem[] = [
    { id: '1', text: '1', onClick: () => appendText('1') },
    { id: '2', text: '2', onClick: () => appendText('2') },
    { id: '3', text: '3', onClick: () => appendText('3') },
    { id: 'clear', text: '归零' /* '清空' */, onClick: () => clearText() },
    { id: '4', text: '4', onClick: () => appendText('4') },
    { id: '5', text: '5', onClick: () => appendText('5') },
    { id: '6', text: '6', onClick: () => appendText('6') },
    {
      id: 'backspace',
      text: '退格',
      onClick: () => backspaceText(),
      hasIcon: true,
      iconName: 'backspace',
      extraClass: cn('children:(size-1.5em)'),
    },
    { id: '7', text: '7', onClick: () => appendText('7') },
    { id: '8', text: '8', onClick: () => appendText('8') },
    { id: '9', text: '9', onClick: () => appendText('9') },
    {
      id: 'submit',
      text: '提交',
      onClick: () => {
        clearText()
      },
      extraClass: cn('bg-[var(--pad-submit-button-bg-color)] text-[var(--button-text-color)]'),
    },
    { id: '0', text: '0', onClick: () => appendText('0') },
    {
      id: 'dot',
      text: 'dot',
      onClick: () => appendText('.'),
      hasIcon: true,
      iconName: 'decimal_point',
      extraClass: cn('children:(size-2em translate-y-[-9px] font-extrabold)'),
    },
  ]

  return { numpadButtons }
}
  • 引用键盘按钮操作逻辑 const { appendText, clearText, backspaceText } = useNumpadAmountStore()
  • 注意使用 cn 函数拼接类名,必须在 TSX 中使用,才有样式代码提示
  • 可使用 text 或者 icon 作为按钮内容
  • 支持单个按钮添加额外样式 extraClass

Numpad 组件使用 ⇧

@/modules/items/input/Numpad.tsx 集成 useNumpadButtons

 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
import { SvgIcon } from "@/components/SvgIcon";
import { useNumpadButtons } from "@/modules/items/input/useNumpadButtons.tsx";
import { cn } from "cn";
import s from './Numpad.module.scss'

interface Props {
  className?: string
}

export const Numpad: React.FC<Props> = ({ /* className, */ }) => {
  const numberSizeClass = (text: string) =>
  (/[0-9|dot]/g.test(text)
    ? cn('text-18px')
    : '')
  const { numpadButtons } = useNumpadButtons()

  return (
    <div
      className={cn(
        'grid grid-cols-[repeat(4,1fr)] grid-rows-[repeat(4,56px)]',
        'where:(bg-[var(--pad-border-color)]) gap-1px p-t-1px',
      )}
      style={{
        'gridTemplateAreas': `
          'btn_1 btn_2 btn_3 btn_clear'
          'btn_4 btn_5 btn_6 btn_backspace'
          'btn_7 btn_8 btn_9 btn_submit'
          'btn_0 btn_0 btn_dot btn_submit'
          `,
      }}>
      {numpadButtons.map((buttonItem) => (
        <button
          type="button"
          key={buttonItem.text}
          data-btn-text={`btn_${buttonItem.id}`}
          onClick={buttonItem.onClick}
          style={{
            'gridArea': `btn_${buttonItem.id}`,
          }}
          className={cn(
            'flex justify-center items-center font-bold',
            'bg-[var(--pad-button-bg-color)] b-unset',
            buttonItem?.extraClass,
            numberSizeClass(buttonItem.text),
            s['button-item'],
          )}>
          {buttonItem.hasIcon ? <SvgIcon name={buttonItem.iconName} /> : buttonItem.text}
        </button>
      ))}
    </div>
  )
}

添加事件总线,解耦通知 ⇧

安装事件总线和提示ui的库 ⇧

1
pnpm install nanoevents@10.0.0 sweetalert2@11.26.25 sweetalert2-react-content@5.1.2

创建 useGlobalHintsBus.ts ⇧

@/hooks/useGlobalHintsBus.ts

 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
import { createNanoEvents } from 'nanoevents'
import { SweetAlertOptions } from 'sweetalert2'

type HintType = 'warning' | 'error' | 'success' | 'info' | 'question'
type HintPosition =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'center'
  | 'center-start'
  | 'center-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'

export type HintParams = {
  type: HintType
  message: string
  position: HintPosition
} & SweetAlertOptions

export type HintPayload = undefined | string | HintParams

export interface GlobalHintsEvents {
  hint: (payload?: HintPayload) => void
  // noPayloadEventName: () => void
}

export const emitter = createNanoEvents<GlobalHintsEvents>()

/**
 * 创建总线单例
 * 导出两个接口,可直接解构获取
 * 1. on: 监听总线事件,返回一个取消监听函数
 * 2. emit: 触发总线事件
 */
export const useGlobalHintsBus = () => ({
  ...(emitter ? emitter : createNanoEvents<GlobalHintsEvents>()),
  on: emitter.on.bind(emitter),
  emit: emitter.emit.bind(emitter),
})
  • 自定义提示事件类型写在 GlobalHintsEvents 中
  • 配合使用 sweetalert2 提示框,payload 传参类型参考 SweetAlertOptions

封装组件内通用事件钩子 useNanoEvent.tsx ⇧

@/hooks/useNanoEvent.tsx

  • 作为组件生命周期事件订阅逻辑钩子,负责挂载时订阅事件,和卸载时移除事件监听
  • 接受 event 和 handler

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    import { 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
    25
    
    import 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

 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
import { useGlobalHintsBus } from '@/hooks/useGlobalHintsBus'
import { create } from 'zustand'

interface NumpadAmountState {
  amount: string
}
interface NumpadAmountActions {
  appendText: (inputText: string) => void
  backspaceText: () => void
  clearText: () => void
}

const { emit } = useGlobalHintsBus()

export const useNumpadAmountStore = create<NumpadAmountState & NumpadAmountActions>()(
  (set, get) => ({
    amount: '0.00',
    appendText: (inputText: string) => {
      const conditions = [
        {
          ruleHint: `输入时,如果当前金额为'0.00',则清空金额`,
          /* ... */
        },
        {
          ruleHint: `小数点后已经有两位数字,则不能再输入`,
          /* ... */
        },
        {
          ruleHint: `已经有'.',则不能再输入'.'`,
          /* ... */
        },
        {
          ruleHint: `输入'.',判断当前金额是否为空,是则首位补零`,
          /* ... */
        },
        {
          ruleHint: `输入 '0',判断金额是否为'0',是则不输入`,
          /* ... */
        },
        {
          ruleHint: `输入 '1' ~ ' 9',判断当前金额是否为'0',是则替换为 '1' ~ ' 9'`,
          /* ... */
        },
        {
          ruleHint: `位数超过10位数字,则不能再输入,并提示`,
          check: () => get().amount.replace('.', '').length >= 10,
          action: () => {
            inputText = ''
            emit('hint')
          },
        },
      ]

      conditions?.forEach((condition) => {
        if (condition.check()) {
          condition.action()
        }
      })

      set((state) => ({ amount: state.amount + inputText }))
    },
    backspaceText: () => {/* ... */},
    clearText: () => {/* ... */},
  })
)

参考


添加防抖 @/lib/helpers/throttledFn.ts ⇧

@/lib/helpers/throttledFn.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export const throttledFn = <F extends (...args: any[]) => unknown>(fn: F, delay: number = 500) => {
  let timer: NodeJS.Timeout | null = null
  let result: ReturnType<F>
  return (...args: Parameters<F>) => {
    if (timer) {
      return result
    }
    result = fn(...args) as ReturnType<F>
    timer = setTimeout(() => {
      clearTimeout(timer as NodeJS.Timeout)
      timer = null
    }, delay)
    return result
  }
}

@/hooks/useGlobalHintsBus.ts

 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
import { throttledFn } from '@/lib/helpers/throttledFn'
import { createNanoEvents } from 'nanoevents'
import { SweetAlertOptions } from 'sweetalert2'

type HintType = 'warning' | 'error' | 'success' | 'info' | 'question'
type HintPosition =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'center'
  | 'center-start'
  | 'center-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'

export type HintParams = {
  type: HintType
  message: string
  position: HintPosition
} & SweetAlertOptions

export type HintPayload = undefined | string | HintParams

export interface GlobalHintsEvents {
  hint: (payload?: HintPayload) => void
  // noPayloadEventName: () => void
}

export const emitter = createNanoEvents<GlobalHintsEvents>()

export const useGlobalHintsBus = () => ({
  ...(emitter ? emitter : createNanoEvents<GlobalHintsEvents>()),
  on: emitter.on.bind(emitter),
  emit: emitter.emit.bind(emitter),
  throttledEmit: throttledFn(emitter.emit.bind(emitter), 1800),
})
  • 使用 throttledEmit 代替 emit

添加移动端按钮震动反馈 ⇧

  • 在移动端 Web 开发中,实现按钮点击或输入的震动反馈效果,主要依赖于 MDN Web API 的 Navigator.vibrate() 方法。

核心实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 封装一个轻量级的按钮轻触震动函数
function triggerHapticFeedback(ms = 15) {
    if ('vibrate' in navigator) {
        // 参数为震动时长(毫秒)
        // 按钮点击通常使用 15ms-20ms 即可,太长会显得沉重、黏糊
        navigator.vibrate(ms)
    }
}

// 绑定到按钮的 click 或 touchstart 事件上
const btn = document.querySelector('.vibrate-btn')
btn.addEventListener('click', () => {
    triggerHapticFeedback(15)
})

创建 @/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
    86
    
    import { 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 组件中使用

 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
import { cn } from 'cn'
import { useNumpadAmountStore } from '@/stores/useNumpadAmountStore.ts'
import { useVibrate } from '@/hooks/useVibrate.ts'

type ButtonItem<T extends boolean = boolean> = {
  id: string
  text: string
  onClick: () => void
  extraClass?: string
} & (T extends true
  ? {
      hasIcon: T
      iconName: string
    }
  : {
      hasIcon?: false
      iconName?: never
    })

export const useNumpadButtons = () => {
  const {
    appendText: _appendText,
    clearText: _clearText,
    backspaceText: _backspaceText,
  } = useNumpadAmountStore()
  const {
    triggerMap: { _appendText: appendText, _clearText: clearText, _backspaceText: backspaceText },
  } = useVibrate(15, {
    rawTriggerMap: {
      _appendText,
      _clearText,
      _backspaceText,
    },
  })

  const numpadButtons: ButtonItem[] = [
    { id: '1', text: '1', onClick: () => appendText('1') },
    { id: '2', text: '2', onClick: () => appendText('2') },
    { id: '3', text: '3', onClick: () => appendText('3') },
    { id: 'clear', text: '归零' /* '清空' */, onClick: () => clearText() },
    { id: '4', text: '4', onClick: () => appendText('4') },
    { id: '5', text: '5', onClick: () => appendText('5') },
    { id: '6', text: '6', onClick: () => appendText('6') },
    {
      id: 'backspace',
      text: '退格',
      onClick: () => backspaceText(),
      hasIcon: true,
      iconName: 'backspace',
      extraClass: cn('children:(size-1.5em)'),
    },
    { id: '7', text: '7', onClick: () => appendText('7') },
    { id: '8', text: '8', onClick: () => appendText('8') },
    { id: '9', text: '9', onClick: () => appendText('9') },
    {
      id: 'submit',
      text: '提交',
      onClick: () => {
        clearText()
      },
      extraClass: cn('bg-[var(--pad-submit-button-bg-color)] text-[var(--button-text-color)]'),
    },
    { id: '0', text: '0', onClick: () => appendText('0') },
    {
      id: 'dot',
      text: 'dot',
      onClick: () => appendText('.'),
      hasIcon: true,
      iconName: 'decimal_point',
      extraClass: cn('children:(size-2em translate-y-[-9px] font-extrabold)'),
    },
  ]

  return { numpadButtons }
}
  • 之后的任意按钮点击事件都可以使用 useVibrate 的 rawTriggerMap 添加振动效果

参考


·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名