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

基于自定义货币配置的Flutter TextFormField数字格式化(无需Locale)

问题:基于自定义货币配置实现TextFormField数字格式化

我有一份包含各类货币配置的currencies列表(JSON格式):

[
  {
    "code": "USD",
    "name": "United States Dollar",
    "symbol": "$",
    "flag": "USD",
    "decimal_digits": 2,
    "number": 840,
    "name_plural": "US dollars",
    "thousands_separator": ",",
    "decimal_separator": ".",
    "space_between_amount_and_symbol": false,
    "symbol_on_left": true
  },
  {
    "code": "EUR",
    "name": "Euro",
    "symbol": "€",
    "flag": "EUR",
    "decimal_digits": 2,
    "number": 978,
    "name_plural": "Euros",
    "thousands_separator": ".",
    "decimal_separator": ",",
    "space_between_amount_and_symbol": true,
    "symbol_on_left": false
  },
  ...
]

目前我已经实现了将double类型转换为格式化价格的函数,但核心需求是:基于指定currencyCode实现TextFormField的实时文本格式化,不显示货币符号,仅展示符合对应货币规则的数字。

试过多种方案及第三方包,但大多依赖locale或无法适配所有场景。请问现有货币配置信息是否足够?如何实现该需求?

示例(USD):

  • 输入:400 → 预期输入框文本:4,00
  • 输入:696942 → 预期输入框文本:6,969.42

解答

现有配置是否足够?

完全足够。实现需求只需要用到配置里的三个核心字段:

  • decimal_digits:小数位数,控制输入的小数部分长度
  • thousands_separator:千位分隔符,用于整数部分的分组
  • decimal_separator:小数点分隔符,区分整数和小数部分
    其他字段(如symbol、name等)暂时用不上。

实现步骤(Flutter/Dart场景)

1. 定义货币配置模型类

先把JSON配置转成强类型模型,方便后续操作:

class CurrencyConfig {
  final String code;
  final int decimalDigits;
  final String thousandsSeparator;
  final String decimalSeparator;

  CurrencyConfig({
    required this.code,
    required this.decimalDigits,
    required this.thousandsSeparator,
    required this.decimalSeparator,
  });

  factory CurrencyConfig.fromJson(Map<String, dynamic> json) {
    return CurrencyConfig(
      code: json['code'],
      decimalDigits: json['decimal_digits'],
      thousandsSeparator: json['thousands_separator'],
      decimalSeparator: json['decimal_separator'],
    );
  }
}

2. 实现货币配置查询函数

根据指定的currencyCode从配置列表中获取对应的规则:

CurrencyConfig? getCurrencyConfig(String currencyCode, List<Map<String, dynamic>> currencies) {
  try {
    final currencyJson = currencies.firstWhere((c) => c['code'] == currencyCode);
    return CurrencyConfig.fromJson(currencyJson);
  } catch (_) {
    return null;
  }
}

3. 自定义TextInputFormatter实现实时格式化

核心是编写自定义输入格式化器,处理用户输入的实时转换:

class CurrencyNumberFormatter extends TextInputFormatter {
  final CurrencyConfig config;

  CurrencyNumberFormatter(this.config);

  @override
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
    // 过滤掉所有非数字字符
    final cleanText = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
    
    // 拆分整数和小数部分
    String integerPart = cleanText;
    String decimalPart = '';
    if (config.decimalDigits > 0) {
      if (cleanText.length > config.decimalDigits) {
        integerPart = cleanText.substring(0, cleanText.length - config.decimalDigits);
        decimalPart = cleanText.substring(cleanText.length - config.decimalDigits);
      } else {
        // 输入长度不足小数位数时,补0并将整数部分设为0
        decimalPart = cleanText.padLeft(config.decimalDigits, '0');
        integerPart = '0';
      }
    }

    // 给整数部分添加千位分隔符
    final formattedInteger = _addThousandsSeparator(integerPart, config.thousandsSeparator);

    // 拼接最终格式化文本
    String formattedText = formattedInteger;
    if (config.decimalDigits > 0 && decimalPart.isNotEmpty) {
      formattedText += '${config.decimalSeparator}$decimalPart';
    }

    // 计算光标位置,保证输入时光标不跳转
    int cursorPosition = formattedText.length;
    if (newValue.selection.end > 0) {
      final originalCursorIndex = newValue.selection.end;
      final cleanCursorIndex = cleanText.substring(0, originalCursorIndex).length;
      cursorPosition = _calculateCursorPosition(formattedText, cleanCursorIndex, config);
    }

    return TextEditingValue(
      text: formattedText,
      selection: TextSelection.collapsed(offset: cursorPosition),
    );
  }

  // 给数字添加千位分隔符
  String _addThousandsSeparator(String number, String separator) {
    if (number.isEmpty) return '';
    final reversed = number.split('').reversed.join();
    final parts = RegExp(r'(.{1,3})').allMatches(reversed).map((m) => m.group(0)).join(separator);
    return parts.split('').reversed.join();
  }

  // 计算格式化后的光标位置
  int _calculateCursorPosition(String formattedText, int cleanCursorIndex, CurrencyConfig config) {
    String integerPart = formattedText.split(config.decimalSeparator).first;
    // 统计千位分隔符的数量
    int separatorCount = (integerPart.length - 1) ~/ 3;
    int cursorPos = cleanCursorIndex + separatorCount;
    // 如果光标在小数部分,加上小数点的位置
    if (cleanCursorIndex > integerPart.replaceAll(config.thousandsSeparator, '').length) {
      cursorPos += 1;
    }
    return cursorPos.clamp(0, formattedText.length);
  }
}

4. 在TextFormField中使用

将自定义格式化器绑定到输入框:

// 假设已有currencies配置列表和选中的currencyCode
final currencyConfig = getCurrencyConfig('USD', currencies);

TextFormField(
  inputFormatters: currencyConfig != null ? [CurrencyNumberFormatter(currencyConfig)] : [],
  keyboardType: TextInputType.number,
  decoration: const InputDecoration(
    labelText: '金额',
  ),
)

测试验证

  • 输入400时,会自动格式化为4,00(符合USD规则)
  • 输入696942时,格式化为6,969.42
  • 切换为EUR时,输入696942会格式化为6.969,42,符合欧元的数字规则

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 10:14:51