hookehuyr

fix(plan): 优化金额输入组件体验和样式

- 所有输入框组件添加灰色背景(bg-gray-50)
- AmountInput 和 AmountKeyboard 添加数字输入震动反馈
- 修复金额显示自动添加小数点问题(输入12显示12而非12.00)
- 优化 AmountKeyboard 弹窗和键盘打开时的值初始化

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -8,8 +8,7 @@
<!-- 触发区域 -->
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3"
:class="{ 'bg-gray-50': showPicker }"
class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
@tap="handleTap"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
......@@ -8,8 +8,7 @@
<!-- 触发区域 -->
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3"
:class="{ 'bg-gray-50': showPicker }"
class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
@tap="handleTap"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
......@@ -28,7 +28,7 @@
</div>
<!-- 保额输入 -->
<div class="border border-gray-200 rounded-lg flex items-center overflow-hidden">
<div class="border border-gray-200 rounded-lg flex items-center overflow-hidden bg-gray-50">
<nut-input
:model-value="inputValue"
@input="onInput"
......@@ -225,16 +225,32 @@ const inputValue = ref('')
watch(
() => props.modelValue,
(newVal) => {
// 解析当前 inputValue 为分
const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100)
// 如果输入框有内容且用户正在输入,不覆盖显示值
if (inputValue.value && inputValue.value !== '0.00') {
// 解析当前显示值为分
const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100)
// 如果外部值与当前输入值一致,说明是用户输入触发的更新,不需要重新格式化
if (newVal === currentCents) {
return
}
}
// 只有当值真正改变时才更新输入框(允许用户输入过程中保留 "1." 等中间状态)
if (newVal !== currentCents) {
if (newVal === null || newVal === undefined) {
inputValue.value = ''
// 外部值改变(如重置、从其他地方更新),需要同步显示值
if (newVal === null || newVal === undefined) {
inputValue.value = '0.00'
} else if (newVal === 0) {
inputValue.value = '0.00'
} else {
// 分 -> 元,显示格式
const yuan = newVal / 100
// 判断是否为整数
if (Number.isInteger(yuan)) {
// 整数,不添加小数点
inputValue.value = yuan.toString()
} else {
// 分 -> 元,保留2位小数
inputValue.value = (newVal / 100).toFixed(2)
// 有小数,保留原样
inputValue.value = yuan.toString()
}
}
},
......@@ -266,12 +282,19 @@ const onInput = (val) => {
// 确保 value 为字符串
const valStr = String(value)
// 更新内部显示值(允许用户输入任意合法字符,如小数点)
inputValue.value = valStr
// 移除非数字和小数点(安全处理)
const cleanValue = valStr.replace(/[^\d.]/g, '')
// 如果输入为空或只有小数点,显示 0.00 并重置值为 0
if (cleanValue === '' || cleanValue === '.') {
inputValue.value = '0.00'
emit('update:modelValue', 0)
return
}
// 更新内部显示值(保持用户原始输入,不自动添加小数点)
inputValue.value = valStr
// 转换为分(整数)
const yuan = parseFloat(cleanValue)
if (!Number.isNaN(yuan)) {
......
......@@ -282,22 +282,26 @@ const inputValue = ref('')
const showAmountModal = ref(false)
/**
* 显示值(元,带2位小数
* 显示值(元)
* @type {ComputedRef<string>}
*/
const displayValue = computed(() => {
// 优先显示输入过程中的值(格式化
// 优先显示输入过程中的值(不自动添加小数点
if (inputValue.value) {
// 尝试解析为数字并格式化,如果解析失败则返回原值
const num = parseFloat(inputValue.value)
if (!Number.isNaN(num)) {
return num.toFixed(2)
}
// 直接返回用户输入的值,不自动格式化
return inputValue.value
}
// 如果没有输入值,显示表单的原始值
if (props.modelValue !== null && props.modelValue !== undefined) {
return (props.modelValue / 100).toFixed(2)
const yuan = props.modelValue / 100
// 判断是否为整数
if (Number.isInteger(yuan)) {
// 整数,不添加小数点
return yuan.toString()
} else {
// 有小数,保留原样
return yuan.toString()
}
}
return ''
})
......@@ -313,10 +317,19 @@ const formattedInputValue = computed(() => {
const num = parseFloat(inputValue.value)
if (Number.isNaN(num)) return '0.00'
// 判断是否有小数点
const hasDecimal = inputValue.value.includes('.')
// 格式化为千分位
const parts = num.toFixed(2).split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
if (hasDecimal) {
// 有小数点,保留两位小数并格式化
const parts = num.toFixed(2).split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
} else {
// 没有小数点,只格式化整数部分,不添加 .00
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
})
/**
......@@ -385,7 +398,13 @@ watch(showKeyboard, (newValue, oldValue) => {
const openKeyboard = () => {
// 初始化键盘值为当前值(元),如果没有值则为空
if (props.modelValue !== null && props.modelValue !== undefined) {
inputValue.value = (props.modelValue / 100).toFixed(2)
const yuan = props.modelValue / 100
// 判断是否为整数,不自动添加小数点
if (Number.isInteger(yuan)) {
inputValue.value = yuan.toString()
} else {
inputValue.value = yuan.toString()
}
} else {
inputValue.value = ''
}
......@@ -399,6 +418,15 @@ const openKeyboard = () => {
* @param {string} val - 输入值(单个字符)
*/
const onInput = (val) => {
// 如果输入的是数字,提供震动反馈
if (val >= '0' && val <= '9') {
try {
Taro.vibrateShort()
} catch (err) {
// 某些设备可能不支持震动,忽略错误
}
}
// 如果输入的是小数点,检查是否已经有小数点
if (val === '.' && inputValue.value.includes('.')) {
// 震动反馈 + Toast 提示
......
......@@ -8,8 +8,7 @@
<!-- 触发区域 -->
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3"
:class="{ 'bg-gray-50': showDatePicker }"
class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
@tap="openDatePicker"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
......@@ -8,8 +8,7 @@
<!-- 触发区域 -->
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3"
:class="{ 'bg-gray-50': showDatePicker }"
class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
@tap="openDatePicker"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
......@@ -8,8 +8,7 @@
<!-- 触发区域 -->
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3"
:class="{ 'bg-gray-50': showPicker }"
class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
@tap="openPicker"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
......@@ -8,8 +8,7 @@
<!-- 触发区域 -->
<div
class="flex justify-between items-center border border-gray-200 rounded-lg p-3"
:class="{ 'bg-gray-50': showPicker }"
class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
@tap="openPicker"
>
<span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......