feat: 扩展账单导入(zip/美团/招行/交行)并新增批量删除与动态支付方式

- 账单导入支持 zip 压缩包,加密包自动检测并弹窗输入解压密码(引入 zip4j)
- 新增美团、招商银行、交通银行账单解析器及对应账单类型
- 支付方式列表改为后端动态获取,前端筛选与新增/编辑表单动态渲染
- 支付方式筛选从「更多筛选」移至常用筛选栏,移除「银行卡」选项
- 收支记录支持多选批量删除
- 补充相关三语文案并透传业务错误码
This commit is contained in:
2026-09-19 02:46:35 +08:00
parent c69e76ed4c
commit 9981e77bf1
18 changed files with 1558 additions and 55 deletions
+68 -23
View File
@@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'
import { useMessage, useDialog, NButton, NSpace, NDataTable, NTag, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NDatePicker, NPagination, NCard, NSwitch, NIcon, NPopover, NAutoComplete, NAlert, NDivider, NDropdown } from 'naive-ui'
import { EyeOutline, EyeOffOutline, CloseCircleOutline, SparklesOutline, BulbOutline } from '@vicons/ionicons5'
import type { DataTableColumns, AutoCompleteOption } from 'naive-ui'
import { getTransactionPage, createTransaction, updateTransaction, deleteTransaction, exportTransaction, toggleTransactionStats, toggleRemarkHidden, batchUpdateCategory, batchTags } from '../api/transaction'
import { getTransactionPage, createTransaction, updateTransaction, deleteTransaction, exportTransaction, toggleTransactionStats, toggleRemarkHidden, batchUpdateCategory, batchTags, getPaymentMethods, batchDeleteTransactions } from '../api/transaction'
import { getAiConfig, suggestByAi, suggestOneByAi, applyAiSuggestions, queryByAi, parseRecordByAi, parseCategoryByAi, parseTagByAi } from '../api/ai'
import type { AiSuggestion, AiQueryResult, AiRecordResult } from '../api/ai'
import { getCategoryList, createCategory } from '../api/category'
@@ -158,6 +158,7 @@ const editingId = ref<number | null>(null) // 当前编辑的记录 ID(null
const categories = ref<CategoryOption[]>([]) // 分类选项列表
const allTags = ref<TagOption[]>([]) // 全部标签(用于筛选、编辑、批量)
const revealedRemarkIds = ref<number[]>([]) // 临时展开查看原文的备注行 ID(不持久,刷新/翻页后重置)
const checkedTransactionIds = ref<number[]>([]) // 表格多选选中的记录ID(用于批量删除)
// 表单数据(新增/编辑共用)
const form = ref({
@@ -177,13 +178,13 @@ const typeOptions = computed(() => [
{ label: t('common.types.income'), value: 'INCOME' },
])
// 支付方式选项
const paymentOptions = computed(() => [
{ label: t('common.payment.wechat'), value: 'WECHAT' },
{ label: t('common.payment.alipay'), value: 'ALIPAY' },
{ label: t('common.payment.cash'), value: 'CASH' },
{ label: t('common.payment.bank'), value: 'BANK' },
])
// 支付方式编码列表(从后端动态获取,含账单导入来源与人工支付方式)
const paymentMethodCodes = ref<string[]>([])
// 支付方式选项:由后端返回的编码映射为当前语言标签
const paymentOptions = computed(() =>
paymentMethodCodes.value.map((code) => ({ label: getPaymentLabel(code), value: code })),
)
// 筛选条件状态
const filterType = ref<string[]>([]) // 类型筛选(多选)
@@ -206,7 +207,6 @@ const showMoreFilter = ref(false) // 更多筛选弹层显示状态
// 更多筛选中已激活的条件数量
const moreFilterCount = computed(() => {
let count = 0
if (filterPaymentMethod.value.length > 0) count++
if (filterTag.value.length > 0) count++
if (filterIncludeInStats.value !== null) count++
if (filterMinAmount.value !== null || filterMaxAmount.value !== null) count++
@@ -215,7 +215,6 @@ const moreFilterCount = computed(() => {
/** 清空「更多筛选」中的条件 */
function clearMoreFilters() {
filterPaymentMethod.value = []
filterTag.value = []
filterIncludeInStats.value = null
filterMinAmount.value = null
@@ -483,6 +482,8 @@ async function setRemarkHidden(row: TransactionItem, hidden: boolean) {
/** 创建表格列配置 */
function createColumns(): DataTableColumns<TransactionItem> {
return [
// 多选列:用于批量操作(如批量删除)
{ type: 'selection' },
{
// 序号列:按分页计算全局序号
title: t('transaction.column.index'), key: 'index', width: 70, align: 'center',
@@ -587,6 +588,16 @@ function createColumns(): DataTableColumns<TransactionItem> {
const columns = computed(() => createColumns()) // 表格列配置(随语言切换响应式更新)
/** 从后端加载支付方式列表,供筛选与编辑表单使用 */
async function loadPaymentMethods() {
try {
const res: any = await getPaymentMethods()
paymentMethodCodes.value = res.data || []
} catch (e) {
// 静默处理,支付方式下拉保持为空
}
}
/** 加载分类列表 */
async function fetchCategories() {
try {
@@ -630,6 +641,7 @@ async function fetchData() {
transactions.value = res.data.records
total.value = res.data.total
revealedRemarkIds.value = []
checkedTransactionIds.value = []
} catch (e: any) {
message.error(e.message || t('common.loadFailed'))
} finally {
@@ -739,6 +751,31 @@ function handleDelete(id: number) {
})
}
/** 批量删除选中的记录:确认后调用接口并刷新列表 */
function handleBatchDelete() {
const count = checkedTransactionIds.value.length
if (count === 0) {
message.warning(t('transaction.batchDeleteNone'))
return
}
dialog.warning({
title: t('common.confirmDelete'),
content: t('transaction.batchDeleteConfirm', { count }),
positiveText: t('common.delete'),
negativeText: t('common.cancel'),
onPositiveClick: async () => {
try {
const res: any = await batchDeleteTransactions(checkedTransactionIds.value)
message.success(t('transaction.batchDeleted', { count: res.data }))
checkedTransactionIds.value = []
await fetchData()
} catch (e: any) {
message.error(e.message || t('common.deleteFailed'))
}
},
})
}
/** 翻页事件处理 */
function handlePageChange(p: number) {
page.value = p
@@ -1558,6 +1595,7 @@ onMounted(() => {
loadAiConfig()
fetchCategories()
fetchTags()
loadPaymentMethods()
fetchData()
})
</script>
@@ -1572,6 +1610,9 @@ onMounted(() => {
<NSelect v-model:value="sortValue" :options="sortOptions" style="width: 170px;" />
<NDivider vertical style="margin: 0 4px;" />
<NButton @click="handleBatchOpen">{{ t('transaction.batchSet') }}</NButton>
<NButton type="error" :disabled="checkedTransactionIds.length === 0" @click="handleBatchDelete">
{{ t('transaction.batchDelete') }}
</NButton>
<NButton @click="showImportModal = true">{{ t('transaction.importBill') }}</NButton>
<NButton @click="handleExport">{{ t('transaction.exportExcel') }}</NButton>
<NButton v-if="aiEnabled" @click="handleAiRecordOpen">
@@ -1661,6 +1702,15 @@ onMounted(() => {
@select="handleRemarkSelect"
@blur="handleRemarkBlur"
/>
<NSelect
v-model:value="filterPaymentMethod"
:options="paymentOptions"
multiple
clearable
:max-tag-count="2"
:placeholder="t('transaction.column.paymentMethod')"
style="width: 180px;"
/>
<NPopover v-model:show="showMoreFilter" trigger="click" placement="bottom-start">
<template #trigger>
<NButton>
@@ -1671,18 +1721,6 @@ onMounted(() => {
</NButton>
</template>
<NSpace vertical :size="12" style="width: 260px;">
<div>
<div class="filter-label">{{ t('transaction.column.paymentMethod') }}</div>
<NSelect
v-model:value="filterPaymentMethod"
:options="paymentOptions"
multiple
clearable
:max-tag-count="2"
:placeholder="t('common.all')"
style="width: 100%;"
/>
</div>
<div>
<div class="filter-label">{{ t('transaction.column.tags') }}</div>
<NSelect
@@ -1768,7 +1806,14 @@ onMounted(() => {
</NSpace>
<!-- 交易记录数据表格 -->
<NDataTable :columns="columns" :data="transactions" :loading="loading" :bordered="false" />
<NDataTable
:columns="columns"
:data="transactions"
:loading="loading"
:bordered="false"
:row-key="(row: TransactionItem) => row.id"
v-model:checked-row-keys="checkedTransactionIds"
/>
<!-- 分页控件 -->
<NSpace justify="center" style="margin-top: 16px;">