feat: 前端新增中英文多语言支持
- 引入 vue-i18n,新增 locales/messages.ts 以中英对照方式维护词条 - 新增 LocaleSwitch 组件,在登录/注册页与主布局顶部栏提供切换入口 - 语言持久化到 localStorage 并同步 <html lang>,Naive UI 组件语言联动 - 各视图、表格列、弹窗与提示文案全部接入国际化 - 修复 confirmImport 缺失的 tagIds 类型定义
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 收支记录管理组件:展示交易记录列表(分页),支持新增、编辑、删除、多条件筛选
|
||||
import { ref, onMounted, computed, h, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NTag, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NDatePicker, NPagination, NCard, NSwitch, NIcon, NPopover } from 'naive-ui'
|
||||
import { EyeOutline, EyeOffOutline } from '@vicons/ionicons5'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
@@ -11,6 +12,7 @@ import BillImportModal from './BillImportModal.vue'
|
||||
|
||||
const message = useMessage() // 消息提示
|
||||
const dialog = useDialog() // 对话框
|
||||
const { t, te } = useI18n() // 国际化
|
||||
|
||||
/** 交易记录数据结构 */
|
||||
interface TransactionItem {
|
||||
@@ -51,12 +53,12 @@ const total = ref(0) // 总记录数
|
||||
const page = ref(1) // 当前页码
|
||||
const pageSize = ref(20) // 每页条数
|
||||
const sortValue = ref('transactionTime-desc') // 排序方式:排序字段-方向
|
||||
const sortOptions = [
|
||||
{ label: '交易时间(新→旧)', value: 'transactionTime-desc' },
|
||||
{ label: '交易时间(旧→新)', value: 'transactionTime-asc' },
|
||||
{ label: '金额(高→低)', value: 'amount-desc' },
|
||||
{ label: '金额(低→高)', value: 'amount-asc' },
|
||||
]
|
||||
const sortOptions = computed(() => [
|
||||
{ label: t('transaction.sort.timeDesc'), value: 'transactionTime-desc' },
|
||||
{ label: t('transaction.sort.timeAsc'), value: 'transactionTime-asc' },
|
||||
{ label: t('transaction.sort.amountDesc'), value: 'amount-desc' },
|
||||
{ label: t('transaction.sort.amountAsc'), value: 'amount-asc' },
|
||||
])
|
||||
const sortField = computed(() => sortValue.value.split('-')[0]) // 服务端排序字段
|
||||
const sortOrder = computed(() => sortValue.value.split('-')[1]) // 服务端排序方向:asc/desc
|
||||
const pageSizes = [10, 20, 50, 100, 200, 500] // 可选的每页条数
|
||||
@@ -86,11 +88,11 @@ const batchRemoveTagIds = ref<number[]>([]) // 批量移除的标签
|
||||
const batching = ref(false) // 批量设置提交状态
|
||||
|
||||
// 批量设置计入统计选项
|
||||
const batchStatsOptions = [
|
||||
{ label: '不修改', value: 'null' },
|
||||
{ label: '计入统计', value: 'true' },
|
||||
{ label: '不计入统计', value: 'false' },
|
||||
]
|
||||
const batchStatsOptions = computed(() => [
|
||||
{ label: t('transaction.batchStats.keep'), value: 'null' },
|
||||
{ label: t('transaction.batchStats.include'), value: 'true' },
|
||||
{ label: t('transaction.batchStats.exclude'), value: 'false' },
|
||||
])
|
||||
const editingId = ref<number | null>(null) // 当前编辑的记录 ID(null 表示新增)
|
||||
const categories = ref<CategoryOption[]>([]) // 分类选项列表
|
||||
const allTags = ref<TagOption[]>([]) // 全部标签(用于筛选、编辑、批量)
|
||||
@@ -109,18 +111,18 @@ const form = ref({
|
||||
})
|
||||
|
||||
// 类型选项
|
||||
const typeOptions = [
|
||||
{ label: '支出', value: 'EXPENSE' },
|
||||
{ label: '收入', value: 'INCOME' },
|
||||
]
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('common.types.expense'), value: 'EXPENSE' },
|
||||
{ label: t('common.types.income'), value: 'INCOME' },
|
||||
])
|
||||
|
||||
// 支付方式选项
|
||||
const paymentOptions = [
|
||||
{ label: '微信', value: 'WECHAT' },
|
||||
{ label: '支付宝', value: 'ALIPAY' },
|
||||
{ label: '现金', value: 'CASH' },
|
||||
{ label: '银行卡', value: 'BANK' },
|
||||
]
|
||||
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 filterType = ref<string[]>([]) // 类型筛选(多选)
|
||||
@@ -155,7 +157,7 @@ function clearMoreFilters() {
|
||||
|
||||
// 分类筛选项:固定包含「未分类」,其余分类按类型筛选联动
|
||||
const categoryFilterOptions = computed(() => [
|
||||
{ label: '未分类', value: 0, type: '' },
|
||||
{ label: t('transaction.uncategorized'), value: 0, type: '' },
|
||||
...categories.value.filter(c => filterType.value.length === 0 || filterType.value.includes(c.type)),
|
||||
])
|
||||
|
||||
@@ -172,10 +174,10 @@ function buildCategoryFilter() {
|
||||
}
|
||||
|
||||
// 计入统计筛选下拉选项
|
||||
const includeInStatsOptions = [
|
||||
{ label: '计入', value: 'true' },
|
||||
{ label: '不计入', value: 'false' },
|
||||
]
|
||||
const includeInStatsOptions = computed(() => [
|
||||
{ label: t('transaction.includeFilter.yes'), value: 'true' },
|
||||
{ label: t('transaction.includeFilter.no'), value: 'false' },
|
||||
])
|
||||
|
||||
/** 筛选条件偏好存储键 */
|
||||
const FILTER_PREFS_KEY = 'finance.transaction.filterPrefs'
|
||||
@@ -195,7 +197,7 @@ function loadFilterPrefs() {
|
||||
if (typeof p.filterMinAmount === 'number') filterMinAmount.value = p.filterMinAmount
|
||||
if (typeof p.filterMaxAmount === 'number') filterMaxAmount.value = p.filterMaxAmount
|
||||
if (typeof p.pageSize === 'number' && pageSizes.includes(p.pageSize)) pageSize.value = p.pageSize
|
||||
if (typeof p.sortValue === 'string' && sortOptions.some((o) => o.value === p.sortValue)) sortValue.value = p.sortValue
|
||||
if (typeof p.sortValue === 'string' && sortOptions.value.some((o) => o.value === p.sortValue)) sortValue.value = p.sortValue
|
||||
if (Array.isArray(p.filterTag)) filterTag.value = p.filterTag
|
||||
} catch (e) {
|
||||
// 忽略损坏的偏好数据
|
||||
@@ -239,10 +241,11 @@ function formatTimestamp(ts: number, isEnd = false): string {
|
||||
return `${date}T${time}`
|
||||
}
|
||||
|
||||
/** 将支付方式编码转换为中文标签 */
|
||||
/** 将支付方式编码转换为当前语言的标签 */
|
||||
function getPaymentLabel(val: string) {
|
||||
const map: Record<string, string> = { WECHAT: '微信', ALIPAY: '支付宝', CASH: '现金', BANK: '银行卡' }
|
||||
return map[val] || val || '-'
|
||||
if (!val) return '-'
|
||||
const key = `common.payment.${val.toLowerCase()}`
|
||||
return te(key) ? t(key) : val
|
||||
}
|
||||
|
||||
/** 分类下拉项渲染:图标 + 名称(下拉选项与已选项均生效) */
|
||||
@@ -292,9 +295,9 @@ async function setRemarkHidden(row: TransactionItem, hidden: boolean) {
|
||||
await toggleRemarkHidden(row.id, hidden)
|
||||
row.remarkHidden = hidden
|
||||
concealRemark(row.id)
|
||||
message.success(hidden ? '已隐藏备注' : '已取消隐藏')
|
||||
message.success(hidden ? t('transaction.remarkHiddenSuccess') : t('transaction.remarkShownSuccess'))
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '操作失败')
|
||||
message.error(e.message || t('common.operationFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,31 +305,31 @@ async function setRemarkHidden(row: TransactionItem, hidden: boolean) {
|
||||
function createColumns(): DataTableColumns<TransactionItem> {
|
||||
return [
|
||||
{
|
||||
title: '交易时间', key: 'transactionTime', width: 170,
|
||||
title: t('transaction.column.transactionTime'), key: 'transactionTime', width: 170,
|
||||
render(row) {
|
||||
const time = row.transactionTime?.replace('T', ' ').substring(0, 16) || '-'
|
||||
const created = row.createdAt?.replace('T', ' ').substring(0, 16)
|
||||
return h('span', { title: `创建时间:${created || '-'}` }, time)
|
||||
return h('span', { title: t('transaction.createdTime', { time: created || '-' }) }, time)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型', key: 'type', width: 90,
|
||||
title: t('transaction.column.type'), key: 'type', width: 90,
|
||||
render(row) {
|
||||
if (!row.includeInStats) {
|
||||
return h(NTag, { type: 'default', size: 'small' }, { default: () => '不计入' })
|
||||
return h(NTag, { type: 'default', size: 'small' }, { default: () => t('transaction.notIncluded') })
|
||||
}
|
||||
return h(NTag, { type: row.type === 'INCOME' ? 'success' : 'error', size: 'small' }, { default: () => row.type === 'INCOME' ? '收入' : '支出' })
|
||||
return h(NTag, { type: row.type === 'INCOME' ? 'success' : 'error', size: 'small' }, { default: () => row.type === 'INCOME' ? t('common.types.income') : t('common.types.expense') })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '分类', key: 'categoryName', width: 120,
|
||||
title: t('transaction.column.category'), key: 'categoryName', width: 120,
|
||||
render(row) {
|
||||
const name = row.categoryName || '未分类'
|
||||
const name = row.categoryName || t('transaction.uncategorized')
|
||||
return row.categoryIcon ? `${row.categoryIcon} ${name}` : name
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '金额', key: 'amount', width: 120,
|
||||
title: t('transaction.column.amount'), key: 'amount', width: 120,
|
||||
render(row) {
|
||||
if (!row.includeInStats) {
|
||||
return h('span', { style: { color: '#999', fontWeight: 'bold' } }, `${row.amount.toFixed(2)}`)
|
||||
@@ -336,34 +339,34 @@ function createColumns(): DataTableColumns<TransactionItem> {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '支付方式', key: 'paymentMethod', width: 100,
|
||||
title: t('transaction.column.paymentMethod'), key: 'paymentMethod', width: 100,
|
||||
render(row) { return getPaymentLabel(row.paymentMethod) },
|
||||
},
|
||||
{
|
||||
title: '备注', key: 'remark', minWidth: 220,
|
||||
title: t('transaction.column.remark'), key: 'remark', minWidth: 220,
|
||||
render(row) {
|
||||
const hidden = !!row.remarkHidden
|
||||
const collapsed = hidden && !revealedRemarkIds.value.includes(row.id)
|
||||
const nodes: any[] = [
|
||||
h('span', {
|
||||
class: 'remark-text',
|
||||
title: collapsed ? '备注已隐藏' : (row.remark || ''),
|
||||
title: collapsed ? t('transaction.remarkHidden') : (row.remark || ''),
|
||||
}, collapsed ? '******' : (row.remark || '-')),
|
||||
]
|
||||
if (collapsed) {
|
||||
nodes.push(renderRemarkAction('查看', EyeOutline, () => revealRemark(row.id)))
|
||||
nodes.push(renderRemarkAction('取消隐藏', EyeOffOutline, () => setRemarkHidden(row, false)))
|
||||
nodes.push(renderRemarkAction(t('transaction.remarkView'), EyeOutline, () => revealRemark(row.id)))
|
||||
nodes.push(renderRemarkAction(t('transaction.remarkUnhide'), EyeOffOutline, () => setRemarkHidden(row, false)))
|
||||
} else if (hidden) {
|
||||
nodes.push(renderRemarkAction('收起', EyeOffOutline, () => concealRemark(row.id)))
|
||||
nodes.push(renderRemarkAction('取消隐藏', EyeOutline, () => setRemarkHidden(row, false)))
|
||||
nodes.push(renderRemarkAction(t('transaction.remarkCollapse'), EyeOffOutline, () => concealRemark(row.id)))
|
||||
nodes.push(renderRemarkAction(t('transaction.remarkUnhide'), EyeOutline, () => setRemarkHidden(row, false)))
|
||||
} else {
|
||||
nodes.push(renderRemarkAction('隐藏', EyeOffOutline, () => setRemarkHidden(row, true)))
|
||||
nodes.push(renderRemarkAction(t('transaction.remarkHide'), EyeOffOutline, () => setRemarkHidden(row, true)))
|
||||
}
|
||||
return h('div', { class: 'remark-cell' }, nodes)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '标签', key: 'tags', minWidth: 140,
|
||||
title: t('transaction.column.tags'), key: 'tags', minWidth: 140,
|
||||
render(row) {
|
||||
if (!row.tags || row.tags.length === 0) return '-'
|
||||
return h(NSpace, { size: 4 }, {
|
||||
@@ -373,7 +376,7 @@ function createColumns(): DataTableColumns<TransactionItem> {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '计入统计', key: 'includeInStats', width: 100, align: 'center',
|
||||
title: t('transaction.column.includeInStats'), key: 'includeInStats', width: 100, align: 'center',
|
||||
render(row) {
|
||||
return h(NSwitch, {
|
||||
value: row.includeInStats,
|
||||
@@ -382,13 +385,13 @@ function createColumns(): DataTableColumns<TransactionItem> {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'actions', width: 240,
|
||||
title: t('transaction.column.actions'), key: 'actions', width: 240,
|
||||
render(row) {
|
||||
return h(NSpace, null, {
|
||||
default: () => [
|
||||
h(NButton, { size: 'small', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
|
||||
h(NButton, { size: 'small', disabled: !row.rawData, onClick: () => handleViewRaw(row) }, { default: () => '原始数据' }),
|
||||
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row.id) }, { default: () => '删除' }),
|
||||
h(NButton, { size: 'small', onClick: () => handleEdit(row) }, { default: () => t('common.edit') }),
|
||||
h(NButton, { size: 'small', disabled: !row.rawData, onClick: () => handleViewRaw(row) }, { default: () => t('transaction.rawData') }),
|
||||
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row.id) }, { default: () => t('common.delete') }),
|
||||
],
|
||||
})
|
||||
},
|
||||
@@ -396,7 +399,7 @@ function createColumns(): DataTableColumns<TransactionItem> {
|
||||
]
|
||||
}
|
||||
|
||||
const columns = createColumns() // 表格列配置
|
||||
const columns = computed(() => createColumns()) // 表格列配置(随语言切换响应式更新)
|
||||
|
||||
/** 加载分类列表 */
|
||||
async function fetchCategories() {
|
||||
@@ -442,7 +445,7 @@ async function fetchData() {
|
||||
total.value = res.data.total
|
||||
revealedRemarkIds.value = []
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '加载失败')
|
||||
message.error(e.message || t('common.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -457,7 +460,7 @@ watch(sortValue, () => {
|
||||
/** 查看某条记录的原始账单数据(格式化为缩进 JSON) */
|
||||
function handleViewRaw(row: TransactionItem) {
|
||||
if (!row.rawData) {
|
||||
message.warning('该记录没有原始账单数据')
|
||||
message.warning(t('transaction.noRawData'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -494,11 +497,11 @@ function handleEdit(row: TransactionItem) {
|
||||
/** 提交表单:校验 -> 新增或更新 -> 刷新列表 */
|
||||
async function handleSubmit() {
|
||||
if (!form.value.categoryId) {
|
||||
message.warning('请选择分类')
|
||||
message.warning(t('transaction.selectCategory'))
|
||||
return
|
||||
}
|
||||
if (!form.value.amount || form.value.amount <= 0) {
|
||||
message.warning('请输入有效金额')
|
||||
message.warning(t('transaction.validAmount'))
|
||||
return
|
||||
}
|
||||
const data = {
|
||||
@@ -514,32 +517,32 @@ async function handleSubmit() {
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await updateTransaction(editingId.value, data)
|
||||
message.success('修改成功')
|
||||
message.success(t('common.updateSuccess'))
|
||||
} else {
|
||||
await createTransaction(data)
|
||||
message.success('创建成功')
|
||||
message.success(t('common.createSuccess'))
|
||||
}
|
||||
showModal.value = false
|
||||
await fetchData()
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '操作失败')
|
||||
message.error(e.message || t('common.operationFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除记录:弹出确认对话框,确认后执行删除并刷新列表 */
|
||||
function handleDelete(id: number) {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条记录吗?',
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
title: t('common.confirmDelete'),
|
||||
content: t('transaction.deleteConfirm'),
|
||||
positiveText: t('common.delete'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await deleteTransaction(id)
|
||||
message.success('删除成功')
|
||||
message.success(t('common.deleteSuccess'))
|
||||
await fetchData()
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '删除失败')
|
||||
message.error(e.message || t('common.deleteFailed'))
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -584,16 +587,16 @@ async function handleToggleStats(row: TransactionItem, value: boolean) {
|
||||
try {
|
||||
await toggleTransactionStats(row.id, value)
|
||||
row.includeInStats = value
|
||||
message.success(value ? '已计入统计' : '已排除统计')
|
||||
message.success(value ? t('transaction.statsIncluded') : t('transaction.statsExcluded'))
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '操作失败')
|
||||
message.error(e.message || t('common.operationFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开批量设置分类弹窗 */
|
||||
function handleBatchOpen() {
|
||||
if (transactions.value.length === 0) {
|
||||
message.warning('当前表格没有可设置的记录')
|
||||
message.warning(t('transaction.noRecordsToSet'))
|
||||
return
|
||||
}
|
||||
batchExpenseCategoryId.value = null
|
||||
@@ -609,7 +612,7 @@ async function handleBatchSubmit() {
|
||||
const hasCategoryOrStats = !!(batchExpenseCategoryId.value || batchIncomeCategoryId.value || batchIncludeInStats.value !== 'null')
|
||||
const hasTags = batchAddTagIds.value.length > 0 || batchRemoveTagIds.value.length > 0
|
||||
if (!hasCategoryOrStats && !hasTags) {
|
||||
message.warning('请至少选择分类、计入统计或标签')
|
||||
message.warning(t('transaction.batchNothingSelected'))
|
||||
return
|
||||
}
|
||||
batching.value = true
|
||||
@@ -630,11 +633,11 @@ async function handleBatchSubmit() {
|
||||
removeTagIds: batchRemoveTagIds.value,
|
||||
})
|
||||
}
|
||||
message.success(`已更新 ${ids.length} 条记录`)
|
||||
message.success(t('transaction.batchUpdated', { count: ids.length }))
|
||||
showBatchModal.value = false
|
||||
await fetchData()
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '操作失败')
|
||||
message.error(e.message || t('common.operationFailed'))
|
||||
} finally {
|
||||
batching.value = false
|
||||
}
|
||||
@@ -661,14 +664,14 @@ async function handleExport() {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `交易记录_${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.xlsx`
|
||||
link.download = `${t('transaction.exportFileName')}_${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.xlsx`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
message.success('导出成功')
|
||||
message.success(t('transaction.exportSuccess'))
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '导出失败')
|
||||
message.error(e.message || t('transaction.exportFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,14 +688,14 @@ onMounted(() => {
|
||||
<div>
|
||||
<!-- 顶部:标题和新增按钮 -->
|
||||
<NSpace justify="space-between" style="margin-bottom: 16px;">
|
||||
<h2 style="margin: 0;">收支记录</h2>
|
||||
<h2 style="margin: 0;">{{ t('transaction.title') }}</h2>
|
||||
<NSpace align="center">
|
||||
<span style="color: #999; font-size: 13px;">排序:</span>
|
||||
<span style="color: #999; font-size: 13px;">{{ t('transaction.sortLabel') }}</span>
|
||||
<NSelect v-model:value="sortValue" :options="sortOptions" style="width: 170px;" />
|
||||
<NButton @click="handleBatchOpen">一键设置</NButton>
|
||||
<NButton @click="showImportModal = true">导入账单</NButton>
|
||||
<NButton @click="handleExport">导出 Excel</NButton>
|
||||
<NButton type="primary" @click="handleAdd">新增记录</NButton>
|
||||
<NButton @click="handleBatchOpen">{{ t('transaction.batchSet') }}</NButton>
|
||||
<NButton @click="showImportModal = true">{{ t('transaction.importBill') }}</NButton>
|
||||
<NButton @click="handleExport">{{ t('transaction.exportExcel') }}</NButton>
|
||||
<NButton type="primary" @click="handleAdd">{{ t('transaction.addRecord') }}</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
|
||||
@@ -705,7 +708,7 @@ onMounted(() => {
|
||||
multiple
|
||||
clearable
|
||||
:max-tag-count="2"
|
||||
placeholder="类型"
|
||||
:placeholder="t('transaction.column.type')"
|
||||
style="width: 160px;"
|
||||
/>
|
||||
<NSelect
|
||||
@@ -717,7 +720,7 @@ onMounted(() => {
|
||||
clearable
|
||||
filterable
|
||||
:max-tag-count="2"
|
||||
placeholder="分类"
|
||||
:placeholder="t('transaction.column.category')"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
<NDatePicker
|
||||
@@ -729,14 +732,14 @@ onMounted(() => {
|
||||
<NInput
|
||||
v-model:value="filterRemark"
|
||||
clearable
|
||||
placeholder="备注搜索"
|
||||
:placeholder="t('transaction.column.remark')"
|
||||
style="width: 180px;"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<NPopover v-model:show="showMoreFilter" trigger="click" placement="bottom-start">
|
||||
<template #trigger>
|
||||
<NButton>
|
||||
更多筛选
|
||||
{{ t('transaction.moreFilter') }}
|
||||
<NTag v-if="moreFilterCount > 0" :bordered="false" size="small" type="primary" style="margin-left: 6px;">
|
||||
{{ moreFilterCount }}
|
||||
</NTag>
|
||||
@@ -744,19 +747,19 @@ onMounted(() => {
|
||||
</template>
|
||||
<NSpace vertical :size="12" style="width: 260px;">
|
||||
<div>
|
||||
<div class="filter-label">支付方式</div>
|
||||
<div class="filter-label">{{ t('transaction.column.paymentMethod') }}</div>
|
||||
<NSelect
|
||||
v-model:value="filterPaymentMethod"
|
||||
:options="paymentOptions"
|
||||
multiple
|
||||
clearable
|
||||
:max-tag-count="2"
|
||||
placeholder="全部"
|
||||
:placeholder="t('common.all')"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="filter-label">标签</div>
|
||||
<div class="filter-label">{{ t('transaction.column.tags') }}</div>
|
||||
<NSelect
|
||||
v-model:value="filterTag"
|
||||
:options="tagFilterOptions"
|
||||
@@ -764,29 +767,29 @@ onMounted(() => {
|
||||
clearable
|
||||
filterable
|
||||
:max-tag-count="2"
|
||||
placeholder="全部"
|
||||
:placeholder="t('common.all')"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="filter-label">计入统计</div>
|
||||
<div class="filter-label">{{ t('transaction.column.includeInStats') }}</div>
|
||||
<NSelect
|
||||
v-model:value="filterIncludeInStats"
|
||||
:options="includeInStatsOptions"
|
||||
clearable
|
||||
placeholder="全部"
|
||||
:placeholder="t('common.all')"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="filter-label">金额区间</div>
|
||||
<div class="filter-label">{{ t('transaction.amountRange') }}</div>
|
||||
<NSpace align="center" :size="8">
|
||||
<NInputNumber
|
||||
v-model:value="filterMinAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
clearable
|
||||
placeholder="最小"
|
||||
:placeholder="t('common.min')"
|
||||
style="width: 110px;"
|
||||
/>
|
||||
<span style="color: #999;">~</span>
|
||||
@@ -795,30 +798,30 @@ onMounted(() => {
|
||||
:min="0"
|
||||
:precision="2"
|
||||
clearable
|
||||
placeholder="最大"
|
||||
:placeholder="t('common.max')"
|
||||
style="width: 110px;"
|
||||
/>
|
||||
</NSpace>
|
||||
</div>
|
||||
<NSpace justify="end">
|
||||
<NButton size="small" @click="clearMoreFilters">清空</NButton>
|
||||
<NButton size="small" @click="clearMoreFilters">{{ t('transaction.clear') }}</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NPopover>
|
||||
<NButton type="primary" @click="handleSearch">查询</NButton>
|
||||
<NButton @click="handleReset">重置</NButton>
|
||||
<NButton type="primary" @click="handleSearch">{{ t('transaction.search') }}</NButton>
|
||||
<NButton @click="handleReset">{{ t('transaction.reset') }}</NButton>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
|
||||
<!-- 表格信息栏(右上角) -->
|
||||
<NSpace justify="end" align="center" :size="12" style="margin-bottom: 8px;">
|
||||
<span style="color: #999; font-size: 12px;">
|
||||
总计 {{ total }} 条 · 第 {{ page }} / {{ totalPages }} 页 · 每页 {{ pageSize }} 条
|
||||
{{ t('transaction.summary', { total, page, pages: totalPages, size: pageSize }) }}
|
||||
</span>
|
||||
<span style="font-size: 12px; color: #999;">
|
||||
本页支出 <span style="color: #d03050;">{{ pageSummary.expense.toFixed(2) }}</span>
|
||||
· 本页收入 <span style="color: #18a058;">{{ pageSummary.income.toFixed(2) }}</span>
|
||||
· 本页结余 <span :style="{ color: pageSummary.balance < 0 ? '#d03050' : '#18a058' }">{{ pageSummary.balance.toFixed(2) }}</span>
|
||||
{{ t('transaction.pageExpense') }} <span style="color: #d03050;">{{ pageSummary.expense.toFixed(2) }}</span>
|
||||
· {{ t('transaction.pageIncome') }} <span style="color: #18a058;">{{ pageSummary.income.toFixed(2) }}</span>
|
||||
· {{ t('transaction.pageBalance') }} <span :style="{ color: pageSummary.balance < 0 ? '#d03050' : '#18a058' }">{{ pageSummary.balance.toFixed(2) }}</span>
|
||||
</span>
|
||||
</NSpace>
|
||||
|
||||
@@ -840,41 +843,41 @@ onMounted(() => {
|
||||
</NSpace>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<NModal v-model:show="showModal" preset="card" :title="editingId ? '编辑记录' : '新增记录'" style="width: 500px;">
|
||||
<NModal v-model:show="showModal" preset="card" :title="editingId ? t('transaction.editRecord') : t('transaction.addRecordTitle')" style="width: 500px;">
|
||||
<NForm>
|
||||
<NFormItem label="类型">
|
||||
<NFormItem :label="t('transaction.column.type')">
|
||||
<NSelect v-model:value="form.type" :options="typeOptions" />
|
||||
</NFormItem>
|
||||
<NFormItem label="分类">
|
||||
<NSelect v-model:value="form.categoryId" :options="categories.filter(c => c.type === form.type)" :render-label="renderCategoryLabel" placeholder="请选择分类" filterable />
|
||||
<NFormItem :label="t('transaction.column.category')">
|
||||
<NSelect v-model:value="form.categoryId" :options="categories.filter(c => c.type === form.type)" :render-label="renderCategoryLabel" :placeholder="t('transaction.selectCategoryPlaceholder')" filterable />
|
||||
</NFormItem>
|
||||
<NFormItem label="金额">
|
||||
<NInputNumber v-model:value="form.amount" :min="0.01" :precision="2" style="width: 100%;" placeholder="请输入金额" />
|
||||
<NFormItem :label="t('transaction.column.amount')">
|
||||
<NInputNumber v-model:value="form.amount" :min="0.01" :precision="2" style="width: 100%;" :placeholder="t('transaction.amountPlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem label="支付方式">
|
||||
<NSelect v-model:value="form.paymentMethod" :options="paymentOptions" clearable placeholder="选择支付方式" />
|
||||
<NFormItem :label="t('transaction.column.paymentMethod')">
|
||||
<NSelect v-model:value="form.paymentMethod" :options="paymentOptions" clearable :placeholder="t('transaction.paymentPlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem label="交易时间">
|
||||
<NFormItem :label="t('transaction.column.transactionTime')">
|
||||
<NDatePicker v-model:value="form.transactionTime" type="datetime" style="width: 100%;" />
|
||||
</NFormItem>
|
||||
<NFormItem label="备注">
|
||||
<NInput v-model:value="form.remark" type="textarea" placeholder="可选备注说明" />
|
||||
<NFormItem :label="t('transaction.column.remark')">
|
||||
<NInput v-model:value="form.remark" type="textarea" :placeholder="t('transaction.remarkPlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem label="标签">
|
||||
<NFormItem :label="t('transaction.column.tags')">
|
||||
<NSelect
|
||||
v-model:value="form.tagIds"
|
||||
:options="tagFilterOptions"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
placeholder="选择标签"
|
||||
:placeholder="t('transaction.tagPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="计入统计">
|
||||
<NFormItem :label="t('transaction.includeInStats')">
|
||||
<NSwitch v-model:value="form.includeInStats" />
|
||||
<span style="color: #999; margin-left: 8px; font-size: 12px;">关闭后不纳入月度统计(如借款、转账等)</span>
|
||||
<span style="color: #999; margin-left: 8px; font-size: 12px;">{{ t('transaction.includeInStatsHint') }}</span>
|
||||
</NFormItem>
|
||||
<NButton type="primary" block @click="handleSubmit">确定</NButton>
|
||||
<NButton type="primary" block @click="handleSubmit">{{ t('common.confirm') }}</NButton>
|
||||
</NForm>
|
||||
</NModal>
|
||||
|
||||
@@ -882,48 +885,48 @@ onMounted(() => {
|
||||
<BillImportModal v-model:show="showImportModal" @imported="fetchData" />
|
||||
|
||||
<!-- 原始账单数据弹窗 -->
|
||||
<NModal v-model:show="showRawModal" preset="card" title="原始账单数据" style="width: 640px;">
|
||||
<NModal v-model:show="showRawModal" preset="card" :title="t('transaction.rawDataTitle')" style="width: 640px;">
|
||||
<NInput :value="rawDataText" type="textarea" readonly :autosize="{ minRows: 8, maxRows: 20 }" />
|
||||
</NModal>
|
||||
|
||||
<!-- 一键设置弹窗(作用于当前表格展示的记录) -->
|
||||
<NModal v-model:show="showBatchModal" preset="card" title="一键设置分类与统计" style="width: 480px;">
|
||||
<NModal v-model:show="showBatchModal" preset="card" :title="t('transaction.batchTitle')" style="width: 480px;">
|
||||
<NForm>
|
||||
<NFormItem label="作用范围">
|
||||
<NFormItem :label="t('transaction.batchScope')">
|
||||
<span style="color: #999; font-size: 12px;">
|
||||
将对当前表格展示的 {{ transactions.length }} 条记录设置分类与计入统计
|
||||
{{ t('transaction.batchScopeHint', { count: transactions.length }) }}
|
||||
</span>
|
||||
</NFormItem>
|
||||
<NFormItem label="支出分类">
|
||||
<NFormItem :label="t('transaction.expenseCategory')">
|
||||
<NSelect
|
||||
v-model:value="batchExpenseCategoryId"
|
||||
:options="categories.filter(c => c.type === 'EXPENSE')"
|
||||
:render-label="renderCategoryLabel"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="不设置支出分类"
|
||||
:placeholder="t('transaction.notSetExpense')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="收入分类">
|
||||
<NFormItem :label="t('transaction.incomeCategory')">
|
||||
<NSelect
|
||||
v-model:value="batchIncomeCategoryId"
|
||||
:options="categories.filter(c => c.type === 'INCOME')"
|
||||
:render-label="renderCategoryLabel"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="不设置收入分类"
|
||||
:placeholder="t('transaction.notSetIncome')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="计入统计">
|
||||
<NFormItem :label="t('transaction.includeInStats')">
|
||||
<NSelect v-model:value="batchIncludeInStats" :options="batchStatsOptions" />
|
||||
</NFormItem>
|
||||
<NFormItem label="添加标签">
|
||||
<NSelect v-model:value="batchAddTagIds" :options="tagFilterOptions" multiple clearable filterable placeholder="不添加标签" />
|
||||
<NFormItem :label="t('transaction.addTag')">
|
||||
<NSelect v-model:value="batchAddTagIds" :options="tagFilterOptions" multiple clearable filterable :placeholder="t('transaction.notAddTag')" />
|
||||
</NFormItem>
|
||||
<NFormItem label="移除标签">
|
||||
<NSelect v-model:value="batchRemoveTagIds" :options="tagFilterOptions" multiple clearable filterable placeholder="不移除标签" />
|
||||
<NFormItem :label="t('transaction.removeTag')">
|
||||
<NSelect v-model:value="batchRemoveTagIds" :options="tagFilterOptions" multiple clearable filterable :placeholder="t('transaction.notRemoveTag')" />
|
||||
</NFormItem>
|
||||
<NButton type="primary" block :loading="batching" @click="handleBatchSubmit">确定</NButton>
|
||||
<NButton type="primary" block :loading="batching" @click="handleBatchSubmit">{{ t('common.confirm') }}</NButton>
|
||||
</NForm>
|
||||
</NModal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user