feat: 邮箱账单自动拉取
- 新增邮箱配置、发件人规则、待处理账单三张表(SQLite/MySQL 双脚本)及 CRUD 接口 - 按邮件发件人匹配账单类型,未命中忽略;发件人规则可配置解压密码与默认分类/标签 - 支持 IMAP IDLE 实时监听、定时兜底与手动立即检查;拉取范围可配置(仅新邮件/最近 N 天/最近 N 封) - 附件保存至本地 data/email-bill,解析预览后手动导入,复用账单解析与规则引擎 - 立即检查通过 SSE 实时推送执行进度 - 前端新增「邮箱账单」页(邮箱配置/发件人规则/待处理账单三 Tab),支持子路由与 Tab 偏好记忆,配置弹窗提供常用 IMAP 服务器候选项 - 新用户注册自动初始化默认发件人规则(支付宝/微信/美团/招商银行/交通银行) - 抽取共用账单预览与结果组件,供上传导入与邮箱账单复用 同时包含: - 统计新增口径筛选(仅计入/全部/仅不计入),支付方式改为后端动态获取 - 统一实体 createdAt/updatedAt 位置至 id 之后,调整部分日志级别
This commit is contained in:
@@ -10,6 +10,8 @@ import { LineChart, PieChart, BarChart } from 'echarts/charts'
|
||||
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
||||
import { getStatisticsSummary, getStatisticsGroup } from '../api/statistics'
|
||||
import { getCategoryList } from '../api/category'
|
||||
import { getPaymentMethods } from '../api/transaction'
|
||||
import { getPaymentLabel } from '../utils/payment'
|
||||
|
||||
// 注册 ECharts 所需组件
|
||||
use([CanvasRenderer, LineChart, PieChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
|
||||
@@ -37,6 +39,7 @@ const customRange = ref<[number, number] | null>(null) // 自定义起止时间
|
||||
const granularity = ref<'day' | 'week' | 'month' | 'quarter' | 'year'>('day') // 趋势粒度
|
||||
|
||||
// 过滤条件
|
||||
const filterStatsScope = ref<'INCLUDED' | 'ALL' | 'EXCLUDED'>('INCLUDED') // 统计口径
|
||||
const filterType = ref<string[]>([]) // 类型(多选)
|
||||
const filterCategory = ref<number[]>([]) // 分类(多选,0 表示未分类)
|
||||
const filterPaymentMethod = ref<string[]>([]) // 支付方式(多选)
|
||||
@@ -44,17 +47,24 @@ const filterMinAmount = ref<number | null>(null) // 最小金额
|
||||
const filterMaxAmount = ref<number | null>(null) // 最大金额
|
||||
const filterRemark = ref('') // 备注关键字
|
||||
|
||||
// 统计口径选项:仅计入 / 全部 / 仅不计入
|
||||
const statsScopeOptions = computed(() => [
|
||||
{ label: t('dashboard.statsScope.included'), value: 'INCLUDED' },
|
||||
{ label: t('dashboard.statsScope.all'), value: 'ALL' },
|
||||
{ label: t('dashboard.statsScope.excluded'), value: 'EXCLUDED' },
|
||||
])
|
||||
// 类型与支付方式选项
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('common.types.expense'), value: 'EXPENSE' },
|
||||
{ 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[]>([])
|
||||
// 支付方式选项:由后端返回的编码映射为当前语言标签,locale 变化时重新生成
|
||||
const paymentOptions = computed(() => {
|
||||
void locale.value
|
||||
return paymentMethodCodes.value.map((code) => ({ label: getPaymentLabel(code), value: code }))
|
||||
})
|
||||
|
||||
/** 分类选项数据结构 */
|
||||
interface CategoryOption {
|
||||
@@ -90,6 +100,7 @@ function buildCategoryFilter() {
|
||||
|
||||
/** 统计接口的过滤条件 */
|
||||
const filterParams = computed(() => ({
|
||||
statsScope: filterStatsScope.value,
|
||||
types: filterType.value.length > 0 ? filterType.value : undefined,
|
||||
...buildCategoryFilter(),
|
||||
paymentMethods: filterPaymentMethod.value.length > 0 ? filterPaymentMethod.value : undefined,
|
||||
@@ -272,6 +283,7 @@ function loadPrefs() {
|
||||
suppressRangeWatch = true
|
||||
if (['month', '3month', '12month', 'custom'].includes(p.rangeType)) rangeType.value = p.rangeType
|
||||
if (Array.isArray(p.customRange) && p.customRange.length === 2) customRange.value = p.customRange
|
||||
if (['INCLUDED', 'ALL', 'EXCLUDED'].includes(p.filterStatsScope)) filterStatsScope.value = p.filterStatsScope
|
||||
if (Array.isArray(p.filterType)) filterType.value = p.filterType
|
||||
if (Array.isArray(p.filterCategory)) filterCategory.value = p.filterCategory
|
||||
if (Array.isArray(p.filterPaymentMethod)) filterPaymentMethod.value = p.filterPaymentMethod
|
||||
@@ -291,6 +303,7 @@ function savePrefs() {
|
||||
rangeType: rangeType.value,
|
||||
customRange: customRange.value,
|
||||
granularity: granularity.value,
|
||||
filterStatsScope: filterStatsScope.value,
|
||||
filterType: filterType.value,
|
||||
filterCategory: filterCategory.value,
|
||||
filterPaymentMethod: filterPaymentMethod.value,
|
||||
@@ -313,8 +326,19 @@ async function fetchCategories() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从后端加载支付方式列表,供筛选下拉使用 */
|
||||
async function loadPaymentMethods() {
|
||||
try {
|
||||
const res: any = await getPaymentMethods()
|
||||
paymentMethodCodes.value = res.data || []
|
||||
} catch (e) {
|
||||
// 静默处理,支付方式下拉保持为空
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置所有过滤条件(保留时间范围与粒度) */
|
||||
function handleResetFilters() {
|
||||
filterStatsScope.value = 'INCLUDED'
|
||||
filterType.value = []
|
||||
filterCategory.value = []
|
||||
filterPaymentMethod.value = []
|
||||
@@ -334,7 +358,7 @@ watch(rangeType, (v) => {
|
||||
// 范围、粒度或过滤条件变化时重新加载
|
||||
watch([() => range.value.startTime, () => range.value.endTime, granularity, filterParams], fetchData)
|
||||
// 任一偏好变化时自动保存
|
||||
watch([rangeType, customRange, granularity, filterType, filterCategory, filterPaymentMethod, filterMinAmount, filterMaxAmount, filterRemark], savePrefs, { deep: true })
|
||||
watch([rangeType, customRange, granularity, filterStatsScope, filterType, filterCategory, filterPaymentMethod, filterMinAmount, filterMaxAmount, filterRemark], savePrefs, { deep: true })
|
||||
// 语言切换时重绘图表,更新标题与图例文案
|
||||
watch(locale, () => {
|
||||
updatePieChart()
|
||||
@@ -345,6 +369,7 @@ watch(locale, () => {
|
||||
onMounted(async () => {
|
||||
loadPrefs()
|
||||
fetchCategories()
|
||||
loadPaymentMethods()
|
||||
await nextTick()
|
||||
suppressRangeWatch = false
|
||||
fetchData()
|
||||
@@ -372,6 +397,12 @@ onMounted(async () => {
|
||||
<!-- 过滤条件(偏好自动记忆) -->
|
||||
<NCard :bordered="false" style="margin-bottom: 16px;">
|
||||
<NSpace align="center" :wrap="true" :size="[12, 12]">
|
||||
<NSelect
|
||||
v-model:value="filterStatsScope"
|
||||
:options="statsScopeOptions"
|
||||
:placeholder="t('dashboard.filterStatsScope')"
|
||||
style="width: 160px;"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value="filterType"
|
||||
:options="typeOptions"
|
||||
@@ -397,6 +428,7 @@ onMounted(async () => {
|
||||
:options="paymentOptions"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
:max-tag-count="2"
|
||||
:placeholder="t('dashboard.filterPaymentMethod')"
|
||||
style="width: 200px;"
|
||||
|
||||
Reference in New Issue
Block a user