feat:新增ai分析,新增储值账户,优化通知规则
This commit is contained in:
@@ -217,7 +217,7 @@ const sheetStyle = computed(() => {
|
||||
input-class="text-2xl font-bold"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="text-stone-400 text-sm">楼</span>
|
||||
<span class="text-stone-400 text-sm">¥</span>
|
||||
</template>
|
||||
</EchoInput>
|
||||
|
||||
|
||||
@@ -1,5 +1,102 @@
|
||||
<script setup>
|
||||
// 分析页当前仍是占位页,先展示未来 AI 财务顾问的交互方向。
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { askFinanceAssistant, hasAiAccess } from '../services/ai/aiService.js'
|
||||
import { useSettingsStore } from '../stores/settings.js'
|
||||
import { useTransactionStore } from '../stores/transactions.js'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const transactionStore = useTransactionStore()
|
||||
const { sortedTransactions } = storeToRefs(transactionStore)
|
||||
|
||||
const prompt = ref('')
|
||||
const sending = ref(false)
|
||||
let messageSeed = 0
|
||||
|
||||
const quickPrompts = [
|
||||
'帮我总结这周花钱最多的三类项目。',
|
||||
'最近有哪些支出值得我留意?',
|
||||
'我这个月的餐饮支出高吗?',
|
||||
]
|
||||
|
||||
const createMessage = (role, content, extra = {}) => ({
|
||||
id: `msg-${Date.now()}-${messageSeed += 1}`,
|
||||
role,
|
||||
content,
|
||||
...extra,
|
||||
})
|
||||
|
||||
const messages = ref([
|
||||
createMessage(
|
||||
'assistant',
|
||||
'可以直接问我账本问题,比如“这周花钱最多的三类项目是什么?”或“最近有哪些支出值得注意?”。',
|
||||
),
|
||||
])
|
||||
|
||||
const aiReady = computed(() => hasAiAccess(settingsStore))
|
||||
const hasTransactions = computed(() => sortedTransactions.value.length > 0)
|
||||
const canSend = computed(() => !!prompt.value.trim() && !sending.value)
|
||||
|
||||
const replaceMessage = (messageId, nextMessage) => {
|
||||
messages.value = messages.value.map((item) => (item.id === messageId ? nextMessage : item))
|
||||
}
|
||||
|
||||
const sendPrompt = async (rawPrompt = prompt.value) => {
|
||||
const question = String(rawPrompt || '').trim()
|
||||
if (!question || sending.value) return
|
||||
|
||||
const conversation = messages.value
|
||||
.filter((item) => !item.pending)
|
||||
.slice(-6)
|
||||
.map(({ role, content }) => ({ role, content }))
|
||||
|
||||
messages.value.push(createMessage('user', question))
|
||||
prompt.value = ''
|
||||
|
||||
if (!aiReady.value) {
|
||||
messages.value.push(
|
||||
createMessage('assistant', '先去设置页填写 DeepSeek API Key,然后这里就可以直接对话。'),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const pendingMessage = createMessage('assistant', '正在整理本地账本并生成回答...', {
|
||||
pending: true,
|
||||
})
|
||||
messages.value.push(pendingMessage)
|
||||
sending.value = true
|
||||
|
||||
try {
|
||||
const result = await askFinanceAssistant({
|
||||
question,
|
||||
transactions: sortedTransactions.value,
|
||||
conversation,
|
||||
})
|
||||
replaceMessage(pendingMessage.id, createMessage('assistant', result.content))
|
||||
} catch (error) {
|
||||
replaceMessage(
|
||||
pendingMessage.id,
|
||||
createMessage('assistant', error?.message || 'AI 请求失败,请稍后重试。'),
|
||||
)
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickPrompt = (value) => {
|
||||
void sendPrompt(value)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
void sendPrompt()
|
||||
}
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
void sendPrompt()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,71 +109,89 @@
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-extrabold text-stone-800">AI 财务顾问</h2>
|
||||
<p class="text-xs text-stone-400">即将接入对话分析与自动分类</p>
|
||||
<p class="text-xs text-stone-400">直接基于本地账本和 DeepSeek 对话分析。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto space-y-5 pr-1 pb-2">
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-stone-100 flex items-center justify-center text-stone-400 mt-1 shrink-0"
|
||||
>
|
||||
<i class="ph-fill ph-robot" />
|
||||
</div>
|
||||
<div
|
||||
class="bg-white p-4 rounded-2xl rounded-tl-none shadow-sm border border-stone-100 text-sm text-stone-600 leading-relaxed max-w-[85%]"
|
||||
>
|
||||
<p>我已经整理了你最近 30 天的消费记录。</p>
|
||||
<div class="mt-3 bg-stone-50 rounded-xl p-3 border border-stone-100">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="ph-fill ph-chart-line text-orange-400" />
|
||||
<span class="font-bold text-stone-700 text-xs">本月摘要</span>
|
||||
</div>
|
||||
<p class="text-xs">
|
||||
餐饮与通勤是主要支出来源,夜间消费频率明显高于白天,预算消耗速度偏快。
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||
<div class="bg-white rounded-2xl border border-stone-100 p-4 shadow-sm">
|
||||
<p class="text-[11px] font-bold text-stone-400">账本样本</p>
|
||||
<p class="text-lg font-extrabold text-stone-800 mt-1">{{ sortedTransactions.length }}</p>
|
||||
<p class="text-[11px] text-stone-400 mt-1">聊天时会带上最近 80 条记录摘要</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl border border-stone-100 p-4 shadow-sm">
|
||||
<p class="text-[11px] font-bold text-stone-400">AI 状态</p>
|
||||
<p class="text-lg font-extrabold mt-1" :class="aiReady ? 'text-emerald-600' : 'text-amber-600'">
|
||||
{{ aiReady ? '已连接' : '待配置' }}
|
||||
</p>
|
||||
<p class="text-[11px] text-stone-400 mt-1">
|
||||
{{ aiReady ? '使用设置页里的 DeepSeek Key 直接调用。' : '先去设置页填写 DeepSeek API Key。' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mb-4">
|
||||
<button
|
||||
v-for="item in quickPrompts"
|
||||
:key="item"
|
||||
class="px-3 py-2 rounded-full bg-white border border-stone-100 text-xs font-bold text-stone-600 shadow-sm"
|
||||
@click="handleQuickPrompt(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasTransactions" class="mb-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
|
||||
当前还没有账本记录。聊天功能可以使用,但回答只会提示你先开始记账。
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto space-y-4 pr-1 pb-2">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="flex gap-3"
|
||||
:class="message.role === 'user' ? 'justify-end' : 'justify-start'"
|
||||
>
|
||||
<template v-if="message.role === 'assistant'">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-stone-100 flex items-center justify-center text-stone-400 mt-1 shrink-0"
|
||||
>
|
||||
<i :class="['ph-fill', message.pending ? 'ph-circle-notch animate-spin' : 'ph-robot']" />
|
||||
</div>
|
||||
<p class="mt-3">下一步会支持自动分类、标签建议、异常支出提醒和自然语言问答。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex gap-3 flex-row-reverse">
|
||||
<div
|
||||
class="bg-stone-700 text-white p-4 rounded-2xl rounded-tr-none shadow-lg text-sm max-w-[85%]"
|
||||
:class="[
|
||||
'max-w-[85%] rounded-2xl p-4 text-sm leading-relaxed shadow-sm',
|
||||
message.role === 'user'
|
||||
? 'bg-stone-800 text-white rounded-tr-none'
|
||||
: 'bg-white border border-stone-100 text-stone-700 rounded-tl-none',
|
||||
]"
|
||||
>
|
||||
帮我总结这周花钱最多的三类项目。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-stone-100 flex items-center justify-center text-stone-400 mt-1 shrink-0"
|
||||
>
|
||||
<i class="ph-fill ph-robot" />
|
||||
</div>
|
||||
<div
|
||||
class="bg-white p-4 rounded-2xl rounded-tl-none shadow-sm border border-stone-100 text-sm text-stone-600 leading-relaxed max-w-[85%]"
|
||||
>
|
||||
<p>示例输出会像这样:</p>
|
||||
<ul class="list-disc list-inside text-xs space-y-1 marker:text-orange-400 mt-2">
|
||||
<li>餐饮:¥428,集中在晚餐和外卖。</li>
|
||||
<li>通勤:¥156,工作日为主。</li>
|
||||
<li>一般支出:¥98,主要是零碎网购和订阅扣费。</li>
|
||||
</ul>
|
||||
<p class="whitespace-pre-wrap">{{ message.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 relative shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入想分析的问题..."
|
||||
class="w-full pl-5 pr-12 py-3.5 rounded-full bg-white border border-stone-100 focus:outline-none focus:border-orange-300 focus:ring-4 focus:ring-orange-50 transition text-sm shadow-sm text-stone-700 font-medium placeholder-stone-300"
|
||||
<div class="mt-3 relative shrink-0">
|
||||
<textarea
|
||||
v-model="prompt"
|
||||
rows="2"
|
||||
placeholder="输入你的账本问题,例如:这周哪里花得最多?"
|
||||
class="w-full pl-5 pr-14 py-3.5 rounded-3xl bg-white border border-stone-100 focus:outline-none focus:border-orange-300 focus:ring-4 focus:ring-orange-50 transition text-sm shadow-sm text-stone-700 font-medium placeholder-stone-300 resize-none"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
<button
|
||||
class="absolute right-2 top-2 w-9 h-9 bg-gradient-warm text-white rounded-full flex items-center justify-center hover:scale-105 transition shadow-md"
|
||||
class="absolute right-3 bottom-3 w-10 h-10 rounded-full flex items-center justify-center transition shadow-md"
|
||||
:class="
|
||||
canSend && aiReady
|
||||
? 'bg-gradient-warm text-white'
|
||||
: 'bg-stone-200 text-stone-400 cursor-not-allowed'
|
||||
"
|
||||
:disabled="!canSend || !aiReady"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<i class="ph-bold ph-arrow-up" />
|
||||
<i :class="['ph-bold', sending ? 'ph-circle-notch animate-spin' : 'ph-arrow-up']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,12 @@ import { useTransactionStore } from '../stores/transactions'
|
||||
import { useTransactionEntry } from '../composables/useTransactionEntry'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
import { getAiStatusMeta } from '../config/aiStatus.js'
|
||||
import {
|
||||
getEntryTypeLabel,
|
||||
getTransferSummary,
|
||||
isStoredValueAccountType,
|
||||
} from '../config/ledger.js'
|
||||
import { getCategoryLabel, getCategoryMeta } from '../config/transactionCategories.js'
|
||||
import { bootstrapApp } from '../services/appBootstrap.js'
|
||||
|
||||
@@ -14,8 +20,14 @@ const router = useRouter()
|
||||
const transactionStore = useTransactionStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const uiStore = useUiStore()
|
||||
const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactions, sortedTransactions } =
|
||||
storeToRefs(transactionStore)
|
||||
const {
|
||||
totalIncome,
|
||||
totalExpense,
|
||||
todaysIncome,
|
||||
todaysExpense,
|
||||
latestTransactions,
|
||||
sortedTransactions,
|
||||
} = storeToRefs(transactionStore)
|
||||
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
|
||||
useTransactionEntry()
|
||||
|
||||
@@ -27,7 +39,6 @@ onMounted(async () => {
|
||||
await syncNotifications()
|
||||
|
||||
try {
|
||||
// App 回到前台时,刷新本地账本和待处理通知。
|
||||
appStateListener = await App.addListener('appStateChange', async ({ isActive }) => {
|
||||
if (isActive) {
|
||||
await transactionStore.hydrateTransactions()
|
||||
@@ -88,7 +99,7 @@ const periodExpense = computed(() => {
|
||||
if (cycle === 'none' || !start) return totalExpense.value || 0
|
||||
|
||||
return (sortedTransactions.value || []).reduce((sum, tx) => {
|
||||
if (tx.amount >= 0) return sum
|
||||
if (tx.amount >= 0 || tx.impactExpense === false) return sum
|
||||
return new Date(tx.date) >= start ? sum + tx.amount : sum
|
||||
}, 0)
|
||||
})
|
||||
@@ -123,6 +134,55 @@ const formatTime = (value) =>
|
||||
new Date(value),
|
||||
)
|
||||
|
||||
const resolveAiBadge = (transaction) => {
|
||||
if (!transaction?.aiStatus || ['idle', 'failed'].includes(transaction.aiStatus)) {
|
||||
return null
|
||||
}
|
||||
return getAiStatusMeta(transaction.aiStatus)
|
||||
}
|
||||
|
||||
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 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 goSettings = () => {
|
||||
router.push({ name: 'settings' })
|
||||
}
|
||||
@@ -275,9 +335,39 @@ const openTransactionDetail = (tx) => {
|
||||
<i :class="['ph-fill text-lg', getCategoryMeta(tx.category).icon]" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-stone-800 text-sm">{{ tx.merchant }}</p>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<p class="font-bold text-stone-800 text-sm">{{ tx.merchant }}</p>
|
||||
<span
|
||||
v-if="resolveLedgerBadge(tx)"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-bold"
|
||||
:class="resolveLedgerBadge(tx).className"
|
||||
>
|
||||
<i :class="['ph-bold text-[10px]', resolveLedgerBadge(tx).icon]" />
|
||||
{{ resolveLedgerBadge(tx).label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="resolveAiBadge(tx)"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-bold"
|
||||
:class="resolveAiBadge(tx).className"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'ph-bold text-[10px]',
|
||||
resolveAiBadge(tx).icon,
|
||||
resolveAiBadge(tx).iconClassName,
|
||||
]"
|
||||
/>
|
||||
{{ resolveAiBadge(tx).label }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-stone-400">
|
||||
{{ formatTime(tx.date) }} 路 {{ getCategoryLabel(tx.category) }}
|
||||
{{ formatTime(tx.date) }} · {{ getCategoryLabel(tx.category) }}
|
||||
</p>
|
||||
<p v-if="formatLedgerHint(tx)" class="text-[10px] text-stone-400 mt-1">
|
||||
{{ formatLedgerHint(tx) }}
|
||||
</p>
|
||||
<p v-if="formatAiHint(tx)" class="text-[10px] text-stone-400 mt-1">
|
||||
{{ formatAiHint(tx) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,7 +407,7 @@ const openTransactionDetail = (tx) => {
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold text-stone-800 text-sm">
|
||||
{{ notif.channel }} 路 {{ formatTime(notif.createdAt) }}
|
||||
{{ notif.channel }} · {{ formatTime(notif.createdAt) }}
|
||||
</h4>
|
||||
<p class="text-xs text-stone-400 leading-relaxed">
|
||||
{{ notif.text }}
|
||||
@@ -359,7 +449,7 @@ const openTransactionDetail = (tx) => {
|
||||
</div>
|
||||
<div class="z-10">
|
||||
<h4 class="font-bold text-stone-700">AI 财务顾问</h4>
|
||||
<p class="text-xs text-stone-400 mt-1">查看消费总结、自动分类建议和可执行的预算提醒。</p>
|
||||
<p class="text-xs text-stone-400 mt-1">现在可直接对话分析账本,并查看自动分类建议和预算提醒。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user