feat:分类改为数据库存储

This commit is contained in:
2025-11-13 15:54:22 +08:00
parent 1f21fbfe16
commit 71eba31740
11 changed files with 259 additions and 28 deletions

View File

@@ -34,6 +34,8 @@ const statusColor = computed(() => {
});
const packageName = computed(() => props.transaction.metadata?.packageName as string | undefined);
const classification = computed(() => props.transaction.metadata?.classification as any | undefined);
const hasRuleId = computed(() => Boolean((props.transaction.metadata as any)?.classification?.ruleId));
const sourceLabel = computed(() => {
switch (props.transaction.source) {
@@ -70,6 +72,10 @@ const sourceLabel = computed(() => {
<p class="text-sm text-gray-400">
{{ new Date(transaction.occurredAt).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }) }}
</p>
<div class="mt-1 flex justify-end space-x-2">
<span v-if="classification?.source === 'ai'" class="text-[10px] px-1.5 py-0.5 rounded bg-indigo-100 text-indigo-700">AI</span>
<span v-else-if="hasRuleId" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">规则</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,58 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
import { apiClient } from '../lib/api/client';
export interface Category {
id: string;
name: string;
type?: 'expense' | 'income';
icon?: string;
color?: string;
enabled: boolean;
}
const queryKey = ['categories'];
export function useCategoriesQuery() {
return useQuery<Category[]>({
queryKey,
queryFn: async () => {
const { data } = await apiClient.get('/categories');
return data.data as Category[];
},
initialData: []
});
}
export function useCreateCategoryMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: Partial<Category>) => {
const { data } = await apiClient.post('/categories', payload);
return data.data as Category;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey })
});
}
export function useUpdateCategoryMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: Partial<Category> }) => {
const { data } = await apiClient.patch(`/categories/${id}`, payload);
return data.data as Category;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey })
});
}
export function useDeleteCategoryMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
await apiClient.delete(`/categories/${id}`);
return id;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey })
});
}

View File

@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
import LucideIcon from '../../../components/common/LucideIcon.vue';
import { useTransactionQuery } from '../../../composables/useTransaction';
import { useUpdateTransactionMutation } from '../../../composables/useTransactions';
import { PRESET_CATEGORIES } from '../../../lib/categories';
import { useCategoriesQuery } from '../../../composables/useCategories';
import { apiClient } from '../../../lib/api/client';
const route = useRoute();
@@ -19,6 +19,7 @@ const goBack = () => router.back();
const updateMutation = useUpdateTransactionMutation();
const editing = reactive({ category: ref<string>('') });
const isNotification = computed(() => txn.value?.source === 'notification');
const categoriesQuery = useCategoriesQuery();
// 初始化默认分类
editing.category = txn.value?.category ?? '';
@@ -132,7 +133,7 @@ const aiSuggestRule = async () => {
<div class="flex items-center space-x-3">
<label class="text-sm text-gray-600 shrink-0">分类</label>
<select v-model="editing.category" class="flex-1 px-3 py-2 border border-gray-200 rounded-xl text-sm">
<option v-for="cat in PRESET_CATEGORIES" :key="cat" :value="cat">{{ cat }}</option>
<option v-for="cat in categoriesQuery.data.value" :key="cat.id" :value="cat.name">{{ cat.name }}</option>
</select>
</div>
<div class="text-right">

View File

@@ -8,6 +8,7 @@ import {
useDeleteTransactionMutation
} from '../../../composables/useTransactions';
import type { TransactionPayload } from '../../../types/transaction';
import { useCategoriesQuery } from '../../../composables/useCategories';
type FilterOption = 'all' | 'expense' | 'income';
@@ -25,6 +26,7 @@ const form = reactive({
});
const transactionsQuery = useTransactionsQuery();
const categoriesQuery = useCategoriesQuery();
const createTransaction = useCreateTransactionMutation();
const deleteTransaction = useDeleteTransactionMutation();
const transactions = computed(() => transactionsQuery.data.value ?? []);
@@ -178,20 +180,7 @@ const refreshTransactions = async () => {
<label class="text-sm text-gray-600">分类筛选</label>
<select v-model="categoryFilter" class="flex-1 px-3 py-2 border border-gray-200 rounded-xl text-sm">
<option value="all">全部分类</option>
<option value="餐饮">餐饮</option>
<option value="交通">交通</option>
<option value="购物">购物</option>
<option value="生活缴费">生活缴费</option>
<option value="医疗">医疗</option>
<option value="教育">教育</option>
<option value="娱乐">娱乐</option>
<option value="转账">转账</option>
<option value="退款">退款</option>
<option value="理财">理财</option>
<option value="公益">公益</option>
<option value="收入">收入</option>
<option value="其他支出">其他支出</option>
<option value="未分类">未分类</option>
<option v-for="cat in categoriesQuery.data.value" :key="cat.id" :value="cat.name">{{ cat.name }}</option>
</select>
</div>