feat:添加登录功能

This commit is contained in:
2025-11-10 13:58:06 +08:00
parent 49f9b28091
commit 2e2caeaab5
36 changed files with 1076 additions and 458 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
import LucideIcon from '../../../components/common/LucideIcon.vue';
import TransactionItem from '../../../components/transactions/TransactionItem.vue';
import {
@@ -23,9 +23,10 @@ const form = reactive({
source: 'manual'
});
const { data: transactions } = useTransactionsQuery();
const transactionsQuery = useTransactionsQuery();
const createTransaction = useCreateTransactionMutation();
const deleteTransaction = useDeleteTransactionMutation();
const transactions = computed(() => transactionsQuery.data.value ?? []);
const filters: Array<{ label: string; value: FilterOption }> = [
{ label: '全部', value: 'all' },
@@ -34,12 +35,33 @@ const filters: Array<{ label: string; value: FilterOption }> = [
];
const filteredTransactions = computed(() => {
if (!transactions.value) return [];
if (!transactions.value.length) return [];
if (filter.value === 'all') return transactions.value;
return transactions.value.filter((txn) => txn.type === filter.value);
});
const isSaving = computed(() => createTransaction.isPending.value);
const isInitialLoading = computed(() => transactionsQuery.isLoading.value);
const isRefreshing = computed(() => transactionsQuery.isFetching.value && !transactionsQuery.isLoading.value);
const feedbackMessage = ref('');
let feedbackTimeout: ReturnType<typeof setTimeout> | null = null;
const showFeedback = (message: string) => {
feedbackMessage.value = message;
if (feedbackTimeout) {
clearTimeout(feedbackTimeout);
}
feedbackTimeout = setTimeout(() => {
feedbackMessage.value = '';
}, 2400);
};
onBeforeUnmount(() => {
if (feedbackTimeout) {
clearTimeout(feedbackTimeout);
}
});
const setFilter = (value: FilterOption) => {
filter.value = value;
@@ -67,13 +89,30 @@ const submit = async () => {
notes: form.notes || undefined,
occurredAt: new Date().toISOString()
};
await createTransaction.mutateAsync(payload);
showSheet.value = false;
resetForm();
try {
await createTransaction.mutateAsync(payload);
showFeedback('新增交易已提交');
showSheet.value = false;
resetForm();
} catch (error) {
console.warn('Failed to create transaction', error);
showFeedback('保存失败,请稍后重试');
}
};
const removeTransaction = async (id: string) => {
await deleteTransaction.mutateAsync(id);
try {
await deleteTransaction.mutateAsync(id);
showFeedback('交易已删除');
} catch (error) {
console.warn('Failed to delete transaction', error);
showFeedback('删除失败,请稍后重试');
}
};
const refreshTransactions = async () => {
await transactionsQuery.refetch();
showFeedback('列表已更新');
};
</script>
@@ -84,14 +123,35 @@ const removeTransaction = async (id: string) => {
<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>
<div class="flex items-center space-x-2">
<button
class="px-4 py-2 border border-gray-200 text-sm rounded-xl text-gray-600 hover:bg-gray-50 disabled:opacity-60"
:disabled="isRefreshing"
@click="refreshTransactions"
>
<span v-if="isRefreshing" class="flex items-center space-x-2">
<svg class="w-4 h-4 animate-spin text-indigo-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
</svg>
<span>刷新中</span>
</span>
<span v-else class="flex items-center space-x-2">
<LucideIcon name="refresh-cw" :size="16" />
<span>刷新</span>
</span>
</button>
<button
class="px-4 py-2 bg-indigo-500 text-white rounded-xl font-medium hover:bg-indigo-600"
@click="showSheet = true"
>
新增
</button>
</div>
</header>
<p v-if="feedbackMessage" class="text-xs text-emerald-500 text-right">{{ feedbackMessage }}</p>
<div class="bg-white rounded-2xl p-2 flex space-x-2 border border-gray-100">
<button
v-for="item in filters"
@@ -104,19 +164,33 @@ const removeTransaction = async (id: string) => {
</button>
</div>
<div class="space-y-4">
<div class="space-y-4 min-h-[140px]">
<div v-if="isInitialLoading" class="flex justify-center py-16">
<svg class="w-8 h-8 animate-spin text-indigo-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
</svg>
</div>
<div
v-for="transaction in filteredTransactions"
:key="transaction.id"
class="relative group"
v-else-if="filteredTransactions.length === 0"
class="bg-white border border-dashed border-gray-200 rounded-2xl py-12 text-center text-sm text-gray-400"
>
<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)"
暂无交易记录点击新增开始记账
</div>
<div v-else>
<div
v-for="transaction in filteredTransactions"
:key="transaction.id"
class="relative group"
>
<LucideIcon name="trash-2" :size="18" />
</button>
<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>