feat:AI分析页面接入AI
This commit is contained in:
37
apps/frontend/src/composables/useAi.ts
Normal file
37
apps/frontend/src/composables/useAi.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user