Files
bill/frontend/src/components/common/AmountInput.vue

226 lines
5.6 KiB
Vue
Raw Normal View History

feat: 完成任务3 - 开发核心交易管理界面 任务3.1 实现基础 UI 组件库 - 创建 BaseIcon 组件,支持 emoji 和 SVG 图标 - 实现 CategoryIcon 组件,用于分类图标显示 - 开发 AmountInput 组件,支持金额输入和类型切换 - 创建 DatePicker 组件,支持日期选择和快捷日期 - 实现 BaseSelect 组件,支持搜索和自定义选项 - 添加 LoadingSpinner 和 EmptyState 组件 - 扩展 Tailwind 配置,完善设计系统色彩 任务3.2 创建交易列表和卡片组件 - 实现 TransactionCard 组件,展示交易详情 - 创建 TransactionList 组件,支持分组显示和批量操作 - 添加日期分组和汇总功能 - 实现无限滚动和加载更多功能 - 支持选择模式和批量删除 - 更新 TransactionsView 使用新组件 任务3.3 开发交易添加和编辑表单 - 创建 TransactionForm 组件,支持添加和编辑交易 - 集成所有输入组件(金额、分类、账户、日期等) - 实现表单验证和数据清理 - 添加重复交易检测功能 - 创建 AddTransactionView 页面 - 配置路由支持添加和编辑交易 任务3.4 实现搜索和筛选功能 - 创建 TransactionSearch 组件,支持智能搜索 - 实现搜索建议和搜索历史功能 - 开发 TransactionFilter 组件,支持多维度筛选 - 添加快速筛选标签和日期预设 - 集成搜索和筛选到 TransactionsView - 实现实时筛选和结果统计 核心特性: - 完整的交易管理界面(列表、添加、编辑、搜索、筛选) - 现代化的 neumorphic 设计风格 - 响应式布局和移动端优化 - 智能搜索和多维度筛选 - 用户友好的交互体验 - 完整的表单验证和错误处理
2025-08-14 16:03:02 +08:00
<template>
<div class="space-y-2">
<label v-if="label" :for="inputId" class="block text-sm font-medium text-text-primary">
{{ label }}
</label>
<div class="relative">
<!-- 货币符号 -->
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-text-secondary text-lg">{{ currency }}</span>
</div>
<!-- 金额输入框 -->
<input
:id="inputId"
ref="inputRef"
type="text"
inputmode="decimal"
:placeholder="placeholder"
:value="displayValue"
:disabled="disabled"
:class="[
'input-neumorphic pl-8 pr-4 text-right text-lg font-semibold',
{
'text-accent-red': type === 'expense' && numericValue > 0,
'text-accent-green': type === 'income' && numericValue > 0,
'text-text-primary': numericValue === 0
}
]"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeydown"
/>
<!-- 类型切换按钮 -->
<div v-if="showTypeToggle" class="absolute inset-y-0 right-0 pr-3 flex items-center">
<button
type="button"
:class="[
'text-xs px-2 py-1 rounded transition-colors',
type === 'expense'
? 'bg-accent-red text-white'
: 'bg-accent-green text-white'
]"
@click="toggleType"
>
{{ type === 'expense' ? '支出' : '收入' }}
</button>
</div>
</div>
<!-- 快捷金额按钮 -->
<div v-if="showQuickAmounts && !disabled" class="flex flex-wrap gap-2 mt-3">
<button
v-for="amount in quickAmounts"
:key="amount"
type="button"
class="px-3 py-1 text-sm bg-surface border border-border rounded-lg hover:bg-primary hover:text-white transition-colors"
@click="setQuickAmount(amount)"
>
{{ currency }}{{ amount }}
</button>
</div>
<!-- 错误提示 -->
<div v-if="error" class="text-sm text-accent-red">
{{ error }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
interface Props {
modelValue: number
type?: 'income' | 'expense'
label?: string
placeholder?: string
currency?: string
disabled?: boolean
showTypeToggle?: boolean
showQuickAmounts?: boolean
quickAmounts?: number[]
max?: number
min?: number
error?: string
}
const props = withDefaults(defineProps<Props>(), {
type: 'expense',
currency: '¥',
disabled: false,
showTypeToggle: false,
showQuickAmounts: false,
quickAmounts: () => [10, 20, 50, 100, 200, 500],
max: 999999999,
min: 0
})
const emit = defineEmits<{
'update:modelValue': [value: number]
'update:type': [type: 'income' | 'expense']
}>()
const inputRef = ref<HTMLInputElement>()
const isFocused = ref(false)
// 生成唯一ID
const inputId = computed(() => `amount-input-${Math.random().toString(36).substr(2, 9)}`)
// 数值
const numericValue = computed(() => props.modelValue || 0)
// 显示值
const displayValue = computed(() => {
if (isFocused.value) {
// 聚焦时显示原始数字
return numericValue.value > 0 ? numericValue.value.toString() : ''
} else {
// 失焦时显示格式化的数字
return numericValue.value > 0 ? formatAmount(numericValue.value) : ''
}
})
// 格式化金额显示
function formatAmount(amount: number): string {
return amount.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
})
}
// 解析输入值
function parseAmount(value: string): number {
// 移除所有非数字和小数点的字符
const cleaned = value.replace(/[^\d.]/g, '')
// 确保只有一个小数点
const parts = cleaned.split('.')
if (parts.length > 2) {
return parseFloat(parts[0] + '.' + parts.slice(1).join(''))
}
const parsed = parseFloat(cleaned)
return isNaN(parsed) ? 0 : parsed
}
// 处理输入
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
const value = target.value
// 解析并验证输入值
let amount = parseAmount(value)
// 应用最小值和最大值限制
if (amount < props.min) amount = props.min
if (amount > props.max) amount = props.max
emit('update:modelValue', amount)
}
// 处理聚焦
function handleFocus() {
isFocused.value = true
}
// 处理失焦
function handleBlur() {
isFocused.value = false
}
// 处理键盘事件
function handleKeydown(event: KeyboardEvent) {
// 允许的键数字、小数点、退格、删除、方向键、Tab
const allowedKeys = [
'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'
]
const isNumber = /^[0-9]$/.test(event.key)
const isDecimal = event.key === '.' && !displayValue.value.includes('.')
if (!allowedKeys.includes(event.key) && !isNumber && !isDecimal) {
event.preventDefault()
}
// Enter 键失焦
if (event.key === 'Enter') {
inputRef.value?.blur()
}
}
// 切换类型
function toggleType() {
const newType = props.type === 'expense' ? 'income' : 'expense'
emit('update:type', newType)
}
// 设置快捷金额
function setQuickAmount(amount: number) {
emit('update:modelValue', amount)
// 聚焦输入框
nextTick(() => {
inputRef.value?.focus()
})
}
// 聚焦方法(供父组件调用)
function focus() {
inputRef.value?.focus()
}
// 选中所有文本
function selectAll() {
inputRef.value?.select()
}
defineExpose({
focus,
selectAll
})
</script>