NumberInput 数字输入
数字输入框,带 +/− 步进按钮、min/max/step 约束、键盘上下步进、自动 clamp。
基础用法
绑定 number | null —— null 代表空值,便于跟「未填写」区分开。min / max 约束,离焦时自动 clamp。
背景 视口
<script setup lang="ts">
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const value = ref<number | null>(10);
</script>
<template>
<div style="max-width: 16rem;">
<CfNumberInput v-model="value" :min="0" :max="100" />
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const value = ref<number | null>(10);
</script>
<template>
<div style="max-width: 16rem;">
<CfNumberInput v-model="value" :min="0" :max="100" />
</div>
</template> import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [value, setValue] = useState(10);
const [value, setValue] = useState(10);
return (
<>
<CfNumberInput value={value} onChange={setValue} min={0} max={100} />
</>
);
} import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [value, setValue] = useState(10);
const [value, setValue] = useState(10);
return (
<>
<CfNumberInput value={value} onChange={setValue} min={0} max={100} />
</>
);
} 步进与精度
step 是按钮 / 键盘每次的步进单位;小数 step 会自动推断 precision(step=0.05 → 显示 2 位小数)。也可以显式传 precision 强制位数。空值 null 时占位文本生效。
背景 视口
<script setup lang="ts">
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const ratio = ref<number | null>(0.25);
const price = ref<number | null>(99.9);
const optional = ref<number | null>(null);
</script>
<template>
<div class="demo-stack">
<div class="demo-row" style="gap: 1rem;">
<CfNumberInput v-model="ratio" :step="0.05" :min="0" :max="1" />
<CfNumberInput v-model="price" :step="0.1" :precision="2" />
<CfNumberInput v-model="optional" placeholder="留空表示不限" />
</div>
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const ratio = ref<number | null>(0.25);
const price = ref<number | null>(99.9);
const optional = ref<number | null>(null);
</script>
<template>
<div class="demo-stack">
<div class="demo-row" style="gap: 1rem;">
<CfNumberInput v-model="ratio" :step="0.05" :min="0" :max="1" />
<CfNumberInput v-model="price" :step="0.1" :precision="2" />
<CfNumberInput v-model="optional" placeholder="留空表示不限" />
</div>
</div>
</template> import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [ratio, setRatio] = useState(0.25);
const [ratio, setRatio] = useState(0.25);
const [price, setPrice] = useState(99.9);
const [price, setPrice] = useState(99.9);
const [optional, setOptional] = useState(null);
const [optional, setOptional] = useState(null);
return (
<>
<CfNumberInput value={ratio} onChange={setRatio} step={0.05} min={0} max={1} />
<CfNumberInput value={price} onChange={setPrice} step={0.1} precision={2} />
<CfNumberInput value={optional} onChange={setOptional} placeholder="留空表示不限" />
</>
);
} import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [ratio, setRatio] = useState(0.25);
const [ratio, setRatio] = useState(0.25);
const [price, setPrice] = useState(99.9);
const [price, setPrice] = useState(99.9);
const [optional, setOptional] = useState(null);
const [optional, setOptional] = useState(null);
return (
<>
<CfNumberInput value={ratio} onChange={setRatio} step={0.05} min={0} max={1} />
<CfNumberInput value={price} onChange={setPrice} step={0.1} precision={2} />
<CfNumberInput value={optional} onChange={setOptional} placeholder="留空表示不限" />
</>
);
} 前缀与单位
prefix / suffix 可以直接传字符串,也可以用同名插槽塞自定义节点。常见用法是币种、百分号、单位(kg / ms / px)。前后缀和输入框共用同一条边框,不会破坏聚焦态。
背景 视口
¥元
%
kg
<script setup lang="ts">
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const price = ref<number | null>(199);
const ratio = ref<number | null>(75);
const weight = ref<number | null>(1.5);
</script>
<template>
<div class="demo-stack">
<CfNumberInput v-model="price" :min="0" :step="0.1" :precision="2" prefix="¥" suffix="元" />
<CfNumberInput v-model="ratio" :min="0" :max="100" :step="5" suffix="%" />
<CfNumberInput v-model="weight" :min="0" :step="0.1" :precision="1" suffix="kg" />
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const price = ref<number | null>(199);
const ratio = ref<number | null>(75);
const weight = ref<number | null>(1.5);
</script>
<template>
<div class="demo-stack">
<CfNumberInput v-model="price" :min="0" :step="0.1" :precision="2" prefix="¥" suffix="元" />
<CfNumberInput v-model="ratio" :min="0" :max="100" :step="5" suffix="%" />
<CfNumberInput v-model="weight" :min="0" :step="0.1" :precision="1" suffix="kg" />
</div>
</template> import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [price, setPrice] = useState(199);
const [price, setPrice] = useState(199);
const [ratio, setRatio] = useState(75);
const [ratio, setRatio] = useState(75);
const [weight, setWeight] = useState(1.5);
const [weight, setWeight] = useState(1.5);
return (
<>
<CfNumberInput value={price} onChange={setPrice} prefix="¥" suffix="元" step={0.1} precision={2} />
<CfNumberInput value={ratio} onChange={setRatio} suffix="%" min={0} max={100} step={5} />
<CfNumberInput value={weight} onChange={setWeight} suffix="kg" step={0.1} precision={1} />
</>
);
} import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [price, setPrice] = useState(199);
const [price, setPrice] = useState(199);
const [ratio, setRatio] = useState(75);
const [ratio, setRatio] = useState(75);
const [weight, setWeight] = useState(1.5);
const [weight, setWeight] = useState(1.5);
return (
<>
<CfNumberInput value={price} onChange={setPrice} prefix="¥" suffix="元" step={0.1} precision={2} />
<CfNumberInput value={ratio} onChange={setRatio} suffix="%" min={0} max={100} step={5} />
<CfNumberInput value={weight} onChange={setWeight} suffix="kg" step={0.1} precision={1} />
</>
);
} 三档尺寸
size 控制控件高度,与 Input / Select 保持一致 — sm 紧凑型 / md 默认 / lg 大号触摸友好。
背景 视口
<script setup lang="ts">
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const a = ref<number | null>(50);
const b = ref<number | null>(50);
const c = ref<number | null>(50);
</script>
<template>
<div class="demo-row" style="gap: 1rem;">
<CfNumberInput v-model="a" size="sm" />
<CfNumberInput v-model="b" size="md" />
<CfNumberInput v-model="c" size="lg" />
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const a = ref<number | null>(50);
const b = ref<number | null>(50);
const c = ref<number | null>(50);
</script>
<template>
<div class="demo-row" style="gap: 1rem;">
<CfNumberInput v-model="a" size="sm" />
<CfNumberInput v-model="b" size="md" />
<CfNumberInput v-model="c" size="lg" />
</div>
</template> import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [a, setA] = useState(50);
const [a, setA] = useState(50);
const [b, setB] = useState(50);
const [b, setB] = useState(50);
const [c, setC] = useState(50);
const [c, setC] = useState(50);
return (
<>
<CfNumberInput value={a} onChange={setA} size="sm" />
<CfNumberInput value={b} onChange={setB} size="md" />
<CfNumberInput value={c} onChange={setC} size="lg" />
</>
);
} import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [a, setA] = useState(50);
const [a, setA] = useState(50);
const [b, setB] = useState(50);
const [b, setB] = useState(50);
const [c, setC] = useState(50);
const [c, setC] = useState(50);
return (
<>
<CfNumberInput value={a} onChange={setA} size="sm" />
<CfNumberInput value={b} onChange={setB} size="md" />
<CfNumberInput value={c} onChange={setC} size="lg" />
</>
);
} 隐藏步进按钮 / 禁用
hideSteppers 隐藏右侧 +/− 按钮但保留键盘步进 —— 在表格内或紧凑表单里更清爽。disabled 完全禁用交互。
背景 视口
默认(带 +/− 按钮)
hideSteppers
disabled
<script setup lang="ts">
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const a = ref<number | null>(42);
const b = ref<number | null>(42);
</script>
<template>
<div class="demo-stack">
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">默认(带 +/− 按钮)</div>
<CfNumberInput v-model="a" />
</div>
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">hideSteppers</div>
<CfNumberInput v-model="b" hide-steppers />
</div>
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">disabled</div>
<CfNumberInput :model-value="42" disabled />
</div>
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfNumberInput } from '@chufix-design/vue';
const a = ref<number | null>(42);
const b = ref<number | null>(42);
</script>
<template>
<div class="demo-stack">
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">默认(带 +/− 按钮)</div>
<CfNumberInput v-model="a" />
</div>
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">hideSteppers</div>
<CfNumberInput v-model="b" hide-steppers />
</div>
<div>
<div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">disabled</div>
<CfNumberInput :model-value="42" disabled />
</div>
</div>
</template> import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [a, setA] = useState(42);
const [a, setA] = useState(42);
const [b, setB] = useState(42);
const [b, setB] = useState(42);
return (
<>
<CfNumberInput value={a} onChange={setA} />
<CfNumberInput value={b} onChange={setB} hideSteppers />
<CfNumberInput value={42} disabled />
</>
);
} import { useState } from 'react';
import { CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [a, setA] = useState(42);
const [a, setA] = useState(42);
const [b, setB] = useState(42);
const [b, setB] = useState(42);
return (
<>
<CfNumberInput value={a} onChange={setA} />
<CfNumberInput value={b} onChange={setB} hideSteppers />
<CfNumberInput value={42} disabled />
</>
);
} 事件与表单
NumberInput 会区分输入中的原始文本、提交后的数字值、步进来源和非法输入。传入 name / id 后会直接落到原生 input,适合放进表单或自动化测试。
背景 视口
ready
输入、步进或提交后会显示事件流。<script setup lang="ts">
import { ref } from 'vue';
import { CfBadge, CfNumberInput, type NumberInputChangeReason } from '@chufix-design/vue';
const value = ref<number | null>(25);
const phase = ref('ready');
const logs = ref(['输入、步进或提交后会显示事件流。']);
function record(name: string, detail: string) {
logs.value = [`${name}: ${detail}`, ...logs.value].slice(0, 5);
}
function onChange(next: number | null, meta: { raw: string; reason: NumberInputChangeReason }) {
value.value = next;
phase.value = meta.reason;
record('change', `${String(next ?? 'null')} / raw=${meta.raw || 'empty'} / ${meta.reason}`);
}
</script>
<template>
<div class="number-events">
<CfNumberInput
:model-value="value"
name="retry-budget"
:min="0"
:max="100"
:step="5"
placeholder="0 - 100"
@input="(raw) => record('input', raw || 'empty')"
@change="onChange"
@step="(next, meta) => record('step', `${next} / direction=${meta.direction}`)"
@invalid="(meta) => {
phase = 'invalid';
record('invalid', `${meta.raw} 不是有效数字`);
}"
@focus="record('focus', 'focused')"
@blur="record('blur', 'committed')"
/>
<div class="number-events__status">
<CfBadge tone="info" :content="phase" />
<div class="number-events__log" aria-live="polite">
<code v-for="entry in logs" :key="entry">{{ entry }}</code>
</div>
</div>
</div>
</template>
<style scoped>
.number-events {
display: grid;
gap: 12px;
width: min(100%, 420px);
}
.number-events__status {
display: flex;
align-items: flex-start;
gap: 10px;
}
.number-events__log {
display: grid;
gap: 4px;
min-width: 0;
}
.number-events__log code {
white-space: normal;
}
</style> <script setup>
import { ref } from 'vue';
import { CfBadge, CfNumberInput } from '@chufix-design/vue';
const value = ref<number | null>(25);
const phase = ref('ready');
const logs = ref(['输入、步进或提交后会显示事件流。']);
function record(name, detail) {
logs.value = [`${name}: ${detail}`, ...logs.value].slice(0, 5);
}
function onChange(next, meta: { raw: string; reason: NumberInputChangeReason }) {
value.value = next;
phase.value = meta.reason;
record('change', `${String(next ?? 'null')} / raw=${meta.raw || 'empty'} / ${meta.reason}`);
}
</script>
<template>
<div class="number-events">
<CfNumberInput
:model-value="value"
name="retry-budget"
:min="0"
:max="100"
:step="5"
placeholder="0 - 100"
@input="(raw) => record('input', raw || 'empty')"
@change="onChange"
@step="(next, meta) => record('step', `${next} / direction=${meta.direction}`)"
@invalid="(meta) => {
phase = 'invalid';
record('invalid', `${meta.raw} 不是有效数字`);
}"
@focus="record('focus', 'focused')"
@blur="record('blur', 'committed')"
/>
<div class="number-events__status">
<CfBadge tone="info" :content="phase" />
<div class="number-events__log" aria-live="polite">
<code v-for="entry in logs" :key="entry">{{ entry }}</code>
</div>
</div>
</div>
</template>
<style scoped>
.number-events {
display: grid;
gap: 12px;
width: min(100%, 420px);
}
.number-events__status {
display: flex;
align-items: flex-start;
gap: 10px;
}
.number-events__log {
display: grid;
gap: 4px;
min-width: 0;
}
.number-events__log code {
white-space: normal;
}
</style> import { useState } from 'react';
import { CfBadge, CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [value, setValue] = useState<number | null>(25);
const [phase, setPhase] = useState('ready');
const [logs, setLogs] = useState(['输入、步进或提交后会显示事件流。']);
function record(name: string, detail: string) {
setLogs([`${name}: ${detail}`, ...logs].slice(0, 5));
}
function onChange(next: number | null, meta: { raw: string; reason: NumberInputChangeReason }) {
setValue(next);
setPhase(meta.reason);
record('change', `${String(next ?? 'null')} / raw=${meta.raw || 'empty'} / ${meta.reason}`);
}
return (
<>
<div className="number-events">
<CfNumberInput modelValue={value} name="retry-budget" min={0} max={100} step={5} placeholder="0 - 100" onInput={(raw) => record('input', raw || 'empty')}
onChange={onChange}
onStep={(next, meta) => record('step', `${next} / direction=${meta.direction}`)}
onInvalid={(meta) => {
setPhase('invalid');
record('invalid', `${meta.raw} 不是有效数字`);
}}
onFocus={() => record('focus', 'focused')}
onBlur={() => record('blur', 'committed')}
/>
<div className="number-events__status">
<CfBadge tone="info" content={phase} />
<div className="number-events__log" aria-live="polite">
<code v-for="entry in logs" key={entry}>{entry}</code>
</div>
</div>
</div>
</>
);
} import { useState } from 'react';
import { CfBadge, CfNumberInput } from '@chufix-design/react';
export default function Demo() {
const [value, setValue] = useState<number | null>(25);
const [phase, setPhase] = useState('ready');
const [logs, setLogs] = useState(['输入、步进或提交后会显示事件流。']);
function record(name, detail) {
setLogs([`${name}: ${detail}`, ...logs].slice(0, 5));
}
function onChange(next, meta: { raw: string; reason: NumberInputChangeReason }) {
setValue(next);
setPhase(meta.reason);
record('change', `${String(next ?? 'null')} / raw=${meta.raw || 'empty'} / ${meta.reason}`);
}
return (
<>
<div className="number-events">
<CfNumberInput modelValue={value} name="retry-budget" min={0} max={100} step={5} placeholder="0 - 100" onInput={(raw) => record('input', raw || 'empty')}
onChange={onChange}
onStep={(next, meta) => record('step', `${next} / direction=${meta.direction}`)}
onInvalid={(meta) => {
setPhase('invalid');
record('invalid', `${meta.raw} 不是有效数字`);
}}
onFocus={() => record('focus', 'focused')}
onBlur={() => record('blur', 'committed')}
/>
<div className="number-events__status">
<CfBadge tone="info" content={phase} />
<div className="number-events__log" aria-live="polite">
<code v-for="entry in logs" key={entry}>{entry}</code>
</div>
</div>
</div>
</>
);
} 键盘交互
↑/↓—— 按 step 步进PageUp/PageDown—— 按 10 倍 step 步进Home/End—— 跳到 min / max(存在边界时)Enter—— 提交并 clamp- 离焦 —— 自动 clamp 到 [min, max],不在范围内的输入会被纠正
- 非数字输入会触发
invalid并回滚到上一个有效值
API
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue (Vue) / value (React) | number | null | null | 当前值;null 表示空 |
min | number | — | 最小值 |
max | number | — | 最大值 |
step | number | 1 | 步进 |
precision | number | 推断 | 小数位数;不传则从 step 推断 |
size | 'sm' | 'md' | 'lg' | 'md' | 高度 |
hideSteppers | boolean | false | 隐藏 +/− 按钮(仍可键盘步进) |
placeholder | string | — | 占位文本(值为 null 时显示) |
prefix | string | slot | ReactNode | — | 前缀(币种、图标);Vue 也可用 prefix 插槽 |
suffix | string | slot | ReactNode | — | 后缀(百分号、单位);Vue 也可用 suffix 插槽 |
disabled | boolean | false | 禁用 |
name | string | — | 原生 input name,参与表单提交 |
id | string | — | 原生 input id |
Events
| Vue 事件 | React 回调 | payload | 说明 |
|---|---|---|---|
input | onInput | raw | 输入中的原始字符串 |
change | onChange | (value, { raw, reason }) | 提交后的数字值,reason 为 blur / enter / step / home / end 等 |
step | onStep | (value, { direction }) | 点击按钮或方向键步进 |
invalid | onInvalid | { raw, reason } | 提交非法数字时触发 |
focus / blur | onFocus / onBlur | FocusEvent | 原生焦点事件 |
反馈与讨论
NumberInput 数字输入 的讨论