You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React自定义货币输入框无法在光标位置编辑替换字符问题

React自定义货币输入光标错位、无法定点编辑问题修复

核心问题

当前出现的两个异常本质是三个逻辑漏洞:

  • 输入逻辑完全忽略光标位置:输入数字时直接对原值做算术运算拼接,相当于永远把新数字加到值的末尾,完全不响应用户移动光标的操作
  • 受控组件重渲染默认重置光标:React控制的input每次value更新触发重渲染时,光标会默认跳到内容末尾,现有代码没有主动把光标设回操作位置
  • 删除逻辑索引计算错误,未覆盖多内容选中场景:退格、删除键的位置偏移计算有误,用户选中一段内容删除/替换时逻辑直接失效

修复思路

  • 新增ref存储每次操作后的光标位置,值更新完成后主动把input光标设置到对应位置,避免自动跳转到末尾
  • 废弃原有的算术运算拼接值逻辑,改为基于字符串切片的方式,根据当前光标/选区位置插入、删除字符
  • 补全选中多段内容时批量删除、输入替换的场景处理

修复后完整组件代码

import React, { useCallback, useRef, useEffect, useState } from "react";
import _ from "lodash";
import { Form } from "react-bootstrap";

export const CurrencyInput = ({ value, setValue, controlId, label, max, min, required, message, setValidity,defaultValue }) => {
    const [isValid, setValid] = useState(true);
    const amountInvalidMessage = "amount-invalid";
    const amountRequiredMessage = "amount-required";
    const [invalidMessage, setInvalidMessage] = useState(amountInvalidMessage);    
    const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", currencyDisplay: "narrowSymbol"})
    const currencyInput = useRef(null)
    // 存储操作后的光标偏移位置
    const cursorPos = useRef(0);

    const validate = useCallback(_.debounce((amount) => {
        let valid = true;
        
        if (amount !== null && amount !== "") {
            amount= amount.replace("$","")
            let amountNumber = parseFloat(amount);
            if (isNaN(amountNumber)) {
                valid = false;
            } else if (min > amountNumber) {
                valid = false;
            } else if (max < amountNumber) {
                valid = false;
            }

            if(!valid) {
                setValue("");
                currencyInput.current.focus()                
                setInvalidMessage(amountInvalidMessage);
            }
        } else if (required) {
            valid = false;
            setValue("");
            currencyInput.current.focus()
            setInvalidMessage(amountRequiredMessage);
        }
        setValid(valid);
        setValidity(valid);
    }, 500), [min, max]);
    
    // 值更新渲染完成后复位光标位置
    useEffect(() => {
        if (currencyInput.current) {
            currencyInput.current.selectionStart = cursorPos.current;
            currencyInput.current.selectionEnd = cursorPos.current;
        }
    }, [value])

    const onBlur = (e) => {
        if(value !== null && value !== "") {
            let formattedValue = parseFloat(value).toFixed(2)
            setValue(formattedValue);
        }
        validate(value);
    }

    const onKeyDown = (e) => {
        const input = e.target;
        const start = input.selectionStart;
        const end = input.selectionEnd;
        const currentVal = value || "";

        // 处理退格、删除键
        if (e.keyCode === 8 || e.keyCode === 46) {
            e.preventDefault();
            // 选中多内容场景:直接删除选中区间
            if (start !== end) {
                const newVal = currentVal.slice(0, start) + currentVal.slice(end);
                setValue(newVal === "0.00" ? "" : newVal);
                cursorPos.current = start;
                return;
            }
            // 单光标场景:退格删光标前字符,删除键删光标后字符
            const deletePos = e.keyCode === 8 ? start - 1 : start;
            if (deletePos < 0 || deletePos >= currentVal.length) return;
            const newVal = currentVal.slice(0, deletePos) + currentVal.slice(deletePos + 1);
            setValue(newVal === "0.00" ? "" : newVal);
            cursorPos.current = e.keyCode === 8 ? start - 1 : start;
            return;
        }

        // 处理数字输入(支持主键盘、小键盘数字区)
        let inputNum = null;
        if (e.keyCode >= 48 && e.keyCode <= 57) inputNum = e.keyCode - 48;
        else if (e.keyCode >= 96 && e.keyCode <= 105) inputNum = e.keyCode - 96;
        else return;

        e.preventDefault();
        let newVal;
        // 选中多内容场景:用新输入数字替换选中内容
        if (start !== end) {
            newVal = currentVal.slice(0, start) + inputNum + currentVal.slice(end);
            cursorPos.current = start + 1;
        } else {
            // 单光标场景:在光标位置插入数字
            newVal = currentVal.slice(0, start) + inputNum + currentVal.slice(start);
            cursorPos.current = start + 1;
        }
        // 统一格式化为两位小数
        const numVal = parseFloat(newVal);
        if (!isNaN(numVal)) {
            newVal = numVal.toFixed(2).toString();
        }
        setValue(newVal === "0.00" ? "" : newVal);
    };

    return <Form.Group {...{ controlId }}>
        <Form.Control 
            type="text"
            isInvalid={!isValid}                       
            onBlur={onBlur}
            ref ={currencyInput}
            onKeyDown={onKeyDown}                    
            placeholder= {formatter.format(defaultValue)}  
            value={value}
            pattern="/\D/g">
        </Form.Control>
        {!isValid ? <Form.Control.Feedback className={'classes.feedbackInvalid'} type="invalid">{invalidMessage}</Form.Control.Feedback> : null}
    </Form.Group>
}

效果验证

修复后可实现:

  • 在任意光标位置插入数字,新字符不会强制追加到内容末尾
  • 删除任意位置字符后光标停留在操作位,不会自动跳转到内容末尾
  • 支持选中一段内容后直接输入数字替换、按删除键批量清除

内容的提问来源于stack exchange,提问作者Vishnu Vedagna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 17:54:26