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

340 lines
7.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>