feat: 添加 AI 分类功能,支持交易自动分类与标签生成

This commit is contained in:
2026-03-12 10:59:44 +08:00
parent 8cbe01dd9c
commit 6a00875246
12 changed files with 594 additions and 27 deletions

View File

@@ -1,12 +1,20 @@
import { defineStore } from 'pinia'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { DEFAULT_CATEGORY_BUDGETS } from '../config/transactionCategories.js'
import { AI_MODEL_OPTIONS, AI_PROVIDER_OPTIONS } from '../config/transactionTags.js'
const AI_PROVIDER_VALUES = AI_PROVIDER_OPTIONS.map((item) => item.value)
const AI_MODEL_VALUES = AI_MODEL_OPTIONS.map((item) => item.value)
export const useSettingsStore = defineStore(
'settings',
() => {
const notificationCaptureEnabled = ref(true)
const aiAutoCategoryEnabled = ref(false)
const aiProvider = ref('deepseek')
const aiApiKey = ref('')
const aiModel = ref('deepseek-chat')
const aiAutoApplyThreshold = ref(0.9)
const monthlyBudget = ref(12000)
const budgetResetCycle = ref('monthly')
const budgetMonthlyResetDay = ref(1)
@@ -24,6 +32,27 @@ export const useSettingsStore = defineStore(
aiAutoCategoryEnabled.value = !!value
}
const setAiProvider = (value) => {
aiProvider.value = AI_PROVIDER_VALUES.includes(value) ? value : 'deepseek'
}
const setAiApiKey = (value) => {
aiApiKey.value = String(value || '').trim()
}
const setAiModel = (value) => {
aiModel.value = AI_MODEL_VALUES.includes(value) ? value : 'deepseek-chat'
}
const setAiAutoApplyThreshold = (value) => {
const numeric = Number(value)
if (!Number.isFinite(numeric)) {
aiAutoApplyThreshold.value = 0.9
return
}
aiAutoApplyThreshold.value = Math.min(Math.max(numeric, 0), 1)
}
const setMonthlyBudget = (value) => {
const numeric = Number(value)
monthlyBudget.value = Number.isFinite(numeric) && numeric >= 0 ? numeric : 0
@@ -72,6 +101,10 @@ export const useSettingsStore = defineStore(
return {
notificationCaptureEnabled,
aiAutoCategoryEnabled,
aiProvider,
aiApiKey,
aiModel,
aiAutoApplyThreshold,
monthlyBudget,
budgetResetCycle,
budgetMonthlyResetDay,
@@ -82,6 +115,10 @@ export const useSettingsStore = defineStore(
profileAvatar,
setNotificationCaptureEnabled,
setAiAutoCategoryEnabled,
setAiProvider,
setAiApiKey,
setAiModel,
setAiAutoApplyThreshold,
setMonthlyBudget,
setBudgetResetCycle,
setBudgetMonthlyResetDay,
@@ -97,6 +134,10 @@ export const useSettingsStore = defineStore(
paths: [
'notificationCaptureEnabled',
'aiAutoCategoryEnabled',
'aiProvider',
'aiApiKey',
'aiModel',
'aiAutoApplyThreshold',
'monthlyBudget',
'budgetResetCycle',
'budgetMonthlyResetDay',

View File

@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { maybeEnrichTransactionWithAi } from '../services/ai/aiService.js'
import {
deleteTransaction,
fetchTransactions,
@@ -26,6 +27,9 @@ const formatDayLabel = (dayKey) => {
}).format(localDate)
}
const replaceTransaction = (list, nextTransaction) =>
list.map((tx) => (tx.id === nextTransaction.id ? nextTransaction : tx))
export const useTransactionStore = defineStore(
'transactions',
() => {
@@ -113,6 +117,12 @@ export const useTransactionStore = defineStore(
}
}
const enrichTransactionInBackground = async (transaction) => {
const enriched = await maybeEnrichTransactionWithAi(transaction)
if (!enriched || enriched.id !== transaction.id) return
transactions.value = replaceTransaction(transactions.value, enriched)
}
const addTransaction = async (payload) => {
const normalized = {
...payload,
@@ -120,6 +130,7 @@ export const useTransactionStore = defineStore(
}
const created = await insertTransaction(normalized)
transactions.value = [created, ...transactions.value]
void enrichTransactionInBackground(created)
return created
}
@@ -130,7 +141,8 @@ export const useTransactionStore = defineStore(
}
const updated = await updateTransaction(normalized)
if (updated) {
transactions.value = transactions.value.map((tx) => (tx.id === updated.id ? updated : tx))
transactions.value = replaceTransaction(transactions.value, updated)
void enrichTransactionInBackground(updated)
}
return updated
}