2025-12-26 15:21:31 +08:00
|
|
|
|
<template>
|
2025-12-27 21:15:26 +08:00
|
|
|
|
<div class="page-container-flex classification-nlp">
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<van-nav-bar title="自然语言分类" left-text="返回" left-arrow @click-left="onClickLeft" />
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
2025-12-27 21:15:26 +08:00
|
|
|
|
<div class="scroll-content">
|
2025-12-26 15:21:31 +08:00
|
|
|
|
<!-- 输入区域 -->
|
|
|
|
|
|
<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>
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
<div class="action-buttons">
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<van-button type="primary" block round :loading="analyzing" @click="handleAnalyze">
|
2025-12-26 15:21:31 +08:00
|
|
|
|
分析查询
|
|
|
|
|
|
</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" />
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<van-cell
|
|
|
|
|
|
title="找到记录"
|
|
|
|
|
|
:value="`${analysisResult.records.length} 条`"
|
2025-12-26 15:21:31 +08:00
|
|
|
|
is-link
|
|
|
|
|
|
@click="showRecordsList = true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</van-cell-group>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 交易详情弹窗 -->
|
|
|
|
|
|
<TransactionDetail
|
|
|
|
|
|
v-model:show="showDetail"
|
|
|
|
|
|
:transaction="currentTransaction"
|
|
|
|
|
|
@save="handleDetailSave"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 记录列表弹窗 -->
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<PopupContainer v-model="showRecordsList" title="交易记录列表" height="75%">
|
|
|
|
|
|
<div style="background: var(--van-background)">
|
2025-12-26 15:21:31 +08:00
|
|
|
|
<!-- 批量操作按钮 -->
|
|
|
|
|
|
<div class="batch-actions">
|
2026-01-16 11:15:44 +08:00
|
|
|
|
<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"
|
2025-12-26 15:21:31 +08:00
|
|
|
|
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"
|
2026-01-07 14:33:30 +08:00
|
|
|
|
:show-delete="false"
|
2025-12-26 15:21:31 +08:00
|
|
|
|
@update:selected-ids="updateSelectedIds"
|
|
|
|
|
|
@click="handleRecordClick"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-30 17:02:30 +08:00
|
|
|
|
</PopupContainer>
|
2025-12-26 15:21:31 +08:00
|
|
|
|
</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'
|
2025-12-30 17:02:30 +08:00
|
|
|
|
import PopupContainer from '@/components/PopupContainer.vue'
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
|
|
|
|
|
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(() => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
if (!analysisResult.value) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return analysisResult.value.records.map((r) => ({
|
2025-12-26 15:21:31 +08:00
|
|
|
|
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)
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
if (response.success) {
|
|
|
|
|
|
analysisResult.value = response.data
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
// 默认全选
|
2026-01-16 11:15:44 +08:00
|
|
|
|
const allIds = new Set(response.data.records.map((r) => r.id))
|
2025-12-26 15:21:31 +08:00
|
|
|
|
selectedIds.value = allIds
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
showToast(`找到 ${response.data.records.length} 条记录`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(response.message || '分析失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('分析失败:', error)
|
|
|
|
|
|
showToast('分析失败,请重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
analyzing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 全选
|
|
|
|
|
|
const selectAll = () => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
if (!analysisResult.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const allIds = new Set(analysisResult.value.records.map((r) => r.id))
|
2025-12-26 15:21:31 +08:00
|
|
|
|
selectedIds.value = allIds
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 全不选
|
|
|
|
|
|
const selectNone = () => {
|
|
|
|
|
|
selectedIds.value = new Set()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新选中状态
|
|
|
|
|
|
const updateSelectedIds = (newSelectedIds) => {
|
|
|
|
|
|
selectedIds.value = newSelectedIds
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点击记录查看详情
|
|
|
|
|
|
const handleRecordClick = (transaction) => {
|
|
|
|
|
|
// 从原始记录中获取完整信息
|
2026-01-16 11:15:44 +08:00
|
|
|
|
const record = analysisResult.value?.records.find((r) => r.id === transaction.id)
|
2025-12-26 15:21:31 +08:00
|
|
|
|
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
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
// 构建批量更新数据(使用AI修改后的结果)
|
|
|
|
|
|
const items = analysisResult.value.records
|
2026-01-16 11:15:44 +08:00
|
|
|
|
.filter((r) => selectedIds.value.has(r.id))
|
|
|
|
|
|
.map((r) => ({
|
2025-12-26 15:21:31 +08:00
|
|
|
|
id: r.id,
|
|
|
|
|
|
classify: r.upsetedClassify,
|
|
|
|
|
|
type: r.upsetedType
|
|
|
|
|
|
}))
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
const response = await batchUpdateClassify(items)
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
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>
|
2025-12-27 21:20:44 +08:00
|
|
|
|
.scroll-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
.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;
|
2026-01-13 17:00:44 +08:00
|
|
|
|
background-color: var(--van-background-2);
|
2025-12-26 15:21:31 +08:00
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.batch-actions > button:last-child {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.records-list {
|
|
|
|
|
|
padding-bottom: 20px;
|
|
|
|
|
|
}
|
2025-12-26 18:03:52 +08:00
|
|
|
|
|
|
|
|
|
|
/* 设置页面容器背景色 */
|
|
|
|
|
|
:deep(.van-nav-bar) {
|
|
|
|
|
|
background: transparent !important;
|
|
|
|
|
|
}
|
2025-12-26 15:21:31 +08:00
|
|
|
|
</style>
|