import { ref, watch, type Ref } from 'vue' /** 是否为可提交的完整非负小数(空字符串视为清空) */ export function isCompleteDecimalInput(raw: string): boolean { const trimmed = raw.trim() return trimmed === '' || /^\d+(\.\d+)?$/.test(trimmed) } export function parseDecimalInput(raw: string, precision?: number): number | null { const trimmed = raw.trim() if (!trimmed) return null if (!/^\d+(\.\d+)?$/.test(trimmed)) return null const num = Number(trimmed) if (!Number.isFinite(num) || num < 0) return null if (precision == null) return num return Number(num.toFixed(precision)) } function formatDecimalDisplay(val: number, precision?: number): string { if (precision == null) return String(val) return Number(val.toFixed(precision)).toString() } function sanitizeDecimalText(raw: string): string { let text = raw.replace(/[^\d.]/g, '') const dotIndex = text.indexOf('.') if (dotIndex >= 0) { text = text.slice(0, dotIndex + 1) + text.slice(dotIndex + 1).replace(/\./g, '') } return text } /** * 将 number | null 与 el-input 文本双向绑定; * 仅在完整小数或清空时写入 source,避免打断输入,同时保证运费计算 watch 能触发。 */ export function useDecimalInputBinding( source: Ref, options?: { precision?: number; onCommit?: () => void } ) { const text = ref('') watch( source, (val) => { const next = val == null || Number.isNaN(Number(val)) ? '' : formatDecimalDisplay(Number(val), options?.precision) if (text.value !== next) text.value = next }, { immediate: true } ) function commitFromText(raw: string, force = false) { const sanitized = sanitizeDecimalText(raw) text.value = sanitized if (!force && !isCompleteDecimalInput(sanitized)) return const parsed = parseDecimalInput(sanitized, options?.precision) const prev = source.value const changed = parsed == null ? prev != null : prev == null || Math.abs(Number(prev) - parsed) > 1e-9 source.value = parsed if (parsed != null) { text.value = formatDecimalDisplay(parsed, options?.precision) } else if (force) { text.value = '' } if (changed) options?.onCommit?.() } function handleInput(val: string) { commitFromText(val, false) } function handleBlur() { commitFromText(text.value, true) } return { text, handleInput, handleBlur } }