feat: 金额编辑器交互优化 — 支持光标定位、插入删除、长按删除

- 新增 AmountEditor 组件,页面只接一个组件即可获得完整金额编辑体验
- Numpad 增强:支持 cursorIndex 光标输入 + 长按连续删除
- 金额编辑纯函数 amount-edit.ts 集中管理插入/删除/校验规则
- 记一笔、预算页、周期账单弹窗统一接入 AmountEditor
- 默认聚焦金额、点击备注/日期/分类隐藏 Numpad、再次点击金额恢复
- 空值输入小数点规范为 0.
This commit is contained in:
2026-06-09 14:09:14 +08:00
parent ac20a70a20
commit 768c72d7b7
7 changed files with 583 additions and 188 deletions

View File

@@ -19,13 +19,16 @@
</view>
</view>
<view class="amount-display">
<text class="currency">¥</text>
<text class="digits">{{ displayAmount }}</text>
<text class="cursor">|</text>
</view>
<AmountEditor
ref="amountEditorRef"
v-model="amountStr"
:fixed="true"
size="large"
:auto-focus="true"
@confirm="save"
/>
<scroll-view class="category-scroll" scroll-y :scroll-into-view="scrollToCat">
<scroll-view class="category-scroll" scroll-y :scroll-into-view="scrollToCat" @tap="blurAmountEditor">
<view class="category-grid">
<view v-for="cat in currentCategories" :key="cat.id" :id="'cat-' + cat.id" class="cat-item" :class="{ suggested: suggestedCatId === cat.id && selectedCat !== cat.id }" @tap="selectedCat = cat.id">
<view class="cat-icon-wrap">
@@ -41,18 +44,18 @@
<view class="extra-fields">
<picker mode="date" :value="selectedDate" :end="todayStr" @change="onDateChange">
<view class="extra-row">
<view class="extra-row" @tap="blurAmountEditor">
<Icon name="calendar" :size="28" color="#8B7E7E" />
<text class="extra-text">{{ dateLabel }}</text>
</view>
</picker>
<view class="extra-row">
<Icon name="edit" :size="28" color="#8B7E7E" />
<input class="extra-input" v-model="note" placeholder="添加备注..." placeholder-class="placeholder" maxlength="200" />
<input class="extra-input" v-model="note" placeholder="添加备注..." placeholder-class="placeholder" maxlength="200" @focus="blurAmountEditor" />
</view>
</view>
<Numpad v-model="amountStr" @confirm="save" />
<SaveSuccess
v-model:visible="showSuccess"
@@ -71,7 +74,7 @@ import { useCategoryStore } from '@/stores/category'
import { useTransactionStore } from '@/stores/transaction'
import { waitForReady } from '@/utils/app-ready'
import { suggestCategory } from '@/api/stats'
import Numpad from '@/components/Numpad/Numpad.vue'
import AmountEditor from '@/components/AmountEditor/AmountEditor.vue'
import CategoryIcon from '@/components/CategoryIcon/CategoryIcon.vue'
import Icon from '@/components/Icon/Icon.vue'
import SaveSuccess from '@/components/SaveSuccess/SaveSuccess.vue'
@@ -95,6 +98,7 @@ const editId = ref<number | null>(null)
const showSuccess = ref(false)
const scrollToCat = ref('')
const todayStr = localToday
const amountEditorRef = ref<InstanceType<typeof AmountEditor> | null>(null)
// Remember last selected category per type
const lastCatByType: Record<string, number | null> = { expense: null, income: null }
@@ -103,10 +107,6 @@ const dateLabel = computed(() => {
return `${d.getMonth() + 1}${d.getDate()}`
})
const displayAmount = computed(() => {
if (!amountStr.value) return ''
return amountStr.value
})
const currentCategories = computed(() => catStore.getByType(type.value))
@@ -217,6 +217,10 @@ function onDateChange(e: any) {
selectedDate.value = e.detail.value
}
function blurAmountEditor() {
amountEditorRef.value?.blur()
}
async function save() {
if (saving.value) return
const amount = Math.round(parseFloat(amountStr.value || '0') * 100)
@@ -261,6 +265,7 @@ function onContinue() {
amountStr.value = ''
note.value = ''
showSuccess.value = false
nextTick(() => amountEditorRef.value?.focus())
}
/** 返回上一页 */
@@ -290,10 +295,14 @@ function goBack() {
@import '@/styles/mixins.scss';
.page {
@include page-base;
min-height: 100vh;
background: $bg;
display: flex;
flex-direction: column;
padding-bottom: 0;
// AmountEditor fixed Numpad 占位: 4行×96rpx + 3间隙×16rpx + padding×2×16rpx = 464rpx
$numpad-h: 464rpx;
padding-bottom: calc(#{$numpad-h} + constant(safe-area-inset-bottom));
padding-bottom: calc(#{$numpad-h} + env(safe-area-inset-bottom));
}
.header-fixed {
@@ -358,38 +367,6 @@ function goBack() {
.income .slider { transform: translateX(160rpx); }
.amount-display {
text-align: center;
padding: $space-lg $space-xl $space-md;
}
.currency {
font-family: 'Fredoka', sans-serif;
font-size: $space-2xl;
font-weight: 500;
color: $text-muted;
margin-right: $space-xs;
}
.digits {
font-family: 'Fredoka', sans-serif;
font-size: 88rpx;
font-weight: 700;
color: $text;
font-variant-numeric: tabular-nums;
}
.cursor {
font-family: 'Fredoka', monospace;
font-size: 88rpx;
font-weight: 300;
color: $primary;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
.category-scroll {
max-height: 320rpx;
@@ -472,10 +449,6 @@ function goBack() {
.placeholder { color: $text-muted; }
@media (prefers-reduced-motion: reduce) {
.cursor {
animation: none;
opacity: 1;
}
}
</style>