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
+40 -11
View File
@@ -21,6 +21,8 @@ import type {
EmailBillSenderRulePayload,
} from '../api/emailBill'
import type { ParsedBillRow, BillType, BillImportItem } from '../api/transaction'
import { getBillTypes } from '../api/transaction'
import type { BillTypeOption } from '../api/transaction'
import { getCategoryList } from '../api/category'
import { getTagList } from '../api/tag'
import { renderCategoryLabel } from '../utils/render'
@@ -44,13 +46,25 @@ const EMAIL_BILL_TAB_KEY = 'emailBill.tab'
const activeTab = ref<string>((route.params.tab as string) || 'configs')
// ===== 公共选项 =====
const billTypeOptions = computed(() => [
{ label: t('import.billTypeALIPAY'), value: 'ALIPAY' },
{ label: t('import.billTypeWECHAT'), value: 'WECHAT' },
{ label: t('import.billTypeMEITUAN'), value: 'MEITUAN' },
{ label: t('import.billTypeCMB'), value: 'CMB' },
{ label: t('import.billTypeBOCOM'), value: 'BOCOM' },
])
// 后端下发的账单类型(编码 + 允许的扩展名)
const billTypes = ref<BillTypeOption[]>([])
// 账单类型对应的名称文案 key(文案由前端按语言维护,新增来源未补文案时回退编码)
const billTypeLabelKeys: Record<string, string> = {
ALIPAY: 'import.billTypeALIPAY',
WECHAT: 'import.billTypeWECHAT',
MEITUAN: 'import.billTypeMEITUAN',
CMB: 'import.billTypeCMB',
BOCOM: 'import.billTypeBOCOM',
}
// 账单类型选项,来源于后端
const billTypeOptions = computed(() =>
billTypes.value.map((item) => ({
label: billTypeLabelKeys[item.code] ? t(billTypeLabelKeys[item.code]) : item.code,
value: item.code,
})),
)
/** 账单类型编码转当前语言标签,未知值回退原编码 */
function billTypeLabel(value: string | null) {
@@ -58,6 +72,21 @@ function billTypeLabel(value: string | null) {
return billTypeOptions.value.find((option) => option.value === value)?.label || value
}
/** 默认账单类型:取后端下发的第一个 */
function defaultBillType(): BillType {
return billTypeOptions.value[0]?.value || ''
}
/** 加载后端支持的账单类型 */
async function loadBillTypes() {
try {
const res: any = await getBillTypes()
billTypes.value = res.data || []
} catch (e) {
// 静默处理,账单类型下拉保持为空
}
}
/** 常用 IMAP 服务器候选项,选择后自动填充默认端口与 SSL */
const imapHostOptions = computed(() => [
{ label: `${t('emailBill.imapHosts.qq')} · imap.qq.com`, value: 'imap.qq.com' },
@@ -326,7 +355,7 @@ const ruleSaving = ref(false) // 发件人规则保存状态
function emptyRuleForm() {
return {
name: '', enabled: true, sortOrder: 0, patternsText: '', billType: 'ALIPAY' as BillType,
name: '', enabled: true, sortOrder: 0, patternsText: '', billType: defaultBillType(),
archivePassword: '', expenseCategoryId: null, incomeCategoryId: null, tagIds: [] as number[],
}
}
@@ -560,7 +589,7 @@ function handleDeleteMessage(row: EmailBillMessage) {
const parseModalVisible = ref(false)
const parseStage = ref<'select' | 'preview' | 'result'>('select')
const parseMessageRow = ref<EmailBillMessage | null>(null)
const parseBillType = ref<BillType>('ALIPAY')
const parseBillType = ref<BillType>(defaultBillType())
const parsePassword = ref('')
const parsing = ref(false)
const importing = ref(false)
@@ -572,7 +601,7 @@ const parseResult = ref<{ total: number; imported: number; skipped: number; fail
function openParseModal(row: EmailBillMessage) {
parseMessageRow.value = row
parseStage.value = 'select'
parseBillType.value = (row.billType || 'ALIPAY') as BillType
parseBillType.value = (row.billType || defaultBillType()) as BillType
parsePassword.value = ''
parsedRows.value = []
parseResult.value = null
@@ -645,7 +674,7 @@ watch(activeTab, (tab) => {
let timer: number | null = null
onMounted(async () => {
await loadOptions()
await Promise.all([loadOptions(), loadBillTypes()])
await Promise.all([loadConfigs(), loadSenderRules(), loadMessages()])
timer = window.setInterval(() => {
if (activeTab.value === 'messages') {