任务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 设计风格 - 响应式布局和移动端优化 - 智能搜索和多维度筛选 - 用户友好的交互体验 - 完整的表单验证和错误处理
226 lines
5.6 KiB
Vue
226 lines
5.6 KiB
Vue
<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> |