feat: 用户级偏好配置与外观设置,枚举统一抽取与账单类型后端下发

- 新增用户偏好表 t_user_preference 与进程内缓存,账单来源统计默认值由全局改为按用户存储(/api/setting)
- 新增外观设置:不计入统计记录支持删除线与灰色标签两种展示样式
- 抽取 ChannelType / TransactionType / StatisticsScope 枚举,替换各处硬编码的渠道、类型与统计口径
- BillType 内聚各来源扩展名,新增 /transaction/import/types 接口,前端改为按后端下发渲染
- 仪表盘趋势图支持点击柱子跳转收支记录并带入筛选条件
- 布局内容区按路由记忆滚动位置;已登录访问登录/注册页自动跳转
This commit is contained in:
2026-09-21 10:51:15 +08:00
parent 2976b85967
commit 1f520f4376
46 changed files with 1565 additions and 284 deletions
+27 -1
View File
@@ -8,6 +8,7 @@ import type { DataTableColumns } from 'naive-ui'
import { getTransactionPage, createTransaction, updateTransaction, deleteTransaction, exportTransaction, toggleTransactionStats, toggleRemarkHidden, batchUpdateCategory, batchTags, getChannels, 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 { getExcludedDisplayStyle } from '../api/setting'
import { getCategoryList, createCategory } from '../api/category'
import { getTagList, createTag } from '../api/tag'
import { getChannelLabel } from '../utils/channel'
@@ -92,6 +93,8 @@ const pageExcludedSummary = computed(() => {
return { income, expense }
})
const loading = ref(false) // 列表加载状态
// 「不计入统计」记录的展示样式:strikethrough-整行删除线(默认)、muted-灰色标签
const excludedDisplayStyle = ref<'strikethrough' | 'muted'>('strikethrough')
const submitting = ref(false) // 表单提交状态
const showModal = ref(false) // 新增/编辑弹窗显示状态
const showImportModal = ref(false) // 账单导入弹窗显示状态
@@ -424,6 +427,8 @@ async function setRemarkHidden(row: TransactionItem, hidden: boolean) {
/** 创建表格列配置 */
function createColumns(): DataTableColumns<TransactionItem> {
// 读取展示样式,使列配置随样式偏好变化而重新计算
const muted = excludedDisplayStyle.value === 'muted'
return [
// 多选列:用于批量操作(如批量删除)
{ type: 'selection' },
@@ -445,6 +450,10 @@ function createColumns(): DataTableColumns<TransactionItem> {
{
title: t('transaction.column.type'), key: 'type', width: 90,
render(row) {
// 灰色标签样式下,不计入统计的记录类型显示为「不计入」
if (muted && !row.includeInStats) {
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' ? t('common.types.income') : t('common.types.expense') })
},
},
@@ -458,6 +467,10 @@ function createColumns(): DataTableColumns<TransactionItem> {
{
title: t('transaction.column.amount'), key: 'amount', width: 120,
render(row) {
// 灰色标签样式下,不计入统计的记录金额置灰且不显示正负号
if (muted && !row.includeInStats) {
return h('span', { style: { color: '#999', fontWeight: 'bold' } }, row.amount.toFixed(2))
}
return h('span', { style: { color: row.type === 'INCOME' ? '#18a058' : '#d03050', fontWeight: 'bold' } },
`${row.type === 'INCOME' ? '+' : '-'}${row.amount.toFixed(2)}`)
},
@@ -535,6 +548,18 @@ async function loadChannels() {
}
}
/** 加载「不计入统计」记录展示样式偏好,失败时保持默认删除线 */
async function loadExcludedDisplayStyle() {
try {
const res: any = await getExcludedDisplayStyle()
if (res.data === 'muted' || res.data === 'strikethrough') {
excludedDisplayStyle.value = res.data
}
} catch (e) {
// 静默处理,保持默认样式
}
}
/** 加载分类列表 */
async function fetchCategories() {
try {
@@ -1487,6 +1512,7 @@ onMounted(() => {
remarkHistory.load()
aiQueryHistory.load()
loadAiConfig()
loadExcludedDisplayStyle()
fetchCategories()
fetchTags()
loadChannels()
@@ -1688,7 +1714,7 @@ onMounted(() => {
:loading="loading"
:bordered="false"
:row-key="(row: TransactionItem) => row.id"
:row-class-name="(row: TransactionItem) => row.includeInStats ? '' : 'excluded-row'"
:row-class-name="(row: TransactionItem) => (!row.includeInStats && excludedDisplayStyle === 'strikethrough') ? 'excluded-row' : ''"
v-model:checked-row-keys="checkedTransactionIds"
/>