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 @@ ...@@ -8,8 +8,7 @@
8 8
9 <!-- 触发区域 --> 9 <!-- 触发区域 -->
10 <div 10 <div
11 - class="flex justify-between items-center border border-gray-200 rounded-lg p-3" 11 + class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
12 - :class="{ 'bg-gray-50': showPicker }"
13 @tap="handleTap" 12 @tap="handleTap"
14 > 13 >
15 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> 14 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
8 8
9 <!-- 触发区域 --> 9 <!-- 触发区域 -->
10 <div 10 <div
11 - class="flex justify-between items-center border border-gray-200 rounded-lg p-3" 11 + class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
12 - :class="{ 'bg-gray-50': showPicker }"
13 @tap="handleTap" 12 @tap="handleTap"
14 > 13 >
15 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> 14 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
28 </div> 28 </div>
29 29
30 <!-- 保额输入 --> 30 <!-- 保额输入 -->
31 - <div class="border border-gray-200 rounded-lg flex items-center overflow-hidden"> 31 + <div class="border border-gray-200 rounded-lg flex items-center overflow-hidden bg-gray-50">
32 <nut-input 32 <nut-input
33 :model-value="inputValue" 33 :model-value="inputValue"
34 @input="onInput" 34 @input="onInput"
...@@ -225,16 +225,32 @@ const inputValue = ref('') ...@@ -225,16 +225,32 @@ const inputValue = ref('')
225 watch( 225 watch(
226 () => props.modelValue, 226 () => props.modelValue,
227 (newVal) => { 227 (newVal) => {
228 - // 解析当前 inputValue 为分 228 + // 如果输入框有内容且用户正在输入,不覆盖显示值
229 + if (inputValue.value && inputValue.value !== '0.00') {
230 + // 解析当前显示值为分
229 const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100) 231 const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100)
230 232
231 - // 只有当值真正改变时才更新输入框(允许用户输入过程中保留 "1." 等中间状态) 233 + // 如果外部值与当前输入值一致,说明是用户输入触发的更新,不需要重新格式化
232 - if (newVal !== currentCents) { 234 + if (newVal === currentCents) {
235 + return
236 + }
237 + }
238 +
239 + // 外部值改变(如重置、从其他地方更新),需要同步显示值
233 if (newVal === null || newVal === undefined) { 240 if (newVal === null || newVal === undefined) {
234 - inputValue.value = '' 241 + inputValue.value = '0.00'
242 + } else if (newVal === 0) {
243 + inputValue.value = '0.00'
235 } else { 244 } else {
236 - // 分 -> 元,保留2位小数 245 + // 分 -> 元,显示格式
237 - inputValue.value = (newVal / 100).toFixed(2) 246 + const yuan = newVal / 100
247 + // 判断是否为整数
248 + if (Number.isInteger(yuan)) {
249 + // 整数,不添加小数点
250 + inputValue.value = yuan.toString()
251 + } else {
252 + // 有小数,保留原样
253 + inputValue.value = yuan.toString()
238 } 254 }
239 } 255 }
240 }, 256 },
...@@ -266,12 +282,19 @@ const onInput = (val) => { ...@@ -266,12 +282,19 @@ const onInput = (val) => {
266 // 确保 value 为字符串 282 // 确保 value 为字符串
267 const valStr = String(value) 283 const valStr = String(value)
268 284
269 - // 更新内部显示值(允许用户输入任意合法字符,如小数点)
270 - inputValue.value = valStr
271 -
272 // 移除非数字和小数点(安全处理) 285 // 移除非数字和小数点(安全处理)
273 const cleanValue = valStr.replace(/[^\d.]/g, '') 286 const cleanValue = valStr.replace(/[^\d.]/g, '')
274 287
288 + // 如果输入为空或只有小数点,显示 0.00 并重置值为 0
289 + if (cleanValue === '' || cleanValue === '.') {
290 + inputValue.value = '0.00'
291 + emit('update:modelValue', 0)
292 + return
293 + }
294 +
295 + // 更新内部显示值(保持用户原始输入,不自动添加小数点)
296 + inputValue.value = valStr
297 +
275 // 转换为分(整数) 298 // 转换为分(整数)
276 const yuan = parseFloat(cleanValue) 299 const yuan = parseFloat(cleanValue)
277 if (!Number.isNaN(yuan)) { 300 if (!Number.isNaN(yuan)) {
......
...@@ -282,22 +282,26 @@ const inputValue = ref('') ...@@ -282,22 +282,26 @@ const inputValue = ref('')
282 const showAmountModal = ref(false) 282 const showAmountModal = ref(false)
283 283
284 /** 284 /**
285 - * 显示值(元,带2位小数 285 + * 显示值(元)
286 * @type {ComputedRef<string>} 286 * @type {ComputedRef<string>}
287 */ 287 */
288 const displayValue = computed(() => { 288 const displayValue = computed(() => {
289 - // 优先显示输入过程中的值(格式化 289 + // 优先显示输入过程中的值(不自动添加小数点
290 if (inputValue.value) { 290 if (inputValue.value) {
291 - // 尝试解析为数字并格式化,如果解析失败则返回原值 291 + // 直接返回用户输入的值,不自动格式化
292 - const num = parseFloat(inputValue.value)
293 - if (!Number.isNaN(num)) {
294 - return num.toFixed(2)
295 - }
296 return inputValue.value 292 return inputValue.value
297 } 293 }
298 // 如果没有输入值,显示表单的原始值 294 // 如果没有输入值,显示表单的原始值
299 if (props.modelValue !== null && props.modelValue !== undefined) { 295 if (props.modelValue !== null && props.modelValue !== undefined) {
300 - return (props.modelValue / 100).toFixed(2) 296 + const yuan = props.modelValue / 100
297 + // 判断是否为整数
298 + if (Number.isInteger(yuan)) {
299 + // 整数,不添加小数点
300 + return yuan.toString()
301 + } else {
302 + // 有小数,保留原样
303 + return yuan.toString()
304 + }
301 } 305 }
302 return '' 306 return ''
303 }) 307 })
...@@ -313,10 +317,19 @@ const formattedInputValue = computed(() => { ...@@ -313,10 +317,19 @@ const formattedInputValue = computed(() => {
313 const num = parseFloat(inputValue.value) 317 const num = parseFloat(inputValue.value)
314 if (Number.isNaN(num)) return '0.00' 318 if (Number.isNaN(num)) return '0.00'
315 319
320 + // 判断是否有小数点
321 + const hasDecimal = inputValue.value.includes('.')
322 +
316 // 格式化为千分位 323 // 格式化为千分位
324 + if (hasDecimal) {
325 + // 有小数点,保留两位小数并格式化
317 const parts = num.toFixed(2).split('.') 326 const parts = num.toFixed(2).split('.')
318 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') 327 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
319 return parts.join('.') 328 return parts.join('.')
329 + } else {
330 + // 没有小数点,只格式化整数部分,不添加 .00
331 + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
332 + }
320 }) 333 })
321 334
322 /** 335 /**
...@@ -385,7 +398,13 @@ watch(showKeyboard, (newValue, oldValue) => { ...@@ -385,7 +398,13 @@ watch(showKeyboard, (newValue, oldValue) => {
385 const openKeyboard = () => { 398 const openKeyboard = () => {
386 // 初始化键盘值为当前值(元),如果没有值则为空 399 // 初始化键盘值为当前值(元),如果没有值则为空
387 if (props.modelValue !== null && props.modelValue !== undefined) { 400 if (props.modelValue !== null && props.modelValue !== undefined) {
388 - inputValue.value = (props.modelValue / 100).toFixed(2) 401 + const yuan = props.modelValue / 100
402 + // 判断是否为整数,不自动添加小数点
403 + if (Number.isInteger(yuan)) {
404 + inputValue.value = yuan.toString()
405 + } else {
406 + inputValue.value = yuan.toString()
407 + }
389 } else { 408 } else {
390 inputValue.value = '' 409 inputValue.value = ''
391 } 410 }
...@@ -399,6 +418,15 @@ const openKeyboard = () => { ...@@ -399,6 +418,15 @@ const openKeyboard = () => {
399 * @param {string} val - 输入值(单个字符) 418 * @param {string} val - 输入值(单个字符)
400 */ 419 */
401 const onInput = (val) => { 420 const onInput = (val) => {
421 + // 如果输入的是数字,提供震动反馈
422 + if (val >= '0' && val <= '9') {
423 + try {
424 + Taro.vibrateShort()
425 + } catch (err) {
426 + // 某些设备可能不支持震动,忽略错误
427 + }
428 + }
429 +
402 // 如果输入的是小数点,检查是否已经有小数点 430 // 如果输入的是小数点,检查是否已经有小数点
403 if (val === '.' && inputValue.value.includes('.')) { 431 if (val === '.' && inputValue.value.includes('.')) {
404 // 震动反馈 + Toast 提示 432 // 震动反馈 + Toast 提示
......
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
8 8
9 <!-- 触发区域 --> 9 <!-- 触发区域 -->
10 <div 10 <div
11 - class="flex justify-between items-center border border-gray-200 rounded-lg p-3" 11 + class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
12 - :class="{ 'bg-gray-50': showDatePicker }"
13 @tap="openDatePicker" 12 @tap="openDatePicker"
14 > 13 >
15 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> 14 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
8 8
9 <!-- 触发区域 --> 9 <!-- 触发区域 -->
10 <div 10 <div
11 - class="flex justify-between items-center border border-gray-200 rounded-lg p-3" 11 + class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
12 - :class="{ 'bg-gray-50': showDatePicker }"
13 @tap="openDatePicker" 12 @tap="openDatePicker"
14 > 13 >
15 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> 14 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
8 8
9 <!-- 触发区域 --> 9 <!-- 触发区域 -->
10 <div 10 <div
11 - class="flex justify-between items-center border border-gray-200 rounded-lg p-3" 11 + class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
12 - :class="{ 'bg-gray-50': showPicker }"
13 @tap="openPicker" 12 @tap="openPicker"
14 > 13 >
15 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> 14 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
8 8
9 <!-- 触发区域 --> 9 <!-- 触发区域 -->
10 <div 10 <div
11 - class="flex justify-between items-center border border-gray-200 rounded-lg p-3" 11 + class="flex justify-between items-center border border-gray-200 rounded-lg p-3 bg-gray-50"
12 - :class="{ 'bg-gray-50': showPicker }"
13 @tap="openPicker" 12 @tap="openPicker"
14 > 13 >
15 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm"> 14 <span :class="displayValue ? 'text-gray-900' : 'text-gray-400'" class="text-sm">
......