chem_mes/mes-ui/mes-ui-admin-vue3/src/views/biz/components/MoneyInput.vue

340 lines
7.5 KiB
Vue
Raw Normal View History

<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
// 发出更新事件
if (inputValue === '') {
emit('update:modelValue', undefined)
} else {
const num = Number(inputValue)
emit('update:modelValue', isNaN(num) ? undefined : num)
}
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>