<template>
|
|
<el-input v-bind="$attrs" v-model="internalValue" @input="handleInput" @blur="handleBlur" :placeholder="placeholder"
|
|
:disabled="disabled" type="text">
|
|
|
|
<template slot="prepend" v-if="prepend">
|
|
{{ prepend }}
|
|
</template>
|
|
</el-input>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: 'DecimalInput',
|
|
props: {
|
|
value: {
|
|
type: [Number, String],
|
|
default: ''
|
|
},
|
|
decimalDigits: {
|
|
type: Number,
|
|
default: 6
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
default: '请输入'
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
prepend: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
internalValue: this.value !== null && this.value !== undefined ? String(this.value) : '',
|
|
oldValue: null,
|
|
oldPattern: null,
|
|
patternRules: [
|
|
{ name: 'NA', pattern: /^NA$/i, inputPattern: /^N?A?$/i },
|
|
{ name: 'FRACTION', pattern: /^\d+(\/\d+)*$/, inputPattern: /^(\d+\/?)*$/ }
|
|
]
|
|
};
|
|
},
|
|
watch: {
|
|
value: {
|
|
handler(newVal) {
|
|
if (newVal === '' || newVal == null) {
|
|
this.internalValue = '';
|
|
this.oldValue = null;
|
|
this.oldPattern = null;
|
|
} else {
|
|
const strVal = String(newVal);
|
|
this.internalValue = strVal;
|
|
this.updateOldValue(strVal);
|
|
}
|
|
},
|
|
immediate: true
|
|
}
|
|
},
|
|
methods: {
|
|
updateOldValue(val) {
|
|
for (const rule of this.patternRules) {
|
|
if (rule.pattern.test(val)) {
|
|
this.oldValue = val;
|
|
this.oldPattern = rule;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
getMatchingRule(val) {
|
|
for (const rule of this.patternRules) {
|
|
if (rule.inputPattern.test(val)) {
|
|
return rule;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
handleInput(val) {
|
|
if (val === '') {
|
|
this.internalValue = '';
|
|
this.$emit('input', '');
|
|
return;
|
|
}
|
|
|
|
const upperVal = val.toUpperCase();
|
|
let cleaned = val;
|
|
let matchedRule = null;
|
|
if (this.oldPattern) {
|
|
// 检查是否是旧模式的延续(以oldValue开头)
|
|
if (val.startsWith(this.oldValue)) {
|
|
// 是旧模式的延续,检查是否符合inputPattern
|
|
if (this.oldPattern.inputPattern.test(val)) {
|
|
matchedRule = this.oldPattern;
|
|
cleaned = val;
|
|
} else {
|
|
// 不符合,但如果是FRACTION类型,尝试特殊处理
|
|
if (this.oldPattern.name === 'FRACTION') {
|
|
cleaned = val.replace(/[^\d/]/g, '');
|
|
const parts = cleaned.split('/');
|
|
const validParts = [];
|
|
for (let i = 0; i < parts.length; i++) {
|
|
if (parts[i] !== '') {
|
|
validParts.push(parts[i]);
|
|
}
|
|
}
|
|
cleaned = validParts.join('/');
|
|
matchedRule = this.oldPattern;
|
|
} else {
|
|
cleaned = this.oldValue || '';
|
|
this.internalValue = cleaned;
|
|
return;
|
|
}
|
|
}
|
|
} else if (this.oldPattern.inputPattern.test(val)) {
|
|
// 不是延续,但符合inputPattern(可能是清空后重新输入)
|
|
matchedRule = this.oldPattern;
|
|
cleaned = val;
|
|
} else {
|
|
// 尝试匹配新规则
|
|
matchedRule = this.getMatchingRule(val);
|
|
if (!matchedRule) {
|
|
cleaned = this.oldValue || '';
|
|
this.internalValue = cleaned;
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
matchedRule = this.getMatchingRule(val);
|
|
|
|
if (matchedRule) {
|
|
if (matchedRule.name === 'FRACTION') {
|
|
cleaned = val.replace(/[^\d/]/g, '');
|
|
const parts = cleaned.split('/');
|
|
const validParts = [];
|
|
for (let i = 0; i < parts.length; i++) {
|
|
if (parts[i] !== '') {
|
|
validParts.push(parts[i]);
|
|
}
|
|
}
|
|
cleaned = validParts.join('/');
|
|
} else {
|
|
cleaned = upperVal;
|
|
}
|
|
} else {
|
|
cleaned = val
|
|
.replace(/[^\d.-]/g, '')
|
|
.replace(/^(-)\1+/, '$1');
|
|
|
|
const firstDotIndex = cleaned.indexOf('.');
|
|
if (firstDotIndex !== -1) {
|
|
const before = cleaned.slice(0, firstDotIndex);
|
|
const after = cleaned.slice(firstDotIndex + 1).replace(/\./g, '');
|
|
cleaned = before + '.' + after;
|
|
}
|
|
|
|
if (this.decimalDigits > 0 && cleaned.includes('.')) {
|
|
const [intPart, decPart = ''] = cleaned.split('.');
|
|
cleaned = intPart + '.' + decPart.slice(0, this.decimalDigits);
|
|
} else if (this.decimalDigits === 0) {
|
|
cleaned = cleaned.split('.')[0];
|
|
}
|
|
|
|
if (cleaned === '.') cleaned = '0.';
|
|
else if (cleaned === '-.') cleaned = '-0.';
|
|
else if (cleaned.startsWith('.')) cleaned = '0' + cleaned;
|
|
else if (cleaned.startsWith('-.')) cleaned = '-0.' + cleaned.slice(2);
|
|
|
|
if (cleaned.includes('.')) {
|
|
const [int, dec] = cleaned.split('.');
|
|
let newInt = int;
|
|
if (/^-?0+\d/.test(int)) {
|
|
newInt = int.replace(/^-?0+(\d)/, '$1');
|
|
} else if (int === '' || int === '-') {
|
|
newInt = int + '0';
|
|
} else if (int === '00' || /^-00+$/.test(int)) {
|
|
newInt = int.startsWith('-') ? '-0' : '0';
|
|
}
|
|
cleaned = newInt + '.' + dec;
|
|
} else {
|
|
if (/^-?0+\d/.test(cleaned)) {
|
|
cleaned = cleaned.replace(/^-?0+(\d)/, '$1');
|
|
} else if (cleaned === '00' || /^-00+$/.test(cleaned)) {
|
|
cleaned = cleaned.startsWith('-') ? '-0' : '0';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.internalValue = cleaned;
|
|
|
|
if (cleaned === '' || cleaned === '-') {
|
|
this.$emit('input', cleaned === '-' ? '-' : '');
|
|
} else if (matchedRule && matchedRule.name !== 'FRACTION') {
|
|
this.$emit('input', cleaned);
|
|
} else if (cleaned.includes('/')) {
|
|
this.$emit('input', cleaned);
|
|
} else {
|
|
const num = parseFloat(cleaned);
|
|
this.$emit('input', isNaN(num) ? '' : num);
|
|
}
|
|
},
|
|
|
|
handleDecimalDigits(val) {
|
|
const actVal = val || this.internalValue;
|
|
let finalValue = actVal.trim();
|
|
|
|
if (finalValue === '' || finalValue === '-') {
|
|
return '';
|
|
}
|
|
|
|
const num = parseFloat(finalValue);
|
|
if (isNaN(num)) {
|
|
return '';
|
|
}
|
|
|
|
let formatted = String(num);
|
|
|
|
if (!finalValue.includes('.')) {
|
|
formatted = String(Math.floor(num));
|
|
} else {
|
|
const decPart = finalValue.split('.')[1];
|
|
const actualDecimalDigits = decPart.length;
|
|
const displayDecimalDigits = Math.min(actualDecimalDigits, this.decimalDigits);
|
|
formatted = num.toFixed(displayDecimalDigits).replace(/\.?0*$/, '');
|
|
}
|
|
return formatted;
|
|
},
|
|
|
|
handleBlur() {
|
|
const val = this.internalValue.trim();
|
|
|
|
if (val === '') {
|
|
this.oldValue = null;
|
|
this.oldPattern = null;
|
|
this.$emit('input', '');
|
|
this.$emit('blur', '');
|
|
return;
|
|
}
|
|
|
|
const upperVal = val.toUpperCase();
|
|
|
|
for (const rule of this.patternRules) {
|
|
if (rule.pattern.test(upperVal)) {
|
|
this.oldValue = upperVal;
|
|
this.oldPattern = rule;
|
|
this.internalValue = upperVal;
|
|
this.$emit('input', upperVal);
|
|
this.$emit('blur', upperVal);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (val.includes('/')) {
|
|
const parts = val.split('/');
|
|
const validParts = parts.filter(part => part !== '');
|
|
|
|
if (validParts.length === 0) {
|
|
if (this.oldValue) {
|
|
this.internalValue = this.oldValue;
|
|
this.$emit('input', this.oldValue);
|
|
this.$emit('blur', this.oldValue);
|
|
} else {
|
|
this.internalValue = '';
|
|
this.$emit('input', '');
|
|
this.$emit('blur', '');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (validParts.length === 1) {
|
|
const result = validParts[0];
|
|
this.oldValue = result;
|
|
this.oldPattern = this.patternRules.find(r => r.name === 'FRACTION');
|
|
this.internalValue = result;
|
|
this.$emit('input', result);
|
|
this.$emit('blur', result);
|
|
return;
|
|
}
|
|
|
|
const formattedValue = validParts.join('/');
|
|
this.oldValue = formattedValue;
|
|
this.oldPattern = this.patternRules.find(r => r.name === 'FRACTION');
|
|
this.internalValue = formattedValue;
|
|
this.$emit('input', formattedValue);
|
|
this.$emit('blur', formattedValue);
|
|
return;
|
|
}
|
|
|
|
if (this.oldValue && this.oldPattern) {
|
|
const partialMatch = this.oldPattern.inputPattern && this.oldPattern.inputPattern.test(val);
|
|
if (!partialMatch) {
|
|
this.internalValue = this.oldValue;
|
|
this.$emit('input', this.oldValue);
|
|
this.$emit('blur', this.oldValue);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let formatted = this.handleDecimalDigits(val);
|
|
this.internalValue = formatted;
|
|
this.$emit('input', parseFloat(formatted));
|
|
this.$emit('blur', parseFloat(formatted));
|
|
}
|
|
}
|
|
};
|
|
</script>
|