Files
EmailBill/Web/src/views/ClassificationNLP.vue
孙诚 ed0fbb1930
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 12s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
样式调整
2025-12-26 18:03:52 +08:00

367 lines
8.8 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.
<template>
<div class="page-container classification-nlp">
<van-nav-bar
title="智能分类助手"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
<div class="page-content">
<!-- 输入区域 -->
<div class="input-section">
<van-cell-group inset>
<van-field
v-model="userInput"
rows="3"
autosize
type="textarea"
maxlength="200"
placeholder="用自然语言描述您的需求AI将帮您找到相关交易并自动设置分类。例如我想要将苏州城慧的支出都改为地铁通勤消费"
show-word-limit
/>
</van-cell-group>
<div class="action-buttons">
<van-button
type="primary"
block
round
:loading="analyzing"
@click="handleAnalyze"
>
分析查询
</van-button>
</div>
</div>
<!-- 分析结果展示 -->
<div v-if="analysisResult" class="result-section">
<van-cell-group inset>
<van-cell title="查询关键词" :value="analysisResult.searchKeyword" />
<van-cell title="AI建议类型" :value="getTypeName(analysisResult.targetType)" />
<van-cell title="AI建议分类" :value="analysisResult.targetClassify" />
<van-cell
title="找到记录"
:value="`${analysisResult.records.length} 条`"
is-link
@click="showRecordsList = true"
/>
</van-cell-group>
</div>
</div>
<!-- 交易详情弹窗 -->
<TransactionDetail
v-model:show="showDetail"
:transaction="currentTransaction"
@save="handleDetailSave"
/>
<!-- 记录列表弹窗 -->
<van-popup
v-model:show="showRecordsList"
position="bottom"
:style="{ height: '80%' }"
round
>
<div class="records-popup">
<div class="popup-header">
<h3>交易记录列表</h3>
<van-icon name="cross" @click="showRecordsList = false" />
</div>
<!-- 批量操作按钮 -->
<div class="batch-actions">
<van-button
plain
type="primary"
size="small"
@click="selectAll"
>
全选
</van-button>
<van-button
plain
type="default"
size="small"
@click="selectNone"
>
全不选
</van-button>
<van-button
type="success"
size="small"
:loading="submitting"
:disabled="selectedIds.size === 0"
@click="handleSubmit"
>
提交分类 ({{ selectedIds.size }})
</van-button>
</div>
<!-- 交易记录列表 -->
<div class="records-list">
<TransactionList
:transactions="displayRecords"
:loading="false"
:finished="true"
:show-checkbox="true"
:selected-ids="selectedIds"
@update:selected-ids="updateSelectedIds"
@click="handleRecordClick"
:show-delete="false"
/>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showConfirmDialog } from 'vant'
import { nlpAnalysis, batchUpdateClassify } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
const router = useRouter()
const userInput = ref('')
const analyzing = ref(false)
const submitting = ref(false)
const analysisResult = ref(null)
const selectedIds = ref(new Set())
const showDetail = ref(false)
const currentTransaction = ref(null)
const showRecordsList = ref(false) // 控制记录列表弹窗
// 返回按钮
const onClickLeft = () => {
router.back()
}
// 将带目标分类的记录转换为普通交易记录格式供列表显示
const displayRecords = computed(() => {
if (!analysisResult.value) return []
return analysisResult.value.records.map(r => ({
id: r.id,
reason: r.reason,
amount: r.amount,
balance: r.balance,
card: r.card,
occurredAt: r.occurredAt,
createTime: r.createTime,
importFrom: r.importFrom,
refundAmount: r.refundAmount,
// 显示目标类型和分类
type: r.targetType,
classify: r.targetClassify,
upsetedClassify: r.upsetedClassify,
upsetedType: r.upsetedType
}))
})
// 获取交易类型名称
const getTypeName = (type) => {
const typeMap = {
0: '支出',
1: '收入',
2: '不计入收支'
}
return typeMap[type] || '未知'
}
// 分析用户输入
const handleAnalyze = async () => {
if (!userInput.value.trim()) {
showToast('请输入查询条件')
return
}
try {
analyzing.value = true
const response = await nlpAnalysis(userInput.value)
if (response.success) {
analysisResult.value = response.data
// 默认全选
const allIds = new Set(response.data.records.map(r => r.id))
selectedIds.value = allIds
showToast(`找到 ${response.data.records.length} 条记录`)
} else {
showToast(response.message || '分析失败')
}
} catch (error) {
console.error('分析失败:', error)
showToast('分析失败,请重试')
} finally {
analyzing.value = false
}
}
// 全选
const selectAll = () => {
if (!analysisResult.value) return
const allIds = new Set(analysisResult.value.records.map(r => r.id))
selectedIds.value = allIds
}
// 全不选
const selectNone = () => {
selectedIds.value = new Set()
}
// 更新选中状态
const updateSelectedIds = (newSelectedIds) => {
selectedIds.value = newSelectedIds
}
// 点击记录查看详情
const handleRecordClick = (transaction) => {
// 从原始记录中获取完整信息
const record = analysisResult.value?.records.find(r => r.id === transaction.id)
if (record) {
currentTransaction.value = {
id: record.id,
reason: record.reason,
amount: record.amount,
balance: record.balance,
card: record.card,
occurredAt: record.occurredAt,
createTime: record.createTime,
importFrom: record.importFrom,
refundAmount: record.refundAmount,
// 用户可以在详情中修改类型和分类
type: record.targetType,
classify: record.targetClassify
}
showDetail.value = true
}
}
// 详情保存后
const handleDetailSave = () => {
// 详情中的修改已经保存到服务器
// 这里可以选择重新分析或者只更新本地显示
showToast('修改已保存')
}
// 提交分类
const handleSubmit = async () => {
if (selectedIds.value.size === 0) {
showToast('请至少选择一条记录')
return
}
try {
await showConfirmDialog({
title: '确认提交',
message: `确定要为选中的 ${selectedIds.value.size} 条记录设置分类吗?`
})
} catch {
return
}
try {
submitting.value = true
// 构建批量更新数据使用AI修改后的结果
const items = analysisResult.value.records
.filter(r => selectedIds.value.has(r.id))
.map(r => ({
id: r.id,
classify: r.upsetedClassify,
type: r.upsetedType
}))
const response = await batchUpdateClassify(items)
if (response.success) {
showToast('分类设置成功')
// 清空结果,让用户进行新的查询
analysisResult.value = null
selectedIds.value = new Set()
userInput.value = ''
} else {
showToast(response.message || '设置失败')
}
} catch (error) {
console.error('提交失败:', error)
showToast('提交失败,请重试')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.input-section {
padding: 12px 0;
}
.action-buttons {
padding: 12px 16px;
}
.result-section {
padding-top: 8px;
}
.van-notice-bar {
margin: 12px 16px;
}
.batch-actions {
display: flex;
gap: 8px;
padding: 12px 16px;
background-color: var(--van-background-2, #fff);
margin-bottom: 8px;
}
.batch-actions > button:last-child {
margin-left: auto;
}
.records-list {
padding-bottom: 20px;
overflow-y: auto;
flex: 1;
}
.records-popup {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--van-background, #f7f8fa);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: var(--van-background-2, #fff);
}
.popup-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.popup-header .van-icon {
font-size: 20px;
cursor: pointer;
}
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
</style>