Files
EmailBill/Web/src/views/BillAnalysisView.vue
孙诚 037bad2d9b
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 10s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
feat: 添加待确认分类功能,支持获取和确认未分类交易记录;优化相关组件和服务
2026-01-10 12:22:37 +08:00

365 lines
7.9 KiB
Vue

<!-- eslint-disable vue/no-v-html -->
<template>
<div class="page-container-flex">
<!-- 顶部导航栏 -->
<van-nav-bar
title="智能分析"
left-arrow
placeholder
@click-left="onClickLeft"
>
<template #right>
<van-icon
name="setting-o"
size="20"
style="cursor: pointer; padding-right: 12px;"
@click="onClickPrompt"
/>
</template>
</van-nav-bar>
<div class="scroll-content analysis-content">
<!-- 输入区域 -->
<div class="input-section">
<van-field
v-model="userInput"
rows="2"
autosize
type="textarea"
maxlength="500"
placeholder="请输入您想了解的账单问题..."
show-word-limit
:disabled="analyzing"
/>
<div class="quick-questions">
<div class="quick-title">快捷问题</div>
<van-tag
v-for="(q, index) in quickQuestions"
:key="index"
type="primary"
plain
size="medium"
class="quick-tag"
@click="selectQuestion(q)"
>
{{ q }}
</van-tag>
</div>
<van-button
type="primary"
block
round
:loading="analyzing"
loading-text="分析中..."
:disabled="!userInput.trim()"
@click="startAnalysis"
>
开始分析
</van-button>
</div>
<!-- 结果区域 -->
<div v-if="showResult" class="result-section">
<div class="result-header">
<h3>分析结果</h3>
<van-icon
v-if="!analyzing"
name="delete-o"
size="18"
@click="clearResult"
/>
</div>
<div ref="resultContainer" class="result-content rich-html-content">
<div v-html="resultHtml"></div>
<van-loading v-if="analyzing" class="result-loading">
AI正在分析中...
</van-loading>
<div ref="scrollAnchor"></div>
</div>
</div>
</div>
<!-- 提示词设置弹窗 -->
<van-dialog
v-model:show="showPromptDialog"
title="编辑分析提示词"
:show-cancel-button="true"
@confirm="confirmPrompt"
>
<van-field
v-model="promptValue"
rows="4"
autosize
type="textarea"
maxlength="2000"
placeholder="输入自定义的分析提示词..."
show-word-limit
/>
</van-dialog>
</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'
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('')
// 快捷问题
const quickQuestions = [
'最近三个月交通费用多少?',
'这个月吃饭花了多少钱?',
'上个月的收入总额是多少?',
'最近半年哪个月花钱最多?'
]
// 返回
const onClickLeft = () => {
router.back()
}
// 点击提示词按钮
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()
}
}
// 选择快捷问题
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 || ''
const response = await fetch(`${baseUrl}/TransactionRecord/AnalyzeBill`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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()
if (done) break
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()
if (data === '[DONE]') {
continue
}
try {
const json = JSON.parse(data)
if (json.content) {
resultHtml.value += json.content
// 滚动到底部
scrollToBottom()
}
} catch (e) {
console.error('JSON解析错误:', e)
// 忽略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 {
background: #ffffff;
padding: 20px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #ebedf0;
margin-bottom: 16px;
}
@media (prefers-color-scheme: dark) {
.input-section {
background: #1f1f1f;
border-color: #2c2c2c;
}
}
.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 {
color: #ff6b6b;
text-align: center;
padding: 20px;
}
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
</style>