功能添加
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 20s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s

This commit is contained in:
孙诚
2025-12-26 15:21:31 +08:00
parent 7dfb6a5902
commit cb11d80d1f
26 changed files with 2208 additions and 841 deletions

View File

@@ -0,0 +1,371 @@
<template>
<div class="classification-nlp">
<van-nav-bar
title="智能分类助手"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
<div class="container">
<!-- 输入区域 -->
<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>
.classification-nlp {
min-height: 100vh;
background-color: var(--van-background, #f7f8fa);
padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px));
}
.container {
padding: 0;
}
.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;
}
</style>