# InputNumber 数字输入框

受控的数值录入控件。所有写回都经过同一套 `parse → clamp → precision → commit` 流程，因此外部拿到的 `modelValue` 永远是「合法且在区间内」的值，业务侧不必再做一次校验与取整。

## 代码演示

### 基础用法

`v-model` 绑定 `number | null`，默认步进 1，`min` / `max` 会把越界值收敛到边界。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const quantity = ref<number | null>(1)
</script>

<template>
  <div class="m-row-demo">
    <MInputNumber v-model="quantity" :min="1" :max="10" clearable />
  </div>
</template>
```

### 控制器布局

`controls="right"` 在右侧并排上下箭头，`controls="both"` 在左右两侧放减号与加号。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const rightControls = ref<number | null>(3)
const bothControls = ref<number | null>(3)
</script>

<template>
  <div class="m-row-demo">
    <MInputNumber v-model="rightControls" controls="right" />
    <MInputNumber v-model="bothControls" controls="both" />
  </div>
</template>
```

### 上下限

到达边界后对应的步进按钮会自动禁用，手动输入越界数字也会被收敛。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const percent = ref<number | null>(50)
</script>

<template>
  <div class="m-row-demo">
    <MInputNumber v-model="percent" :min="0" :max="100" :step="5" controls="both" />
  </div>
</template>
```

### 精度与步长

`precision` 在小数录入、金额计算这类场景下避免浮点尾数；`step` 支持小数。

**演示说明：** 第一个输入框把 `change` 抛出的值弹成 toast，可以直观看到精度归一化后的结果。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { toast } from 'miao-design'

const price = ref<number | null>(9.9)

function onChange(value: number | null) {
  toast.info(`外部收到的值：${value}`)
}
</script>

<template>
  <div class="m-row-demo--col">
    <MInputNumber
      v-model="price"
      :precision="2"
      :step="0.5"
      :min="0"
      controls="both"
      width="200px"
      @change="onChange" />
    <MInputNumber v-model="price" :precision="2" :step="0.5" :min="0" width="200px" />
  </div>
</template>
```

### 尺寸

与 `MInput` 同体系，高度取全局 `--m-size-*` 令牌。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const small = ref<number | null>(1)
const normal = ref<number | null>(2)
const large = ref<number | null>(3)
</script>

<template>
  <div class="m-row-demo--col">
    <MInputNumber v-model="small" size="small" :min="0" />
    <MInputNumber v-model="normal" size="default" :min="0" />
    <MInputNumber v-model="large" size="large" :min="0" />
  </div>
</template>
```

### 清空、只读与禁用

`clearable` 清空后把值置为 `null` 并重新聚焦；只读与禁用都会同时停用步进按钮。

**演示说明：** 非数字输入会在失焦时被解析成 `null`。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { toast } from 'miao-design'

const editable = ref<number | null>(12)
const readonlyValue = ref<number | null>(256)

function onChange(value: number | null) {
  toast.info(value === null ? '外部收到 null（清空或非数字输入）' : `外部收到 ${value}`)
}
</script>

<template>
  <div class="m-row-demo--col">
    <MInputNumber v-model="editable" clearable :min="0" :max="100" @change="onChange" />

    <MInputNumber v-model="readonlyValue" readonly />

    <MInputNumber :model-value="128" :min="0" disabled />
  </div>
</template>
```

## API

### Props

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `number \| null` | `undefined`（未提供） | 数值（`v-model`）；空值传 `undefined` / `null` 均可 |
| `placeholder` | `string` | `undefined`（未提供） | 占位文本 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `readonly` | `boolean` | `false` | 是否只读 |
| `clearable` | `boolean` | `false` | 是否显示清除按钮 |
| `size` | `'small' \| 'default' \| 'large'` | `'default'` | 高度取全局 `--m-size-*` 令牌：small 28px / default 36px / large 48px |
| `controls` | `'right' \| 'both'` | `'right'` | 步进按钮布局：`right` 右侧并排（down / up 箭头）／`both` 左右两侧（减号 / 加号） |
| `min` | `number` | `undefined`（内部按 `-Infinity`） | 最小值 |
| `max` | `number` | `undefined`（内部按 `+Infinity`） | 最大值 |
| `step` | `number` | `1` | 步进值 |
| `precision` | `number` | `undefined`（未提供） | 显示精度，归一化时按该精度四舍五入 |
| `width` | `string` | `undefined`（未提供；样式默认 180px） | 宽度 |

`MInputNumber` **没有** `radius` 属性，圆角固定取全局令牌 `--m-radius`。

### Events

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `value: number \| null` | 值变化（清空或非法输入为 `null`） |
| `focus` | `event: FocusEvent` | 聚焦 |
| `blur` | `event: FocusEvent` | 失焦 |
| `change` | `value: number \| null` | 归一化后的值发生变化时触发（失焦 / 步进 / 清空） |
| `clear` | 无 | 点击清除按钮 |

### Slots

无。

### 类型定义

```ts
export type InputNumberControls = 'right' | 'both';
// size 复用 InputSize = 'small' | 'default' | 'large'
```

::: warning InputNumberControls 未从包入口导出
`miao-design` 的入口只再导出了 `MInputNumberProps`，`InputNumberControls` 这个类型别名不在入口的导出清单里。需要标注类型时，直接写字面量 `'right' | 'both'`，或者用索引访问 `MInputNumberProps['controls']`。
:::

## 使用建议

::: tip 回车提交，Escape 放弃
输入框内的值在失焦时才会写回外部。为了避免「输入后按回车看着生效、外部还是旧值」，组件额外支持：**回车立即提交**，**Escape 放弃本次编辑**回到外部传入的值。键盘上下键也可以步进。
:::

::: warning 空值与非法输入的语义
空字符串、纯空格、`Number()` 解析为 `NaN` 的输入都会在提交时变成 `null`，而不是 `0`。如果业务需要一个非空默认值，请在提交前兜底（例如 `value ?? 0`）。
:::

::: warning change 只在值真正变化时抛
`commit()` 内部比较归一化后的值与原 `modelValue`，相同则不抛 `update:modelValue` / `change`。因此「输入 10 再失焦」「输入 11 被 `max=10` 收敛回 10」都不会产生事件，不要依赖 `change` 计数。
:::

## 相关组件

- [Input 输入框](/components/input)：自由文本录入。
- [Slider 滑动条](/components/slider)：需要连续拖拽调值时比步进按钮更顺手。
- [Select 选择器](/components/select)：取值来自固定选项集合时改用下拉。
