feat:添加登录功能
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user