Files
AI-Bill/apps/frontend/src/features/analysis/pages/AnalysisPage.vue
2025-11-01 09:24:26 +08:00

150 lines
5.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { reactive, ref } from 'vue';
import LucideIcon from '../../../components/common/LucideIcon.vue';
import { useSpendingInsightQuery, useCalorieEstimationMutation } from '../../../composables/useAnalysis';
const range = ref<'30d' | '90d'>('30d');
const { data: insight } = useSpendingInsightQuery(range);
const calorieQuery = useCalorieEstimationMutation();
const calorieForm = reactive({ query: '' });
interface ChatMessage {
role: 'assistant' | 'user';
text: string;
}
const messages = ref<ChatMessage[]>([
{
role: 'assistant',
text: '你好我分析了你近30天的消费。你似乎在“餐饮”上的支出较多特别是工作日午餐和咖啡。这里有几条建议\n- 尝试每周带 1-2 次午餐。\n- 将每天喝咖啡的习惯改为每周 3 次。\n- 设置一个“餐饮”类的单日预算。'
}
]);
const submitCalorieQuery = async () => {
if (!calorieForm.query.trim()) return;
const userMessage: ChatMessage = { role: 'user', text: calorieForm.query };
messages.value.push(userMessage);
try {
const result = await calorieQuery.mutateAsync(calorieForm.query);
const response: ChatMessage = {
role: 'assistant',
text: `热量估算:${result.calories} kcal\n${result.insights?.join('\n') ?? ''}`
};
messages.value.push(response);
} catch (error) {
messages.value.push({
role: 'assistant',
text: 'AI 服务暂时不可用,请稍后重试。'
});
console.error(error);
} finally {
calorieForm.query = '';
}
};
</script>
<template>
<div class="p-6 pt-10 pb-24 space-y-8">
<header class="space-y-2">
<p class="text-sm text-gray-500">AI 助手随时待命</p>
<h1 class="text-3xl font-bold text-gray-900">AI 智能分析</h1>
</header>
<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">消费洞察</h2>
<div class="bg-gray-100 rounded-full p-1 flex space-x-1">
<button
v-for="option in ['30d', '90d']"
:key="option"
class="px-3 py-1 text-sm rounded-full"
:class="range === option ? 'bg-white text-indigo-500 shadow-sm' : 'text-gray-500'"
@click="range = option as '30d' | '90d'"
>
{{ option === '30d' ? '近30天' : '近90天' }}
</button>
</div>
</div>
<p class="text-sm text-gray-600 whitespace-pre-line">{{ insight?.summary }}</p>
<ul class="space-y-2">
<li
v-for="recommendation in insight?.recommendations ?? []"
:key="recommendation"
class="flex items-start space-x-2 text-sm text-gray-700"
>
<LucideIcon name="sparkles" class="mt-1 text-indigo-500" :size="16" />
<span>{{ recommendation }}</span>
</li>
</ul>
<div class="grid grid-cols-3 gap-4 pt-2">
<div
v-for="category in insight?.categories ?? []"
:key="category.name"
class="bg-gray-50 rounded-2xl p-4"
>
<p class="text-sm text-gray-500">{{ category.name }}</p>
<p class="text-lg font-semibold text-gray-900 mt-1">¥ {{ category.amount.toFixed(2) }}</p>
<span
class="text-xs font-medium"
:class="{
'text-red-500': category.trend === 'up',
'text-emerald-500': category.trend === 'down',
'text-gray-500': category.trend === 'flat'
}"
>
{{ category.trend === 'up' ? '上升' : category.trend === 'down' ? '下降' : '持平' }}
</span>
</div>
</div>
</section>
<section class="bg-white rounded-3xl p-6 space-y-4">
<h2 class="text-lg font-semibold text-gray-900">卡路里估算</h2>
<div class="flex space-x-3">
<input
v-model="calorieForm.query"
type="text"
placeholder="例如:一份麦辣鸡腿堡"
class="flex-1 px-4 py-3 border border-gray-300 rounded-2xl focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button
class="w-14 h-14 rounded-2xl bg-indigo-500 text-white flex items-center justify-center hover:bg-indigo-600 disabled:opacity-60"
:disabled="calorieQuery.isPending.value"
@click="submitCalorieQuery"
>
<LucideIcon v-if="!calorieQuery.isPending.value" name="search" :size="22" />
<svg
v-else
class="animate-spin w-5 h-5"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
</button>
</div>
<div class="bg-gray-50 rounded-2xl p-4 space-y-4 h-72 overflow-y-auto">
<div
v-for="(message, index) in messages"
:key="index"
class="flex"
:class="message.role === 'user' ? 'justify-end' : 'justify-start'"
>
<div
class="max-w-[70%] px-4 py-3 text-sm whitespace-pre-line"
:class="message.role === 'user'
? 'bg-gray-200 text-gray-800 rounded-2xl rounded-br-none'
: 'bg-indigo-500 text-white rounded-2xl rounded-bl-none'"
>
{{ message.text }}
</div>
</div>
</div>
</section>
</div>
</template>