feat:AI分析页面接入AI

This commit is contained in:
2025-11-13 17:08:31 +08:00
parent 71eba31740
commit 15a4fa618f
12 changed files with 256 additions and 23 deletions

View File

@@ -0,0 +1,37 @@
import { useMutation, useQuery } from '@tanstack/vue-query';
import { apiClient } from '../lib/api/client';
export interface AiStatus {
configured: boolean;
enableClassification: boolean;
autoLearnRules: boolean;
model: string;
baseURL: string;
}
export interface AiClassification {
category?: string;
type?: 'income' | 'expense';
amount?: number;
confidence?: number;
}
export function useAiStatusQuery() {
return useQuery<AiStatus>({
queryKey: ['ai-status'],
queryFn: async () => {
const { data } = await apiClient.get('/ai/status');
return data.data as AiStatus;
}
});
}
export function useAiClassifyMutation() {
return useMutation<AiClassification, unknown, { title: string; body?: string }>({
mutationFn: async (payload) => {
const { data } = await apiClient.post('/ai/classify', payload);
return data.data as AiClassification;
}
});
}

View File

@@ -2,6 +2,7 @@
import { reactive, ref } from 'vue';
import LucideIcon from '../../../components/common/LucideIcon.vue';
import { useSpendingInsightQuery, useCalorieEstimationMutation } from '../../../composables/useAnalysis';
import { useAiStatusQuery, useAiClassifyMutation } from '../../../composables/useAi';
const range = ref<'30d' | '90d'>('30d');
const { data: insight } = useSpendingInsightQuery(range);
@@ -43,6 +44,22 @@ const submitCalorieQuery = async () => {
calorieForm.query = '';
}
};
// AI 文本分类(用于测试通知/自由文本)
const aiStatusQuery = useAiStatusQuery();
const aiClassify = useAiClassifyMutation();
const aiForm = reactive({ title: '', body: '' });
const aiResult = ref<string>('');
const submitAiClassify = async () => {
if (!aiForm.title.trim() && !aiForm.body.trim()) return;
try {
const res = await aiClassify.mutateAsync({ title: aiForm.title.trim() || '(无标题)', body: aiForm.body.trim() || undefined });
aiResult.value = `分类: ${res.category ?? '未知'}\n类型: ${res.type ?? '-'}\n金额: ${res.amount ?? '-'}\n置信度: ${res.confidence ?? '-'}`;
} catch (error: any) {
aiResult.value = error?.response?.data?.message ?? 'AI 服务不可用';
}
};
</script>
<template>
@@ -145,5 +162,24 @@ const submitCalorieQuery = async () => {
</div>
</div>
</section>
<section class="bg-white rounded-3xl p-6 space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">AI 文本分类</h2>
<span class="text-xs" :class="aiStatusQuery.data.value?.configured ? 'text-emerald-600' : 'text-red-500'">
{{ aiStatusQuery.data.value?.configured ? `已启用 · ${aiStatusQuery.data.value?.model}` : '未配置' }}
</span>
</div>
<div class="space-y-3">
<input v-model="aiForm.title" type="text" class="w-full px-4 py-3 border border-gray-300 rounded-2xl focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="标题,如:支付宝支付成功" />
<textarea v-model="aiForm.body" rows="3" class="w-full px-4 py-3 border border-gray-300 rounded-2xl focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="正文,如:已支付 25.50 元"></textarea>
<div class="flex justify-end">
<button class="px-4 py-2 rounded-xl bg-indigo-500 text-white disabled:opacity-60" :disabled="aiClassify.isPending.value || !aiStatusQuery.data.value?.configured" @click="submitAiClassify">
{{ aiClassify.isPending.value ? '分析中...' : '测试分类' }}
</button>
</div>
</div>
<pre v-if="aiResult" class="text-sm bg-gray-50 rounded-2xl p-4 whitespace-pre-wrap">{{ aiResult }}</pre>
</section>
</div>
</template>