Files
bill/frontend/src/components/common/AmountInput.vue
Jafeng 4ea91a0f59 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

226 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>