332 lines
7.3 KiB
Vue
332 lines
7.3 KiB
Vue
|
|
<template>
|
|||
|
|
<!--
|
|||
|
|
通用金额输入组件
|
|||
|
|
支持小数位数限制、负数控制、前后缀显示等功能
|
|||
|
|
-->
|
|||
|
|
<el-input
|
|||
|
|
v-model="displayValue"
|
|||
|
|
:placeholder="placeholder"
|
|||
|
|
:disabled="disabled"
|
|||
|
|
:readonly="readonly"
|
|||
|
|
:clearable="clearable"
|
|||
|
|
@input="handleInput"
|
|||
|
|
@blur="handleBlur"
|
|||
|
|
@focus="handleFocus"
|
|||
|
|
>
|
|||
|
|
<!-- 前缀模板:根据 showPrefix 控制显示 -->
|
|||
|
|
<template v-if="showPrefix" #prefix>{{ prefix }}</template>
|
|||
|
|
<!-- 后缀模板:根据 showSuffix 控制显示 -->
|
|||
|
|
<template v-if="showSuffix" #suffix>{{ suffix }}</template>
|
|||
|
|
</el-input>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
/**
|
|||
|
|
* 通用金额输入组件 MoneyInput
|
|||
|
|
*
|
|||
|
|
* 功能特性:
|
|||
|
|
* - 自动限制小数位数
|
|||
|
|
* - 可配置是否允许负数输入
|
|||
|
|
* - 支持自定义前后缀符号(如 ¥、$、% 等)
|
|||
|
|
* - 自动格式化显示,失焦时自动补全小数位
|
|||
|
|
* - 输入时自动过滤非法字符
|
|||
|
|
*
|
|||
|
|
* 使用示例:
|
|||
|
|
* ```vue
|
|||
|
|
* <!-- 基本用法:人民币金额,2位小数 -->
|
|||
|
|
* <MoneyInput v-model="amount" />
|
|||
|
|
*
|
|||
|
|
* <!-- 订单数量:整数,不允许负数,无符号 -->
|
|||
|
|
* <MoneyInput
|
|||
|
|
* v-model="quantity"
|
|||
|
|
* :decimal-places="0"
|
|||
|
|
* :allow-negative="false"
|
|||
|
|
* :show-prefix="false"
|
|||
|
|
* />
|
|||
|
|
*
|
|||
|
|
* <!-- 美元金额 -->
|
|||
|
|
* <MoneyInput
|
|||
|
|
* v-model="usdAmount"
|
|||
|
|
* prefix="$"
|
|||
|
|
* :decimal-places="2"
|
|||
|
|
* />
|
|||
|
|
*
|
|||
|
|
* <!-- 百分比显示:2位小数,显示%后缀 -->
|
|||
|
|
* <MoneyInput
|
|||
|
|
* v-model="rate"
|
|||
|
|
* :decimal-places="2"
|
|||
|
|
* suffix="%"
|
|||
|
|
* :show-suffix="true"
|
|||
|
|
* :show-prefix="false"
|
|||
|
|
* />
|
|||
|
|
* ```
|
|||
|
|
*/
|
|||
|
|
import { ref, watch } from 'vue'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Props 定义
|
|||
|
|
*/
|
|||
|
|
interface Props {
|
|||
|
|
/**
|
|||
|
|
* 绑定值(双向绑定)
|
|||
|
|
*/
|
|||
|
|
modelValue?: number | string
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 输入框占位文本
|
|||
|
|
* @default '请输入'
|
|||
|
|
*/
|
|||
|
|
placeholder?: string
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否禁用输入框
|
|||
|
|
* @default false
|
|||
|
|
*/
|
|||
|
|
disabled?: boolean
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否只读
|
|||
|
|
* @default false
|
|||
|
|
*/
|
|||
|
|
readonly?: boolean
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否显示清除按钮
|
|||
|
|
* @default true
|
|||
|
|
*/
|
|||
|
|
clearable?: boolean
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 小数位数
|
|||
|
|
* @default 2
|
|||
|
|
*/
|
|||
|
|
decimalPlaces?: number
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否允许输入负数
|
|||
|
|
* @default true
|
|||
|
|
*/
|
|||
|
|
allowNegative?: boolean
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 前缀符号(如 ¥、$)
|
|||
|
|
* @default '¥'
|
|||
|
|
*/
|
|||
|
|
prefix?: string
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 后缀符号(如 %、元)
|
|||
|
|
* @default ''
|
|||
|
|
*/
|
|||
|
|
suffix?: string
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否显示前缀
|
|||
|
|
* @default true
|
|||
|
|
*/
|
|||
|
|
showPrefix?: boolean
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否显示后缀
|
|||
|
|
* @default false
|
|||
|
|
*/
|
|||
|
|
showSuffix?: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Props 默认值配置
|
|||
|
|
*/
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
modelValue: undefined,
|
|||
|
|
placeholder: '请输入',
|
|||
|
|
disabled: false,
|
|||
|
|
readonly: false,
|
|||
|
|
clearable: true,
|
|||
|
|
decimalPlaces: 2,
|
|||
|
|
allowNegative: true,
|
|||
|
|
prefix: '¥',
|
|||
|
|
suffix: '',
|
|||
|
|
showPrefix: true,
|
|||
|
|
showSuffix: false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 事件定义
|
|||
|
|
*/
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
/**
|
|||
|
|
* 值变化事件(双向绑定)
|
|||
|
|
* @param value - 新的值(number | string | undefined)
|
|||
|
|
*/
|
|||
|
|
'update:modelValue': [value: number | string | undefined]
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 值改变事件(失焦时触发)
|
|||
|
|
* @param value - 新的值(number | string | undefined)
|
|||
|
|
*/
|
|||
|
|
'change': [value: number | string | undefined]
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 显示值(格式化后的字符串)
|
|||
|
|
*/
|
|||
|
|
const displayValue = ref<string>('')
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 初始化显示值
|
|||
|
|
* 将 modelValue 转换为格式化后的字符串
|
|||
|
|
*/
|
|||
|
|
const initDisplayValue = () => {
|
|||
|
|
if (props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== '') {
|
|||
|
|
const num = Number(props.modelValue)
|
|||
|
|
if (!isNaN(num)) {
|
|||
|
|
displayValue.value = num.toFixed(props.decimalPlaces)
|
|||
|
|
} else {
|
|||
|
|
displayValue.value = ''
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
displayValue.value = ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化显示值
|
|||
|
|
initDisplayValue()
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 监听 modelValue 变化,同步更新显示值
|
|||
|
|
*/
|
|||
|
|
watch(() => props.modelValue, () => {
|
|||
|
|
initDisplayValue()
|
|||
|
|
}, { immediate: true })
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理输入事件
|
|||
|
|
* 过滤非法字符,限制小数位数
|
|||
|
|
*/
|
|||
|
|
const handleInput = (value: string) => {
|
|||
|
|
let inputValue = value
|
|||
|
|
|
|||
|
|
// 处理负数:如果允许负数,保留一个负号在开头
|
|||
|
|
if (props.allowNegative) {
|
|||
|
|
inputValue = inputValue.replace(/[^\d.-]/g, '')
|
|||
|
|
const negativeCount = (inputValue.match(/-/g) || []).length
|
|||
|
|
if (negativeCount > 1) {
|
|||
|
|
inputValue = '-' + inputValue.replace(/-/g, '').slice(1)
|
|||
|
|
}
|
|||
|
|
if (inputValue.indexOf('-') > 0) {
|
|||
|
|
inputValue = inputValue.replace(/-/g, '')
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 不允许负数:只保留数字和小数点
|
|||
|
|
inputValue = inputValue.replace(/[^\d.]/g, '')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 正整数模式(decimalPlaces 为 0):不允许输入小数点
|
|||
|
|
if (props.decimalPlaces === 0) {
|
|||
|
|
inputValue = inputValue.replace(/\./g, '')
|
|||
|
|
displayValue.value = inputValue
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 输入以小数点开头时,自动在前面补 0
|
|||
|
|
if (inputValue.startsWith('.')) {
|
|||
|
|
inputValue = '0' + inputValue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 负数情况下,小数点在负号后直接输入时补 0
|
|||
|
|
if (inputValue.startsWith('-') && inputValue.length > 1 && inputValue[1] === '.') {
|
|||
|
|
inputValue = '-0' + inputValue.slice(1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 限制小数位数
|
|||
|
|
const dotIndex = inputValue.indexOf('.')
|
|||
|
|
if (dotIndex !== -1) {
|
|||
|
|
const integerPart = inputValue.slice(0, dotIndex)
|
|||
|
|
const decimalPart = inputValue.slice(dotIndex + 1)
|
|||
|
|
const limitedDecimal = decimalPart.slice(0, props.decimalPlaces)
|
|||
|
|
inputValue = integerPart + '.' + limitedDecimal
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只允许一个小数点
|
|||
|
|
const dotCount = (inputValue.match(/\./g) || []).length
|
|||
|
|
if (dotCount > 1) {
|
|||
|
|
const firstDotIndex = inputValue.indexOf('.')
|
|||
|
|
inputValue = inputValue.slice(0, firstDotIndex) + inputValue.slice(firstDotIndex).replace(/\./g, '')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 特殊情况处理:单独的负号或小数点
|
|||
|
|
if (inputValue === '-' || inputValue === '.') {
|
|||
|
|
displayValue.value = inputValue
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证并处理负数限制
|
|||
|
|
if (inputValue !== '' && inputValue !== '-') {
|
|||
|
|
const num = Number(inputValue)
|
|||
|
|
if (!isNaN(num)) {
|
|||
|
|
if (!props.allowNegative && num < 0) {
|
|||
|
|
inputValue = Math.abs(num).toFixed(props.decimalPlaces)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
displayValue.value = inputValue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理失焦事件
|
|||
|
|
* 格式化最终值并触发事件
|
|||
|
|
*/
|
|||
|
|
const handleBlur = () => {
|
|||
|
|
let value = displayValue.value
|
|||
|
|
|
|||
|
|
// 空值处理
|
|||
|
|
if (value === '' || value === '-' || value === '.') {
|
|||
|
|
displayValue.value = ''
|
|||
|
|
emit('update:modelValue', undefined)
|
|||
|
|
emit('change', undefined)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证数字有效性
|
|||
|
|
const num = Number(value)
|
|||
|
|
if (isNaN(num)) {
|
|||
|
|
displayValue.value = ''
|
|||
|
|
emit('update:modelValue', undefined)
|
|||
|
|
emit('change', undefined)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 负数限制处理
|
|||
|
|
if (!props.allowNegative && num < 0) {
|
|||
|
|
const absNum = Math.abs(num)
|
|||
|
|
displayValue.value = absNum.toFixed(props.decimalPlaces)
|
|||
|
|
emit('update:modelValue', absNum)
|
|||
|
|
emit('change', absNum)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化并输出
|
|||
|
|
displayValue.value = num.toFixed(props.decimalPlaces)
|
|||
|
|
emit('update:modelValue', num)
|
|||
|
|
emit('change', num)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理聚焦事件
|
|||
|
|
* 聚焦时移除格式化,显示原始数值
|
|||
|
|
*/
|
|||
|
|
const handleFocus = () => {
|
|||
|
|
if (displayValue.value !== '') {
|
|||
|
|
const num = Number(displayValue.value)
|
|||
|
|
if (!isNaN(num)) {
|
|||
|
|
displayValue.value = num.toString()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
/* 组件样式 */
|
|||
|
|
</style>
|