diff --git a/apps/frontend/src/composables/useNotificationRules.ts b/apps/frontend/src/composables/useNotificationRules.ts new file mode 100644 index 0000000..9264563 --- /dev/null +++ b/apps/frontend/src/composables/useNotificationRules.ts @@ -0,0 +1,56 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'; +import { apiClient } from '../lib/api/client'; +import type { NotificationRule } from '../types/notification'; + +const queryKey = ['notification-rules']; + +export function useNotificationRulesQuery() { + return useQuery({ + queryKey, + queryFn: async () => { + const { data } = await apiClient.get('/notification-rules'); + return data.data as NotificationRule[]; + }, + initialData: [] + }); +} + +export function useCreateNotificationRuleMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: Partial) => { + const { data } = await apiClient.post('/notification-rules', payload); + return data.data as NotificationRule; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey }); + } + }); +} + +export function useUpdateNotificationRuleMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, payload }: { id: string; payload: Partial }) => { + const { data } = await apiClient.patch(`/notification-rules/${id}`, payload); + return data.data as NotificationRule; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey }); + } + }); +} + +export function useDeleteNotificationRuleMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (id: string) => { + await apiClient.delete(`/notification-rules/${id}`); + return id; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey }); + } + }); +} + diff --git a/apps/frontend/src/features/settings/pages/NotificationRulesPage.vue b/apps/frontend/src/features/settings/pages/NotificationRulesPage.vue new file mode 100644 index 0000000..0245d19 --- /dev/null +++ b/apps/frontend/src/features/settings/pages/NotificationRulesPage.vue @@ -0,0 +1,252 @@ + + + + diff --git a/apps/frontend/src/features/settings/pages/SettingsPage.vue b/apps/frontend/src/features/settings/pages/SettingsPage.vue index 2e3a33c..b445e0a 100644 --- a/apps/frontend/src/features/settings/pages/SettingsPage.vue +++ b/apps/frontend/src/features/settings/pages/SettingsPage.vue @@ -301,6 +301,7 @@ const removeChannel = async (channelId: string) => {

通知监听

配置原生插件的 Webhook 地址与安全密钥。

+ 规则管理 +
+ + +
+
diff --git a/apps/frontend/src/lib/categories.ts b/apps/frontend/src/lib/categories.ts new file mode 100644 index 0000000..066c325 --- /dev/null +++ b/apps/frontend/src/lib/categories.ts @@ -0,0 +1,17 @@ +export const PRESET_CATEGORIES = [ + '餐饮', + '交通', + '购物', + '生活缴费', + '医疗', + '教育', + '娱乐', + '转账', + '退款', + '理财', + '公益', + '收入', + '其他支出', + '未分类' +]; + diff --git a/apps/frontend/src/router/index.ts b/apps/frontend/src/router/index.ts index 6fac998..b2691c2 100644 --- a/apps/frontend/src/router/index.ts +++ b/apps/frontend/src/router/index.ts @@ -32,6 +32,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('../features/settings/pages/SettingsPage.vue'), meta: { title: '设置', requiresAuth: true } }, + { + path: '/settings/rules', + name: 'notification-rules', + component: () => import('../features/settings/pages/NotificationRulesPage.vue'), + meta: { title: '通知规则', requiresAuth: true } + }, { path: '/auth', component: () => import('../features/auth/pages/AuthLayout.vue'), diff --git a/apps/frontend/src/types/notification.ts b/apps/frontend/src/types/notification.ts index 41e7167..b557b95 100644 --- a/apps/frontend/src/types/notification.ts +++ b/apps/frontend/src/types/notification.ts @@ -11,3 +11,21 @@ export interface NotificationChannel { amountPattern?: string; }>; } + +export interface NotificationRule { + id: string; + name: string; + enabled: boolean; + priority: number; + packageName?: string; + keywords?: string[]; + pattern?: string; + action: 'record' | 'ignore'; + category?: string; + type?: 'income' | 'expense'; + amountPattern?: string; + matchCount?: number; + lastMatchedAt?: string; + createdAt?: string; + updatedAt?: string; +}