# useInputNumber

<script setup>
import { reactive } from 'vue'

const stateEl = reactive({
  value: '',
  displayValue: ''
})

const stateAntd = reactive({
  value: '',
  displayValue: ''
})
</script>

一个用于管理数字输入、精度控制和格式化展示的 Vue Hook。

## 基本用法

```vue
<script setup lang="ts">
import { useInputNumber } from 'xn-fe-tools'

// 基础用法：第一个参数传入初始值，第二个参数传入配置选项
const { value, displayValue, handleInput, handleBlur } = useInputNumber('100.50', {
  precision: 2,
  showThousands: true,
})
</script>

<template>
  <div>
    <input
      :value="displayValue"
      @input="e => handleInput((e.target as HTMLInputElement).value)"
      @blur="handleBlur"
    />
    <p>原始数值: {{ value }}</p>
  </div>
</template>
```

## API

### 参数

```ts
// 方式 A: 仅传入配置对象（初始值默认为空字符串）
function useInputNumber(options?: UseInputNumberOptions): UseInputNumberReturn

// 方式 B: 双参数模式（推荐用于组件封装或需要绑定响应式数据源的场景）
function useInputNumber(
  inputValue: MaybeRefOrGetter<string | number | undefined>,
  options?: UseInputNumberOptions
): UseInputNumberReturn
```

#### 配置项

| 参数名          | 类型                         | 默认值      | 说明                                                                                                             |
| :-------------- | :--------------------------- | :---------- | :--------------------------------------------------------------------------------------------------------------- |
| `precision`     | `number`                     | `0`         | 小数点保留位数                                                                                                   |
| `decimalLimitMode`| `'discard' \| 'ceil' \| 'round'` | `'discard'` | 小数位超限时的处理方式。`discard`: 直接丢弃; `ceil`: 向上取整; `round`: 四舍五入                                 |
| `autoFixed`     | `boolean \| 'pad' \| 'trim'` | `false`     | 失去焦点时如何处理小数位。`true`/`'pad'`: 补全(如 1->1.00); `'trim'`: 清除末尾的0(如 1.10->1.1); `false`: 不处理 |
| `max`           | `number`                     | `Infinity`  | 允许输入的最大数值                                                                                               |
| `min`           | `number`                     | `-Infinity` | 允许输入的最小数值                                                                                               |
| `allowNegative` | `boolean`                    | `false`     | 是否允许输入负数                                                                                                 |
| `showThousands` | `boolean`                    | `false`     | 是否开启千分位格式化展示                                                                                         |

### 返回值

| 属性名         | 类型                                | 说明                                                          |
| :------------- | :---------------------------------- | :------------------------------------------------------------ |
| `value`        | `Ref<string>`                       | 内部维护的“纯净”数值字符串（不含逗号等格式化字符）            |
| `displayValue` | `ComputedRef<string>`               | 格式化后的显示字符串（含千分位等）                            |
| `handleInput`  | `(val: string \| number) => void`   | 输入处理函数，用于实时同步和清洗用户的输入内容                |
| `handleBlur`   | `() => void`                        | 失去焦点处理函数，执行边界值检查及自动补全精度逻辑            |
| `formatter`    | `(val: string) => string`           | 格式化函数，可直接适配 Element Plus 等组件的 `formatter` 属性 |
| `parser`       | `(val: string) => string`           | 解析函数，可直接适配 Element Plus 等组件的 `parser` 属性      |
| `clean`        | `(val: string \| number) => string` | 核心清洗函数，用于提取字符串中的合法数字部分                  |

## 示例

### 封装 Element Plus 组件

通过封装 `<el-input>`，我们可以快速创建一个支持千分位、精度控制的数字输入框组件。

#### 组件实现 (InputNumber.vue)

```vue
<script setup lang="ts">
import { watch } from 'vue'
import { useInputNumber } from 'xn-fe-tools'

const props = withDefaults(
  defineProps<{
    modelValue?: string | number
    displayValue?: string | number
    precision?: number
    autoFixed?: boolean
    max?: number
    min?: number
    allowNegative?: boolean
    showThousands?: boolean
  }>(),
  {
    modelValue: '',
    precision: 0,
    autoFixed: true,
    showThousands: false,
    allowNegative: false,
  }
)

const emits = defineEmits(['update:modelValue', 'update:displayValue', 'change', 'blur', 'input'])

// 使用 Hook：第一个参数传 Getter，第二个参数传 props
const {
  value,
  displayValue,
  handleBlur: _handleBlur,
  parser,
  formatter,
  handleInput,
} = useInputNumber(() => props.modelValue, props)

watch(value, val => {
  emits('update:modelValue', val)
  emits('change', val)
})

watch(displayValue, val => {
  emits('update:displayValue', val)
})

const handleBlur = () => {
  _handleBlur()
  emits('blur')
}

// 注意：在使用 v-model="value" 时，el-input 会自动处理输入
</script>

<template>
  <el-input
    v-bind="$attrs"
    v-model="value"
    @blur="handleBlur"
    @input="handleInput"
    :formatter="formatter"
    :parser="parser"
  >
    <!-- 透传插槽 -->
    <template v-for="(_, name) in $slots" #[name]="scope">
      <slot :name="name" v-bind="scope || {}" />
    </template>
  </el-input>
</template>
```

#### 组件调用

```vue
<script setup lang="ts">
import { reactive } from 'vue'
import InputNumber from './InputNumber.vue'

const state = reactive({
  value: '',
  displayValue: '',
})
</script>

<template>
  <InputNumber
    v-model="state.value"
    v-model:display-value="state.displayValue"
    :precision="2"
    :show-thousands="true"
    :auto-fixed="true"
    :allow-negative="true"
    placeholder="请输入金额..."
    :max="10000"
  />
  <p>实际值: {{ state.value }}</p>
  <p>显示值: {{ state.displayValue }}</p>
</template>
```

#### 实际组件Demo

<br/>

<DemoInputNumber
  v-model="stateEl.value"
  v-model:display-value="stateEl.displayValue"
  :precision="2"
  :show-thousands="true"
  :auto-fixed="true"
  :allow-negative="true"
  placeholder="请输入金额..."
  :max="10000"
/>

<p>实际值: {{ stateEl.value }}</p>
<p>显示值: {{ stateEl.displayValue }}</p>

### 封装 Ant Design Vue 组件

由于 Ant Design Vue 的 `<a-input>` 组件对光标位置的维护机制不同，我们需要在 Wrapper 中结合 `clean` 函数手动处理光标修正逻辑。

#### 组件实现 (InputNumberAntd.vue)

```vue
<script setup lang="ts">
import { watch } from 'vue'
import { Input } from 'ant-design-vue'
import { useInputNumber } from 'xn-fe-tools'

const props = withDefaults(
  defineProps<{
    modelValue?: string | number
    displayValue?: string | number
    precision?: number
    autoFixed?: boolean
    max?: number
    min?: number
    allowNegative?: boolean
    showThousands?: boolean
  }>(),
  {
    modelValue: '',
    displayValue: '',
    precision: 0,
    autoFixed: true,
    showThousands: false,
    allowNegative: false,
  }
)

const emits = defineEmits(['update:modelValue', 'update:displayValue', 'change', 'blur'])

const {
  value,
  displayValue,
  handleBlur: _handleBlur,
  handleInput: _handleInput,
  clean: _clean,
} = useInputNumber(() => props.modelValue, props)

// 同步状态输出
watch(value, val => {
  emits('update:modelValue', val)
})

watch(displayValue, val => {
  emits('update:displayValue', val)
})

const handleBlur = () => {
  _handleBlur()
  emits('blur')
}

const handleChange = (e: any) => {
  const inputEl = e.target
  const oldRawValue = inputEl.value
  const oldCursorPos = inputEl.selectionStart || 0

  // 使用 hook 内部的 clean 逻辑来计算光标前真正会保留的字符数量
  // 这样就能自动处理“中间输入负号被过滤”等情况，保持计数的一致性
  const validBeforeCursor = _clean(oldRawValue.slice(0, oldCursorPos)).length

  // 执行 hook 清洗逻辑
  _handleInput(oldRawValue)

  const newVal = displayValue.value

  // 强制同步 DOM
  if (inputEl.value !== newVal) {
    inputEl.value = newVal

    // 恢复光标位置：找到第 validBeforeCursor 个有效字符在 newVal 中的位置
    let newPos = 0
    let validCount = 0
    while (validCount < validBeforeCursor && newPos < newVal.length) {
      // 这里的判断逻辑要与 clean 保持一致：只有数字、小数点、负号被视为有效字符
      if (/[\d.-]/.test(newVal[newPos])) {
        validCount++
      }
      newPos++
    }

    inputEl.setSelectionRange(newPos, newPos)
  }

  emits('change', e)
}
</script>

<template>
  <Input v-bind="$attrs" :value="displayValue" @change="handleChange" @blur="handleBlur">
    <!-- 透传 Ant Design Vue 的插槽 (prefix, suffix, addonBefore, addonAfter 等) -->
    <template v-for="(_, name) in $slots" #[name]="scope">
      <slot :name="name" v-bind="scope || {}" />
    </template>
  </Input>
</template>
```

#### 组件调用

```vue
<script setup lang="ts">
import { reactive } from 'vue'
import InputNumberAntd from './InputNumberAntd.vue'

const state = reactive({
  value: '',
  displayValue: '',
})
</script>

<template>
  <InputNumberAntd
    v-model="state.value"
    v-model:display-value="state.displayValue"
    :precision="2"
    :show-thousands="true"
    :auto-fixed="true"
    :allow-negative="true"
    placeholder="请输入..."
    :max="10000"
  />
</template>
```

#### 实际组件Demo

<br/>

<DemoInputNumberAntd
  v-model="stateAntd.value"
  v-model:display-value="stateAntd.displayValue"
  :precision="2"
  :show-thousands="true"
  :auto-fixed="true"
  :allow-negative="true"
  placeholder="请输入..."
  :max="10000"
/>

<p>实际值: {{ stateAntd.value }}</p>
<p>显示值: {{ stateAntd.displayValue }}</p>
