2025-11-01 09:24:26 +08:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, reactive, ref } from 'vue';
|
|
|
|
|
import LucideIcon from '../../../components/common/LucideIcon.vue';
|
|
|
|
|
import TransactionItem from '../../../components/transactions/TransactionItem.vue';
|
|
|
|
|
import {
|
|
|
|
|
useTransactionsQuery,
|
|
|
|
|
useCreateTransactionMutation,
|
|
|
|
|
useDeleteTransactionMutation
|
|
|
|
|
} from '../../../composables/useTransactions';
|
|
|
|
|
import type { TransactionPayload } from '../../../types/transaction';
|
|
|
|
|
|
|
|
|
|
type FilterOption = 'all' | 'expense' | 'income';
|
|
|
|
|
|
|
|
|
|
const filter = ref<FilterOption>('all');
|
|
|
|
|
const showSheet = ref(false);
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
title: '',
|
|
|
|
|
amount: '',
|
|
|
|
|
category: '',
|
|
|
|
|
type: 'expense',
|
|
|
|
|
notes: '',
|
|
|
|
|
source: 'manual'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: transactions } = useTransactionsQuery();
|
|
|
|
|
const createTransaction = useCreateTransactionMutation();
|
|
|
|
|
const deleteTransaction = useDeleteTransactionMutation();
|
|
|
|
|
|
|
|
|
|
const filters: Array<{ label: string; value: FilterOption }> = [
|
|
|
|
|
{ label: '全部', value: 'all' },
|
|
|
|
|
{ label: '支出', value: 'expense' },
|
|
|
|
|
{ label: '收入', value: 'income' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const filteredTransactions = computed(() => {
|
|
|
|
|
if (!transactions.value) return [];
|
|
|
|
|
if (filter.value === 'all') return transactions.value;
|
|
|
|
|
return transactions.value.filter((txn) => txn.type === filter.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const isSaving = computed(() => createTransaction.isPending.value);
|
|
|
|
|
|
|
|
|
|
const setFilter = (value: FilterOption) => {
|
|
|
|
|
filter.value = value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
form.title = '';
|
|
|
|
|
form.amount = '';
|
|
|
|
|
form.category = '';
|
|
|
|
|
form.type = 'expense';
|
|
|
|
|
form.source = 'manual';
|
|
|
|
|
form.notes = '';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
if (!form.title || !form.amount) return;
|
|
|
|
|
const payload: TransactionPayload = {
|
|
|
|
|
title: form.title,
|
|
|
|
|
amount: Math.abs(Number(form.amount)),
|
|
|
|
|
category: form.category || '未分类',
|
|
|
|
|
type: form.type as TransactionPayload['type'],
|
|
|
|
|
source: form.source as TransactionPayload['source'],
|
|
|
|
|
status: 'pending',
|
|
|
|
|
currency: 'CNY',
|
|
|
|
|
notes: form.notes || undefined,
|
|
|
|
|
occurredAt: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
await createTransaction.mutateAsync(payload);
|
|
|
|
|
showSheet.value = false;
|
|
|
|
|
resetForm();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeTransaction = async (id: string) => {
|
|
|
|
|
await deleteTransaction.mutateAsync(id);
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="p-6 pt-10 pb-24 space-y-6">
|
|
|
|
|
<header class="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-sm text-gray-500">轻松管理你的收支</p>
|
|
|
|
|
<h1 class="text-3xl font-bold text-gray-900">交易记录</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
class="px-4 py-2 bg-indigo-500 text-white rounded-xl font-medium hover:bg-indigo-600"
|
|
|
|
|
@click="showSheet = true"
|
|
|
|
|
>
|
|
|
|
|
新增
|
|
|
|
|
</button>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="bg-white rounded-2xl p-2 flex space-x-2 border border-gray-100">
|
|
|
|
|
<button
|
|
|
|
|
v-for="item in filters"
|
|
|
|
|
:key="item.value"
|
|
|
|
|
class="flex-1 py-2 rounded-lg text-sm font-medium"
|
|
|
|
|
:class="filter === item.value ? 'bg-indigo-500 text-white' : 'text-gray-500'"
|
|
|
|
|
@click="setFilter(item.value)"
|
|
|
|
|
>
|
|
|
|
|
{{ item.label }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<div
|
|
|
|
|
v-for="transaction in filteredTransactions"
|
|
|
|
|
:key="transaction.id"
|
|
|
|
|
class="relative group"
|
|
|
|
|
>
|
|
|
|
|
<TransactionItem :transaction="transaction" />
|
|
|
|
|
<button
|
|
|
|
|
class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-red-500"
|
|
|
|
|
@click="removeTransaction(transaction.id)"
|
|
|
|
|
>
|
|
|
|
|
<LucideIcon name="trash-2" :size="18" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-if="showSheet"
|
|
|
|
|
class="fixed inset-0 bg-black/40 flex items-end justify-center"
|
|
|
|
|
@click.self="showSheet = false"
|
|
|
|
|
>
|
2025-11-01 11:39:29 +08:00
|
|
|
<div class="w-full max-w-sm bg-white rounded-t-3xl p-6 space-y-4">
|
2025-11-01 09:24:26 +08:00
|
|
|
<div class="flex justify-between items-center">
|
|
|
|
|
<h2 class="text-lg font-semibold text-gray-900">快速记一笔</h2>
|
|
|
|
|
<button @click="showSheet = false">
|
|
|
|
|
<LucideIcon name="x" :size="24" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
<label class="flex flex-col text-sm font-medium text-gray-700 space-y-2">
|
|
|
|
|
标题
|
|
|
|
|
<input v-model="form.title" type="text" class="px-4 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
|
|
|
</label>
|
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
|
|
<label class="flex flex-col text-sm font-medium text-gray-700 space-y-2">
|
|
|
|
|
金额
|
|
|
|
|
<input v-model="form.amount" type="number" min="0" class="px-4 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
|
|
|
</label>
|
|
|
|
|
<label class="flex flex-col text-sm font-medium text-gray-700 space-y-2">
|
|
|
|
|
类型
|
|
|
|
|
<select v-model="form.type" class="px-4 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
|
|
|
|
<option value="expense">支出</option>
|
|
|
|
|
<option value="income">收入</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<label class="flex flex-col text-sm font-medium text-gray-700 space-y-2">
|
|
|
|
|
分类
|
|
|
|
|
<input v-model="form.category" type="text" placeholder="如:餐饮" class="px-4 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
|
|
|
</label>
|
|
|
|
|
<label class="flex flex-col text-sm font-medium text-gray-700 space-y-2">
|
|
|
|
|
来源
|
|
|
|
|
<select v-model="form.source" class="px-4 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
|
|
|
|
<option value="manual">手动</option>
|
|
|
|
|
<option value="notification">通知自动</option>
|
|
|
|
|
<option value="ocr">票据识别</option>
|
|
|
|
|
<option value="ai">AI 推荐</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<label class="flex flex-col text-sm font-medium text-gray-700 space-y-2">
|
|
|
|
|
备注
|
|
|
|
|
<textarea v-model="form.notes" rows="2" class="px-4 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500" />
|
|
|
|
|
</label>
|
|
|
|
|
<button
|
|
|
|
|
class="w-full bg-indigo-500 text-white py-3 rounded-xl font-semibold hover:bg-indigo-600 disabled:opacity-60"
|
|
|
|
|
:disabled="isSaving"
|
|
|
|
|
@click="submit"
|
|
|
|
|
>
|
|
|
|
|
{{ isSaving ? '保存中...' : '保存' }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|