| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- import React, { useState } from 'react';
- import { InputNumber, Select, Space, SpaceProps } from 'antd';
- import type { InputNumberProps } from 'antd/es/input-number';
- import type { SelectProps } from 'antd/es/select';
- const { Option } = Select;
- export interface NumberUnitOption {
- label: React.ReactNode;
- value: string;
- }
- export interface NumberUnitValue {
- number?: number;
- unit?: string;
- }
- export type NumberWithUnitProps = SpaceProps & {
- value?: NumberUnitValue;
- onChange?: (next: NumberUnitValue) => void;
- numberClassName?: string;
- unitClassName?: string;
- className?: string;
- options: NumberUnitOption[];
- defaultUnit?: string;
- defaultNumber?: number;
- numberProps?: Partial<InputNumberProps>;
- selectProps?: Partial<SelectProps>;
- };
- const NumberWithUnit: React.FC<NumberWithUnitProps> = ({
- value = {},
- onChange,
- numberClassName,
- unitClassName,
- className,
- options,
- defaultUnit,
- defaultNumber,
- numberProps,
- selectProps,
- onBlur,
- ...rest // ← 这里会收到、style 等
- }) => {
- const unit = value?.unit ?? defaultUnit ?? undefined;
- const [isComposing, setIsComposing] = useState(false);
- const triggerChange = (changed: Partial<NumberUnitValue>) => {
- onChange?.({ ...value, ...changed });
- };
- // 处理键盘输入 - 阻止非数字字符的输入
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- // 如果正在使用输入法,不拦截
- if (isComposing) return;
- // 允许的功能键
- const allowedKeys = [
- 'Backspace',
- 'Delete',
- 'ArrowLeft',
- 'ArrowRight',
- 'ArrowUp',
- 'ArrowDown',
- 'Tab',
- 'Enter',
- 'Escape',
- 'Home',
- 'End',
- ];
- // 如果是功能键,允许
- if (allowedKeys.includes(e.key)) return;
- // 如果是 Ctrl/Cmd 组合键(如 Ctrl+A, Ctrl+C, Ctrl+V),允许
- if (e.ctrlKey || e.metaKey) return;
- // 如果不是数字,阻止输入
- if (!/^\d$/.test(e.key)) {
- e.preventDefault();
- }
- };
- // 处理粘贴 - 过滤粘贴内容中的非数字字符
- const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
- e.preventDefault();
- // 获取剪贴板内容
- const pastedText = e.clipboardData.getData('text');
- // 只保留数字
- const cleanedText = pastedText.replace(/\D/g, '');
- if (cleanedText) {
- const numValue = parseInt(cleanedText, 10);
- triggerChange({ number: numValue });
- }
- };
- // 处理输入法开始
- const handleCompositionStart = () => {
- setIsComposing(true);
- };
- // 处理输入法结束 - 过滤输入法输入的非数字字符
- const handleCompositionEnd = (
- e: React.CompositionEvent<HTMLInputElement>
- ) => {
- setIsComposing(false);
- // 获取输入的内容并过滤
- const inputValue = e.currentTarget.value;
- const cleanedValue = inputValue.replace(/\D/g, '');
- if (cleanedValue) {
- const numValue = parseInt(cleanedValue, 10);
- triggerChange({ number: numValue });
- } else {
- // 如果过滤后没有有效数字,清空
- triggerChange({ number: undefined });
- }
- };
- return (
- <>
- <style>{`
- .my-space .ant-space-item:nth-child(1) {
- width: 70%;
- }
- .my-space .ant-space-item:nth-child(2) {
- width: 30%;
- }
- `}</style>
- <Space {...rest} className={`${className} my-space`}>
- <InputNumber
- className={numberClassName}
- value={value?.number}
- onChange={(n) =>
- triggerChange({ number: typeof n === 'number' ? n : undefined })
- }
- min={0}
- precision={0}
- parser={(value) => value?.replace(/\D/g, '') || ''}
- keyboard={true}
- onKeyDown={handleKeyDown}
- onPaste={handlePaste}
- onCompositionStart={handleCompositionStart}
- onCompositionEnd={handleCompositionEnd}
- {...numberProps}
- defaultValue={defaultNumber}
- onBlur={onBlur}
- />
- <Select
- className={unitClassName}
- value={unit}
- onChange={(u) => triggerChange({ unit: u })}
- {...selectProps}
- >
- {options.map((opt) => (
- <Option key={opt.value} value={opt.value}>
- {opt.label}
- </Option>
- ))}
- </Select>
- </Space>
- </>
- );
- };
- export default NumberWithUnit;
|