diff --git a/DEV.md b/DEV.md
index 4650af6..230dcd1 100644
--- a/DEV.md
+++ b/DEV.md
@@ -119,6 +119,7 @@ onPullDownRefresh(async () => {
- Never 使用 git stash
- Never 切换分支
- Never 过度封装,保持代码简洁明了
+- Never 未确定需求就开始实现
## Requirements
diff --git a/client/src/components/AmountEditor/AmountEditor.vue b/client/src/components/AmountEditor/AmountEditor.vue
new file mode 100644
index 0000000..805992a
--- /dev/null
+++ b/client/src/components/AmountEditor/AmountEditor.vue
@@ -0,0 +1,263 @@
+
+
+
+ ¥
+
+
+
+
+ {{ char }}
+
+
+
+
+ {{ placeholder }}
+
+
+
+ {{ unit }}
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/AmountEditor/amount-edit.ts b/client/src/components/AmountEditor/amount-edit.ts
new file mode 100644
index 0000000..6e085d9
--- /dev/null
+++ b/client/src/components/AmountEditor/amount-edit.ts
@@ -0,0 +1,108 @@
+export type AmountKey = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '00' | '.'
+export type AmountActionKey = AmountKey | 'delete'
+
+export interface AmountEditOptions {
+ max?: number
+ maxLength?: number
+}
+
+export interface AmountEditResult {
+ value: string
+ cursorIndex: number
+ accepted: boolean
+}
+
+const DEFAULT_MAX = 9999999.99
+// 对应最大金额字符串 '9999999.99' 的长度。
+const DEFAULT_MAX_LENGTH = 10
+const MAX_DECIMAL_PLACES = 2
+
+export function clampCursorIndex(value: string, cursorIndex: number): number {
+ if (cursorIndex < 0) return 0
+ if (cursorIndex > value.length) return value.length
+ return cursorIndex
+}
+
+export function insertAmountKey(
+ value: string,
+ cursorIndex: number,
+ key: AmountKey,
+ options: AmountEditOptions = {}
+): AmountEditResult {
+ const safeCursor = clampCursorIndex(value, cursorIndex)
+
+ // 替换模式:当当前值只有一个 '0' 且光标在末尾时,非 '.' 输入应替换 '0'
+ if (value === '0' && safeCursor === value.length && key !== '.') {
+ // key 可能是 '00',需要规范化
+ const insert = normalizeInsert('', 0, key)
+ if (insert === '') return { value, cursorIndex: safeCursor, accepted: true }
+ const next = insert
+ if (!isValidAmountInput(next, options)) {
+ return { value, cursorIndex: safeCursor, accepted: false }
+ }
+ return {
+ value: next,
+ cursorIndex: insert.length,
+ accepted: true,
+ }
+ }
+
+ const normalizedInsert = normalizeInsert(value, safeCursor, key)
+ const next = value.slice(0, safeCursor) + normalizedInsert + value.slice(safeCursor)
+
+ if (!isValidAmountInput(next, options)) {
+ return { value, cursorIndex: safeCursor, accepted: false }
+ }
+
+ return {
+ value: next,
+ cursorIndex: safeCursor + normalizedInsert.length,
+ accepted: true,
+ }
+}
+
+export function deleteAmountChar(value: string, cursorIndex: number): AmountEditResult {
+ const safeCursor = clampCursorIndex(value, cursorIndex)
+ if (safeCursor === 0) {
+ return { value, cursorIndex: safeCursor, accepted: false }
+ }
+
+ return {
+ value: value.slice(0, safeCursor - 1) + value.slice(safeCursor),
+ cursorIndex: safeCursor - 1,
+ accepted: true,
+ }
+}
+
+export function applyAmountKey(
+ value: string,
+ cursorIndex: number,
+ key: AmountActionKey,
+ options: AmountEditOptions = {}
+): AmountEditResult {
+ if (key === 'delete') return deleteAmountChar(value, cursorIndex)
+ return insertAmountKey(value, cursorIndex, key, options)
+}
+
+export function isValidAmountInput(value: string, options: AmountEditOptions = {}): boolean {
+ if (value === '') return true
+ if (value.length > (options.maxLength ?? DEFAULT_MAX_LENGTH)) return false
+ if (!/^\d*(\.\d*)?$/.test(value)) return false
+
+ const dotIndex = value.indexOf('.')
+ if (dotIndex >= 0 && value.length - dotIndex - 1 > MAX_DECIMAL_PLACES) return false
+
+ const num = parseFloat(value)
+ if (!Number.isNaN(num) && num > (options.max ?? DEFAULT_MAX)) return false
+
+ return true
+}
+
+function normalizeInsert(value: string, cursorIndex: number, key: AmountKey): string {
+ if ((key === '0' || key === '00') && value === '') return '0'
+ if ((key === '0' || key === '00') && value === '0' && cursorIndex === value.length) return ''
+ if (value === '0' && cursorIndex === value.length && key !== '.') return key
+ // 空值时输入小数点,补前导零,保证合法格式
+ if (value === '' && key === '.') return '0.'
+ return key
+}
\ No newline at end of file
diff --git a/client/src/components/Numpad/Numpad.vue b/client/src/components/Numpad/Numpad.vue
index 9503ef5..11f1b47 100644
--- a/client/src/components/Numpad/Numpad.vue
+++ b/client/src/components/Numpad/Numpad.vue
@@ -1,9 +1,15 @@
-
+
{{ key }}
-
+
@@ -16,76 +22,118 @@
@@ -463,18 +482,7 @@ function goBack() { uni.navigateBack() }
}
.income .slider { transform: translateX(140rpx); }
-/* 金额 */
-.amount-row {
- text-align: center; padding: $space-sm 0 $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: 72rpx; font-weight: 600; color: $text;
- &.placeholder { color: #D0C4C4; }
-}
+
/* 分类 */
.category-scroll { margin-bottom: $space-md; }