fix(plan): 优化金额输入组件体验和样式
- 所有输入框组件添加灰色背景(bg-gray-50) - AmountInput 和 AmountKeyboard 添加数字输入震动反馈 - 修复金额显示自动添加小数点问题(输入12显示12而非12.00) - 优化 AmountKeyboard 弹窗和键盘打开时的值初始化 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
8 changed files
with
81 additions
and
36 deletions
| ... | @@ -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 | - const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100) | 229 | + if (inputValue.value && inputValue.value !== '0.00') { |
| 230 | + // 解析当前显示值为分 | ||
| 231 | + const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100) | ||
| 232 | + | ||
| 233 | + // 如果外部值与当前输入值一致,说明是用户输入触发的更新,不需要重新格式化 | ||
| 234 | + if (newVal === currentCents) { | ||
| 235 | + return | ||
| 236 | + } | ||
| 237 | + } | ||
| 230 | 238 | ||
| 231 | - // 只有当值真正改变时才更新输入框(允许用户输入过程中保留 "1." 等中间状态) | 239 | + // 外部值改变(如重置、从其他地方更新),需要同步显示值 |
| 232 | - if (newVal !== currentCents) { | 240 | + if (newVal === null || newVal === undefined) { |
| 233 | - if (newVal === null || newVal === undefined) { | 241 | + inputValue.value = '0.00' |
| 234 | - inputValue.value = '' | 242 | + } else if (newVal === 0) { |
| 243 | + inputValue.value = '0.00' | ||
| 244 | + } else { | ||
| 245 | + // 分 -> 元,显示格式 | ||
| 246 | + const yuan = newVal / 100 | ||
| 247 | + // 判断是否为整数 | ||
| 248 | + if (Number.isInteger(yuan)) { | ||
| 249 | + // 整数,不添加小数点 | ||
| 250 | + inputValue.value = yuan.toString() | ||
| 235 | } else { | 251 | } else { |
| 236 | - // 分 -> 元,保留2位小数 | 252 | + // 有小数,保留原样 |
| 237 | - inputValue.value = (newVal / 100).toFixed(2) | 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 | // 格式化为千分位 |
| 317 | - const parts = num.toFixed(2).split('.') | 324 | + if (hasDecimal) { |
| 318 | - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') | 325 | + // 有小数点,保留两位小数并格式化 |
| 319 | - return parts.join('.') | 326 | + const parts = num.toFixed(2).split('.') |
| 327 | + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') | ||
| 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"> | ... | ... |
-
Please register or login to post a comment