"use client"; import { Minus, Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export type AdminNumericStepperProps = { id?: string; value: string; onValueChange: (value: string) => void; min?: number; max?: number; step?: number; /** 整数模式(授信额度等);默认 false 支持小数(比例 %) */ integer?: boolean; disabled?: boolean; /** 失焦时超出 max 是否保留原输入(供保存前校验报错),默认 clamp */ preserveOverMaxOnBlur?: boolean; className?: string; suffix?: string; }; function parseNumeric(value: string, integer: boolean): number { const parsed = integer ? Number.parseInt(value, 10) : Number.parseFloat(value); return Number.isFinite(parsed) ? parsed : 0; } function formatNumeric(value: number, integer: boolean, step: number): string { if (integer) { return String(Math.trunc(value)); } const decimals = step < 1 ? 2 : step < 0.1 ? 3 : 1; const fixed = value.toFixed(decimals); return fixed.replace(/\.?0+$/, "") || "0"; } function clamp(value: number, min?: number, max?: number): number { let next = value; if (min !== undefined) { next = Math.max(min, next); } if (max !== undefined) { next = Math.min(max, next); } return next; } function isPartialInput(text: string, integer: boolean): boolean { const trimmed = text.trim(); if (trimmed === "" || trimmed === "-" || trimmed === ".") { return true; } if (!integer && trimmed.endsWith(".")) { return true; } return false; } export function AdminNumericStepper({ id, value, onValueChange, min = 0, max, step = 1, integer = false, disabled = false, preserveOverMaxOnBlur = false, className, suffix, }: AdminNumericStepperProps): React.ReactElement { const current = parseNumeric(value, integer); const atMin = min !== undefined && current <= min; const atMax = max !== undefined && current >= max; const outOfRange = !disabled && Number.isFinite(current) && ((min !== undefined && current < min) || (max !== undefined && current > max)); const applyDelta = (delta: number) => { const next = clamp(current + delta, min, max); onValueChange(formatNumeric(next, integer, step)); }; const commitInput = (raw: string) => { if (isPartialInput(raw, integer)) { onValueChange(formatNumeric(min ?? 0, integer, step)); return; } const parsed = parseNumeric(raw, integer); if (!Number.isFinite(parsed)) { onValueChange(formatNumeric(min ?? 0, integer, step)); return; } if ( preserveOverMaxOnBlur && max !== undefined && parsed > max ) { return; } onValueChange(formatNumeric(clamp(parsed, min, max), integer, step)); }; return (