feat:新增ai分析,新增储值账户,优化通知规则

This commit is contained in:
2026-03-12 14:03:01 +08:00
parent 6a00875246
commit 6ca962a187
22 changed files with 1294 additions and 237 deletions

View File

@@ -1,6 +1,12 @@
<script setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { getAiStatusMeta } from '../config/aiStatus.js'
import {
getEntryTypeLabel,
getTransferSummary,
isStoredValueAccountType,
} from '../config/ledger.js'
import { CATEGORY_CHIPS, getCategoryLabel } from '../config/transactionCategories.js'
import { useTransactionStore } from '../stores/transactions'
import { useUiStore } from '../stores/ui'
@@ -19,14 +25,62 @@ const toggleTodayOnly = () => {
filters.value.showOnlyToday = !filters.value.showOnlyToday
}
const formatAmount = (value) =>
`${value >= 0 ? '+' : '-'}${Math.abs(value || 0).toFixed(2)}`
const formatAmount = (value) => `${value >= 0 ? '+' : '-'} ¥${Math.abs(value || 0).toFixed(2)}`
const formatTime = (value) =>
new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(
new Date(value),
)
const resolveLedgerBadge = (transaction) => {
if (transaction.entryType === 'transfer') {
return {
label: getEntryTypeLabel(transaction.entryType),
icon: 'ph-arrows-left-right',
className: 'bg-cyan-50 text-cyan-700 border-cyan-200',
}
}
if (isStoredValueAccountType(transaction.fundSourceType)) {
return {
label: '储值支付',
icon: 'ph-wallet',
className: 'bg-teal-50 text-teal-700 border-teal-200',
}
}
return null
}
const resolveAiBadge = (transaction) => {
if (!transaction?.aiStatus || ['idle', 'failed'].includes(transaction.aiStatus)) {
return null
}
return getAiStatusMeta(transaction.aiStatus)
}
const formatLedgerHint = (transaction) => {
if (transaction.entryType === 'transfer') {
return getTransferSummary(transaction)
}
if (isStoredValueAccountType(transaction.fundSourceType)) {
return `${transaction.fundSourceName || '储值账户'}扣款`
}
return ''
}
const formatAiHint = (transaction) => {
if (transaction.aiStatus === 'running') {
return '正在补全分类与标签'
}
if (transaction.aiStatus === 'suggested' && transaction.aiCategory) {
return `建议分类:${getCategoryLabel(transaction.aiCategory)}`
}
if (transaction.aiStatus === 'applied') {
const tags = (transaction.aiTags || []).slice(0, 2).join(' · ')
return tags ? `已补全:${tags}` : '已完成自动补全'
}
return ''
}
const handleEdit = (item) => {
uiStore.openAddEntry(item.id)
}
@@ -77,7 +131,7 @@ const hasData = computed(() => groupedTransactions.value.length > 0)
</div>
<div class="flex items-center justify-between text-[11px] text-stone-400 font-bold px-1">
<span>按一条记录可快速编辑</span>
<span>按一条记录可快速编辑</span>
<button
class="flex items-center gap-1 px-2 py-1 rounded-full border border-stone-200"
:class="filters.showOnlyToday ? 'bg-gradient-warm text-white border-transparent' : 'bg-white'"
@@ -103,7 +157,7 @@ const hasData = computed(() => groupedTransactions.value.length > 0)
<div>
<h4 class="text-sm font-bold text-stone-500">{{ group.label }}</h4>
<p class="text-[10px] text-stone-400">
支出 {{ group.totalExpense.toFixed(2) }}
支出 ¥{{ group.totalExpense.toFixed(2) }}
</p>
</div>
<span class="text-xs text-stone-300 font-bold">{{ group.dayKey }}</span>
@@ -128,18 +182,48 @@ const hasData = computed(() => groupedTransactions.value.length > 0)
/>
</div>
<div>
<p class="font-bold text-stone-800 text-sm">
{{ item.merchant }}
</p>
<div class="flex items-center gap-2 flex-wrap">
<p class="font-bold text-stone-800 text-sm">
{{ item.merchant }}
</p>
<span
v-if="resolveLedgerBadge(item)"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-bold"
:class="resolveLedgerBadge(item).className"
>
<i :class="['ph-bold text-[10px]', resolveLedgerBadge(item).icon]" />
{{ resolveLedgerBadge(item).label }}
</span>
<span
v-if="resolveAiBadge(item)"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-bold"
:class="resolveAiBadge(item).className"
>
<i
:class="[
'ph-bold text-[10px]',
resolveAiBadge(item).icon,
resolveAiBadge(item).iconClassName,
]"
/>
{{ resolveAiBadge(item).label }}
</span>
</div>
<p class="text-xs text-stone-400 mt-0.5">
{{ formatTime(item.date) }}
<span v-if="item.category" class="text-stone-300">
{{ getCategoryLabel(item.category) }}
· {{ getCategoryLabel(item.category) }}
</span>
<span v-if="item.note" class="text-stone-300">
{{ item.note }}
· {{ item.note }}
</span>
</p>
<p v-if="formatLedgerHint(item)" class="text-[10px] text-stone-400 mt-1">
{{ formatLedgerHint(item) }}
</p>
<p v-if="formatAiHint(item)" class="text-[10px] text-stone-400 mt-1">
{{ formatAiHint(item) }}
</p>
</div>
</div>
<div class="text-right">