feat: 扩展账单导入(zip/美团/招行/交行)并新增批量删除与动态支付方式
- 账单导入支持 zip 压缩包,加密包自动检测并弹窗输入解压密码(引入 zip4j) - 新增美团、招商银行、交通银行账单解析器及对应账单类型 - 支付方式列表改为后端动态获取,前端筛选与新增/编辑表单动态渲染 - 支付方式筛选从「更多筛选」移至常用筛选栏,移除「银行卡」选项 - 收支记录支持多选批量删除 - 补充相关三语文案并透传业务错误码
This commit is contained in:
@@ -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;">
|
||||
|
||||
Reference in New Issue
Block a user