# number-input
**Repository Path**: hejsky/number-input
## Basic Information
- **Project Name**: number-input
- **Description**: 数字金额处理
- **Primary Language**: JavaScript
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2026-06-11
- **Last Updated**: 2026-06-12
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# number-input
一个零依赖、即插即用的金额/数字输入器插件。原生 JavaScript 编写,支持千分位分隔、大写金额转换、位数单位显示、数值范围限制、前后缀包裹、只读模式与多种回调事件。同时提供 5 个静态工具方法,无需创建实例即可独立调用。
建议配合**Decimal.js**:JavaScript中的任意精度计算库
#### 特性一览
- **千分位格式化**:聚焦时空格分隔便于编辑,失焦后按 `thousandSymbol`(默认 `,`)格式化显示
- **大写金额转换**:实时将数字转为中文大写金额(壹贰叁肆 / 拾佰仟万亿…)
- **位数单位显示**:根据整数位数自动显示 千 / 万 / 亿 / 兆 / 京 / 垓 / 秭 / 穰 / 沟 / 涧 / 正 / 载
- **小数/整数控制**:`maxDecimal` 限制最多小数位;`forceDecimal` 失焦自动补 0;`maxInteger` 限制整数部分最长位数
- **数值范围校验**:`min` / `max` 失焦自动钳制,越界时通过 `onValidChange` 反馈
- **前后缀包裹**:`prefix` / `suffix` 非空时自动包裹为带边框的容器,点击前后缀自动聚焦输入框
- **负数支持**:`allowNegative` 控制是否允许输入负数
- **只读模式**:`readOnly` 选项 + 实例方法 `readOnly()` / `enable()` / `isReadOnly()`,采用 `readOnly` 而非 `disabled`,值仍可读取和提交
- **完善回调**:`onChange` / `onBlur` / `onFocus` / `onValidChange` 四种事件,均带防重复触发保护
- **静态工具方法**:5 个独立方法无需创建实例即可调用,适用于纯数据处理场景
- **零依赖**:不依赖任何第三方库,单文件 `index.js` 即可使用,支持 CommonJS 与浏览器全局变量
#### 软件架构
```
number-input/
├── index.html # 14 种使用场景的可运行 Demo 页面
├── index.js # 插件主体(createNumberInput 工厂函数 + 静态工具方法)
└── README.md # 使用文档
```
- **入口函数**:`createNumberInput(inputEl, options)`
- **静态方法**:`createNumberInput.format()` / `.clean()` / `.getValue()` / `.getNumberUnit()` / `.toChineseCurrency()`
- **导出方式**:浏览器 `window.createNumberInput` + Node `module.exports`
#### 安装教程
1. 直接克隆/下载本仓库到本地
2. 在 HTML 中引入 `index.js`(无构建步骤,开箱即用)
3. 打开 `index.html` 即可查看 14 种使用场景的 Demo
```html
```
Node / CommonJS 环境:
```js
const createNumberInput = require('./index.js');
```
#### 使用说明
##### API 总览
```js
// 创建实例(绑定到 input 元素)
const inst = createNumberInput(inputEl, options);
// 静态工具方法(无需创建实例,独立调用)
createNumberInput.format(val, separator); // 千分位格式化
createNumberInput.clean(val); // 清理千分位等非数字字符
createNumberInput.getValue(val, forceDecimal); // 应用强制小数位数
createNumberInput.getNumberUnit(val); // 获取位数单位
createNumberInput.toChineseCurrency(val); // 转换为大写金额
```
##### 配置项 `options`
| 参数 | 类型 | 默认值 | 说明 |
| --------------------- | --------------------- | ------- | --------------------------------------------------------------------------- |
| `maxDecimal` | `number` | `0` | 最大小数位数。`0` 表示整数 |
| `forceDecimal` | `number` | `0` | 强制保留几位小数。`0` 表示不强制;`>0` 时失焦 / `getValue` / `getFormattedValue` 会补 0 到指定位数 |
| `maxInteger` | `number` | `0` | 整数部分最长位数。`0` 表示不限制;`>0` 时输入超过会自动截断 |
| `min` | `number \| null` | `null` | 数值最小值。`null` 表示无限制;失焦时超出范围自动钳制 |
| `max` | `number \| null` | `null` | 数值最大值。`null` 表示无限制;失焦时超出范围自动钳制 |
| `prefix` | `string` | `''` | 前缀文本(如 `"¥"`)。非空时自动包裹输入框为容器 |
| `suffix` | `string` | `''` | 后缀文本(如 `"元"`)。非空时自动包裹输入框为容器 |
| `allowNegative` | `boolean` | `false` | 是否允许负数 |
| `thousandSeparator` | `boolean` | `false` | 是否启用千分位分隔符 |
| `thousandSymbol` | `string` | `','` | 千分位分隔符号(失焦后使用) |
| `showChineseCurrency` | `boolean` | `false` | 是否显示大写金额 |
| `showNumberUnit` | `boolean` | `false` | 是否显示位数单位 |
| `chineseOutputEl` | `HTMLElement \| null` | `null` | 大写金额输出容器 |
| `unitOutputEl` | `HTMLElement \| null` | `null` | 位数单位输出容器 |
| `readOnly` | `boolean` | `false` | 初始是否只读 |
| `onChange` | `Function \| null` | `null` | 值变化回调 `(value: string) => void` |
| `onBlur` | `Function \| null` | `null` | 失焦回调 `(value: string) => void` |
| `onFocus` | `Function \| null` | `null` | 聚焦回调 `(value: string) => void` |
| `onValidChange` | `Function \| null` | `null` | 校验状态变化回调 `(isValid: boolean, error: string \| null, value: string) => void` |
##### 实例方法
| 方法 | 说明 |
| --------------------- | --------------------------------------- |
| `getValue()` | 获取原始数值(不含千分位分隔符,已应用 `forceDecimal`) |
| `setValue(val)` | 设置输入值(自动应用 `forceDecimal` + 范围钳制 + 千分位) |
| `getFormattedValue()` | 获取格式化后的值(含千分位,已应用 `forceDecimal`) |
| `readOnly()` | 设为只读 |
| `enable()` | 设为可编辑 |
| `isReadOnly()` | 返回当前是否已只读 |
| `getWrapperEl()` | 获取 `prefix` / `suffix` 包裹容器(启用时存在) |
| `destroy()` | 销毁实例,移除所有事件监听与定时器,拆解包裹容器 |
##### 静态工具方法
以下方法无需创建实例即可独立调用,适用于纯数据处理场景(如后端数据格式化、表单提交前清理等)。
| 静态方法 | 参数 | 返回值 | 说明 |
| ----------------------------------------------- | ------------------------------------------------------------------ | -------------------- | --------------------------------------------- |
| `createNumberInput.format(val, separator)` | `val`: `string\|number` 要格式化的值`separator`: `string` 千分位符号,默认 `','` | `string` 千分位格式化后的字符串 | 将纯数字格式化为千分位显示。**注意**:传入带千分位的值需先 `.clean()` 清理 |
| `createNumberInput.clean(val)` | `val`: `string\|number` 要清理的值 | `string` 纯数字字符串 | 移除千分位分隔符、货币符号等非数字字符,仅保留数字、小数点、负号 |
| `createNumberInput.getValue(val, forceDecimal)` | `val`: `string\|number` 纯数字值`forceDecimal`: `number` 强制小数位数,默认 `0` | `string` 处理后的数字字符串 | 将小数部分补 0 或截断到指定位数。**注意**:`val` 应为纯数字(不含千分位) |
| `createNumberInput.getNumberUnit(val)` | `val`: `string\|number` 数字值 | `string` 中文位数单位 | 根据整数位数返回单位(千/万/亿/兆…载),支持自动清理千分位 |
| `createNumberInput.toChineseCurrency(val)` | `val`: `string\|number` 数字值 | `string` 中文大写金额 | 转换为财务大写金额格式,支持自动清理千分位,负数前缀"负" |
**静态方法使用示例**:
```js
// 1. 千分位格式化
createNumberInput.format('1234567.89'); // '1,234,567.89'
createNumberInput.format('1234567.89', '_'); // '1_234_567.89'
createNumberInput.format('-9876543.21'); // '-9,876,543.21'
// 2. 清理千分位等非数字字符
createNumberInput.clean('1,234,567.89'); // '1234567.89'
createNumberInput.clean('¥1,234.56元'); // '1234.56'
createNumberInput.clean('-1,234.56'); // '-1234.56'
// 3. 应用强制小数位数(补0/截断)
createNumberInput.getValue('1', 2); // '1.00'
createNumberInput.getValue('1.5', 2); // '1.50'
createNumberInput.getValue('1.234', 2); // '1.23'
createNumberInput.getValue('-1', 2); // '-1.00'
createNumberInput.getValue('1.5', 0); // '1.5'(不强制)
// 4. 获取位数单位
createNumberInput.getNumberUnit('1234'); // '千'
createNumberInput.getNumberUnit('10000'); // '万'
createNumberInput.getNumberUnit('100000000'); // '亿'
createNumberInput.getNumberUnit('1,234,567'); // '万'(自动清理千分位)
// 5. 转换为大写金额
createNumberInput.toChineseCurrency('1234.56'); // '壹仟贰佰叁拾肆元伍角陆分'
createNumberInput.toChineseCurrency('-100'); // '负壹佰元整'
createNumberInput.toChineseCurrency('0'); // '零元整'
createNumberInput.toChineseCurrency('1,234,567'); // '壹佰贰拾叁万肆仟伍佰陆拾柒元整'
```
**典型组合使用场景**:
```js
// 场景1:从后端获取带千分位的数据 → 清理 → 计算 → 格式化显示
const serverData = '1,234,567.89';
// 步骤1:清理千分位,获取纯数字
const cleanVal = createNumberInput.clean(serverData); // '1234567.89'
// 步骤2:强制2位小数
const stdVal = createNumberInput.getValue(cleanVal, 2); // '1234567.89'
// 步骤3:数值计算
const result = Number(stdVal) * 1.08; // 1333333.3212
// 步骤4:格式化显示
const display = createNumberInput.format(String(result)); // '1,333,333.32'
// 步骤5:显示大写金额
const chinese = createNumberInput.toChineseCurrency(String(result)); // 大写金额
```
```js
// 场景2:带货币符号和千分位的复杂组合(合同/发票场景)
// 模拟从页面获取的显示值,包含货币符号、千分位、单位
const displayValue = '¥1,234,567.89元';
// 步骤1:清理所有非数字字符(货币符号、千分位逗号、单位)
const pureNum = createNumberInput.clean(displayValue); // '1234567.89'
// 步骤2:强制2位小数(确保金额格式规范)
const stdNum = createNumberInput.getValue(pureNum, 2); // '1234567.89'
// 步骤3:数值计算(如加收8%税费)
const taxAmount = Number(stdNum) * 0.08; // 98765.4312
const totalAmount = Number(stdNum) + taxAmount; // 1333333.3212
// 步骤4:计算结果标准化
const totalStd = createNumberInput.getValue(String(totalAmount), 2); // '1333333.32'
// 步骤5:格式化为千分位显示
const totalDisplay = createNumberInput.format(totalStd); // '1,333,333.32'
// 步骤6:拼接货币符号和单位
const finalDisplay = `¥${totalDisplay}元`; // '¥1,333,333.32元'
// 步骤7:获取位数单位
const unit = createNumberInput.getNumberUnit(totalStd); // '万'
// 步骤8:转换大写金额(合同/发票用)
const chinese = createNumberInput.toChineseCurrency(totalStd);
// '壹佰叁拾叁万叁仟叁佰叁拾叁元叁角贰分'
// 最终输出示例:
console.log(`金额:${finalDisplay}`); // 金额:¥1,333,333.32元
console.log(`量级:${unit}`); // 量级:万
console.log(`大写:${chinese}`); // 大写:壹佰叁拾叁万叁仟叁佰叁拾叁元叁角贰分
```
##### 完整示例
```js
const moneyInput = createNumberInput(document.getElementById('moneyInput'), {
maxDecimal: 4, // 最多 4 位小数
forceDecimal: 2, // 失焦自动补 0 到 2 位
maxInteger: 6, // 整数部分最多 6 位
min: 0, // 最小 0
max: 999999, // 最大 999999
prefix: '¥', // 前缀
suffix: '元', // 后缀
allowNegative: true, // 允许负数
thousandSeparator: true,
thousandSymbol: ',',
showChineseCurrency: true,
showNumberUnit: true,
chineseOutputEl: document.getElementById('chineseOutput'),
unitOutputEl: document.getElementById('unitOutput'),
onChange: (val) => console.log('值变化:', val),
onBlur: (val) => console.log('失焦:', val),
onFocus: (val) => console.log('聚焦:', val),
onValidChange: (ok, err, val) => console.log('校验:', ok, err, val)
});
// 程序化取值 / 设值
moneyInput.setValue('12345.6'); // 自动补 0 → '12345.60'
console.log(moneyInput.getValue()); // '12345.60'
console.log(moneyInput.getFormattedValue()); // '12,345.60'
// 切换只读
moneyInput.readOnly();
moneyInput.enable();
console.log(moneyInput.isReadOnly()); // false
// 销毁
// moneyInput.destroy();
```
##### 行为约定
- **聚焦时**:千分位由 `thousandSymbol` 替换为空格,便于编辑
- **失焦时**:空格千分位替换为 `thousandSymbol`,应用 `forceDecimal`,按 `min` / `max` 钳制
- **输入中**:实时清理非法字符、限制小数/整数位数、智能保留光标位置
- **输入法兼容**:使用 `compositionstart` / `compositionend` 兼容中文输入法
- **大写金额**:支持正负数,最大支持到「载」位(10^44)
- **位数单位**:1-4 位 → 千以下;5-8 位 → 万级;9-12 位 → 亿级;依次类推至「载」级
- **静态方法**:定义在 `createNumberInput` 外部,不依赖任何闭包变量,可独立调用
#### Demo 场景说明
`index.html` 内置 14 种典型使用场景:
1. 完整功能(千分位 + 大写 + 单位 + 负数)
2. 仅千分位分隔符(不允许负数)
3. 千分位符号为下划线 `_`
4. 仅位数单位(无千分位,不允许负数)
5. 大写 + 单位 + 负数(无千分位)
6. 千分位 + 位数单位 + 负数
7. 强制保留 4 位小数(失焦自动补 0)
8. 不强制小数位数(输入什么显示什么)
9. 整数部分最长 6 位(超过自动截断)
10. `prefix` / `suffix` 前后缀(自动包裹为容器)
11. `min` / `max` 范围限制(失焦自动钳制 + 校验提示)
12. 回调事件演示(`onChange` / `onBlur` / `onValidChange`)
13. 只读状态演示(`readOnly` + `readOnly()` / `enable()` 方法)
14. 静态工具方法演示(`format` / `clean` / `getValue` / `getNumberUnit` / `toChineseCurrency`)
#### 参与贡献
1. Fork 本仓库
2. 新建 `Feat_xxx` 分支
3. 提交代码
4. 新建 Pull Request