Files
AI-Bill/apps/frontend/src/features/transactions/pages/TransactionsPage.vue

182 lines
6.5 KiB
Vue
Raw Normal View History

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>