Files
EmailBill/Web/src/views/BillAnalysisView.vue

397 lines
8.6 KiB
Vue
Raw Normal View History

2026-02-15 10:10:28 +08:00
<!-- eslint-disable vue/no-v-html -->
<template>
2026-01-16 11:15:44 +08:00
<div class="page-container-flex">
2025-12-26 17:13:57 +08:00
<!-- 顶部导航栏 -->
2026-01-16 11:15:44 +08:00
<van-nav-bar
title="智能分析"
left-arrow
placeholder
2025-12-26 17:13:57 +08:00
@click-left="onClickLeft"
>
<template #right>
2026-01-16 11:15:44 +08:00
<van-icon
name="setting-o"
size="20"
style="cursor: pointer; padding-right: 12px"
@click="onClickPrompt"
/>
</template>
</van-nav-bar>
2025-12-26 17:13:57 +08:00
2025-12-27 22:34:19 +08:00
<div class="scroll-content analysis-content">
2025-12-26 17:13:57 +08:00
<!-- 输入区域 -->
<div class="input-section">
<van-field
v-model="userInput"
rows="2"
autosize
type="textarea"
maxlength="500"
placeholder="请输入您想了解的账单问题..."
show-word-limit
:disabled="analyzing"
/>
2026-01-16 11:15:44 +08:00
2025-12-26 17:13:57 +08:00
<div class="quick-questions">
2026-01-16 11:15:44 +08:00
<div class="quick-title">
快捷问题
</div>
<van-tag
v-for="(q, index) in quickQuestions"
2025-12-26 17:13:57 +08:00
:key="index"
type="primary"
plain
size="medium"
class="quick-tag"
@click="selectQuestion(q)"
2025-12-26 17:13:57 +08:00
>
{{ q }}
</van-tag>
</div>
2026-01-16 11:15:44 +08:00
<van-button
type="primary"
block
2025-12-26 17:13:57 +08:00
round
:loading="analyzing"
loading-text="分析中..."
:disabled="!userInput.trim()"
@click="startAnalysis"
2025-12-26 17:13:57 +08:00
>
开始分析
</van-button>
</div>
<!-- 结果区域 -->
2026-01-16 11:15:44 +08:00
<div
v-if="showResult"
class="result-section"
>
2025-12-26 17:13:57 +08:00
<div class="result-header">
<h3>分析结果</h3>
2026-01-16 11:15:44 +08:00
<van-icon
v-if="!analyzing"
name="delete-o"
size="18"
2025-12-26 17:13:57 +08:00
@click="clearResult"
/>
</div>
2026-01-16 11:15:44 +08:00
<div
ref="resultContainer"
class="result-content rich-html-content"
>
<div v-html="resultHtml" />
<van-loading
v-if="analyzing"
class="result-loading"
>
2025-12-26 17:13:57 +08:00
AI正在分析中...
</van-loading>
2026-01-16 11:15:44 +08:00
<div ref="scrollAnchor" />
2025-12-26 17:13:57 +08:00
</div>
</div>
</div>
<!-- 提示词设置弹窗 -->
2026-02-20 14:57:19 +08:00
<PopupContainerV2
v-model:show="showPromptDialog"
title="编辑分析提示词"
2026-02-20 14:57:19 +08:00
:height="'75%'"
>
2026-02-20 14:57:19 +08:00
<div style="padding: 16px">
<van-field
v-model="promptValue"
rows="4"
autosize
type="textarea"
maxlength="2000"
placeholder="输入自定义的分析提示词..."
show-word-limit
/>
</div>
<template #footer>
<div style="display: flex; gap: 12px">
<van-button
plain
style="flex: 1"
@click="showPromptDialog = false"
>
取消
</van-button>
<van-button
type="primary"
style="flex: 1"
@click="confirmPrompt"
>
保存
</van-button>
</div>
</template>
</PopupContainerV2>
2025-12-26 17:13:57 +08:00
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showLoadingToast, closeToast } from 'vant'
import { getConfig, setConfig } from '@/api/config'
2026-02-20 14:57:19 +08:00
import PopupContainerV2 from '@/components/PopupContainerV2.vue'
2025-12-26 17:13:57 +08:00
const router = useRouter()
const userInput = ref('')
const analyzing = ref(false)
const showResult = ref(false)
const resultHtml = ref('')
const resultContainer = ref(null)
const scrollAnchor = ref(null)
// 提示词弹窗相关
const showPromptDialog = ref(false)
const promptValue = ref('')
2025-12-26 17:13:57 +08:00
// 快捷问题
const quickQuestions = [
'最近三个月交通费用多少?',
'这个月吃饭花了多少钱?',
'上个月的收入总额是多少?',
'最近半年哪个月花钱最多?'
]
// 返回
const onClickLeft = () => {
2026-01-20 19:56:29 +08:00
if (window.history.length > 1) {
router.back()
} else {
router.replace('/')
}
2025-12-26 17:13:57 +08:00
}
// 点击提示词按钮
const onClickPrompt = async () => {
try {
const response = await getConfig('BillAnalysisPrompt')
if (response.success) {
promptValue.value = response.data || ''
}
} catch (error) {
console.error('获取提示词失败:', error)
}
showPromptDialog.value = true
}
// 确认提示词
const confirmPrompt = async () => {
if (!promptValue.value.trim()) {
showToast('请输入提示词')
return
}
showLoadingToast({
message: '保存中...',
forbidClick: true
})
try {
const response = await setConfig('BillAnalysisPrompt', promptValue.value)
if (response.success) {
showToast('提示词已保存')
showPromptDialog.value = false
}
} catch (error) {
console.error('保存提示词失败:', error)
showToast('保存失败,请重试')
} finally {
closeToast()
}
}
2025-12-26 17:13:57 +08:00
// 选择快捷问题
const selectQuestion = (question) => {
userInput.value = question
}
// 清空结果
const clearResult = () => {
showResult.value = false
resultHtml.value = ''
}
// 滚动到底部
const scrollToBottom = async () => {
await nextTick()
if (scrollAnchor.value) {
scrollAnchor.value.scrollIntoView({ behavior: 'smooth', block: 'end' })
}
}
// 开始分析
const startAnalysis = async () => {
if (!userInput.value.trim()) {
showToast('请输入您的问题')
return
}
analyzing.value = true
showResult.value = true
resultHtml.value = ''
try {
const baseUrl = import.meta.env.VITE_API_BASE_URL || ''
2026-01-12 14:34:58 +08:00
const token = localStorage.getItem('token')
2025-12-26 17:13:57 +08:00
const response = await fetch(`${baseUrl}/TransactionRecord/AnalyzeBill`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
2026-01-16 11:15:44 +08:00
Authorization: `Bearer ${token}`
2025-12-26 17:13:57 +08:00
},
body: JSON.stringify({
userInput: userInput.value
})
})
if (!response.ok) {
throw new Error('分析请求失败')
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
2026-01-16 11:15:44 +08:00
if (done) {
break
}
2025-12-26 17:13:57 +08:00
const chunk = decoder.decode(value, { stream: true })
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6).trim()
2026-01-16 11:15:44 +08:00
2025-12-26 17:13:57 +08:00
if (data === '[DONE]') {
continue
}
try {
const json = JSON.parse(data)
if (json.content) {
resultHtml.value += json.content
// 滚动到底部
scrollToBottom()
}
} catch (e) {
2025-12-26 18:03:52 +08:00
console.error('JSON解析错误:', e)
2025-12-26 17:13:57 +08:00
// 忽略JSON解析错误
}
}
}
}
} catch (error) {
console.error('分析失败:', error)
showToast('分析失败,请重试')
resultHtml.value = '<div class="error-message">分析失败,请重试</div>'
} finally {
analyzing.value = false
// 确保分析完成后滚动到底部
scrollToBottom()
}
}
</script>
<style scoped>
.analysis-content {
padding: 16px;
}
/* 输入区域 */
.input-section {
2026-01-13 17:00:44 +08:00
background: var(--van-background-2);
2025-12-26 17:13:57 +08:00
padding: 20px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
2026-01-13 17:00:44 +08:00
border: 1px solid var(--van-border-color);
2025-12-26 17:13:57 +08:00
margin-bottom: 16px;
}
.input-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 8px 0;
}
.input-tip {
font-size: 13px;
color: var(--van-text-color-3);
margin: 0 0 16px 0;
}
.quick-questions {
margin: 16px 0;
}
.quick-title {
font-size: 14px;
color: var(--van-text-color-2);
margin-bottom: 12px;
}
.quick-tag {
margin: 0 8px 8px 0;
cursor: pointer;
}
/* 结果区域 */
.result-section {
background: var(--van-background);
padding: 20px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid var(--van-border-color);
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--van-border-color);
}
.result-header h3 {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
margin: 0;
}
.result-content {
max-height: 60vh;
overflow-y: auto;
line-height: 1.8;
color: var(--van-text-color);
}
.result-loading {
text-align: center;
padding: 20px;
}
.error-message {
2026-01-13 17:00:44 +08:00
color: var(--van-danger-color);
2025-12-26 17:13:57 +08:00
text-align: center;
padding: 20px;
}
2025-12-26 18:03:52 +08:00
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
2025-12-26 17:13:57 +08:00
</style>