fix:页面优化

This commit is contained in:
2026-03-12 15:36:36 +08:00
parent 6c209d8781
commit 5d45ba4731
5 changed files with 95 additions and 84 deletions

View File

@@ -3,7 +3,7 @@ import { computed, reactive, ref, watch } from 'vue'
import { import {
DEFAULT_TRANSACTION_CATEGORY, DEFAULT_TRANSACTION_CATEGORY,
TRANSACTION_CATEGORIES, TRANSACTION_CATEGORIES,
getCategoryLabel, getCategoryMeta,
} from '../config/transactionCategories.js' } from '../config/transactionCategories.js'
import { useTransactionStore } from '../stores/transactions' import { useTransactionStore } from '../stores/transactions'
import { useUiStore } from '../stores/ui' import { useUiStore } from '../stores/ui'
@@ -19,7 +19,11 @@ const dragStartY = ref(0)
const dragging = ref(false) const dragging = ref(false)
const dragOffset = ref(0) const dragOffset = ref(0)
const categories = TRANSACTION_CATEGORIES const categories = computed(() =>
TRANSACTION_CATEGORIES.filter((category) =>
form.type === 'income' ? ['Income', 'Uncategorized'].includes(category.value) : category.value !== 'Income',
),
)
const toDatetimeLocal = (value) => { const toDatetimeLocal = (value) => {
const date = value ? new Date(value) : new Date() const date = value ? new Date(value) : new Date()
@@ -65,6 +69,19 @@ const hydrateForm = () => {
watch(editingId, hydrateForm, { immediate: true }) watch(editingId, hydrateForm, { immediate: true })
watch(
() => form.type,
(nextType) => {
if (nextType === 'income' && form.category !== 'Income') {
form.category = 'Income'
return
}
if (nextType === 'expense' && form.category === 'Income') {
form.category = DEFAULT_TRANSACTION_CATEGORY
}
},
)
const closePanel = () => { const closePanel = () => {
uiStore.closeAddEntry() uiStore.closeAddEntry()
} }
@@ -181,9 +198,9 @@ const sheetStyle = computed(() => {
<button class="text-stone-400 text-sm font-bold" @click="closePanel">关闭</button> <button class="text-stone-400 text-sm font-bold" @click="closePanel">关闭</button>
</div> </div>
<div class="flex items-center gap-2 mb-6 bg-stone-100 rounded-3xl p-1"> <div class="grid grid-cols-2 gap-2 mb-6 bg-stone-100 rounded-[26px] p-1.5">
<button <button
class="flex-1 py-2 rounded-2xl text-sm font-bold transition" class="inline-flex items-center justify-center gap-2 min-h-[48px] rounded-[22px] text-sm font-bold transition"
:class=" :class="
form.type === 'expense' form.type === 'expense'
? 'bg-white shadow text-rose-500' ? 'bg-white shadow text-rose-500'
@@ -194,7 +211,7 @@ const sheetStyle = computed(() => {
<i class="ph-bold ph-arrow-up-right" /> 支出 <i class="ph-bold ph-arrow-up-right" /> 支出
</button> </button>
<button <button
class="flex-1 py-2 rounded-2xl text-sm font-bold transition" class="inline-flex items-center justify-center gap-2 min-h-[48px] rounded-[22px] text-sm font-bold transition"
:class=" :class="
form.type === 'income' form.type === 'income'
? 'bg-white shadow text-emerald-500' ? 'bg-white shadow text-emerald-500'
@@ -229,11 +246,11 @@ const sheetStyle = computed(() => {
<div> <div>
<span class="text-xs font-bold text-stone-400">分类</span> <span class="text-xs font-bold text-stone-400">分类</span>
<div class="mt-2 flex gap-2 overflow-x-auto hide-scrollbar pb-1"> <div class="mt-3 grid grid-cols-3 gap-3 sm:grid-cols-4">
<button <button
v-for="category in categories" v-for="category in categories"
:key="category.value" :key="category.value"
class="px-4 py-2 rounded-2xl text-xs font-bold border transition" class="flex min-h-[78px] flex-col items-center justify-center gap-2 rounded-[24px] border px-3 py-3 text-xs font-bold leading-snug transition text-center"
:class=" :class="
form.category === category.value form.category === category.value
? 'bg-gradient-warm text-white border-transparent' ? 'bg-gradient-warm text-white border-transparent'
@@ -241,7 +258,8 @@ const sheetStyle = computed(() => {
" "
@click="form.category = category.value" @click="form.category = category.value"
> >
{{ getCategoryLabel(category.value) }} <i :class="['ph-fill text-lg leading-none', getCategoryMeta(category.value).icon]" />
<span class="block leading-tight">{{ getCategoryMeta(category.value).label }}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -113,38 +113,17 @@ const handleKeydown = (event) => {
</div> </div>
</div> </div>
<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 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>
<div
v-if="!aiReady"
class="mb-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700"
>
先去设置页填写 DeepSeek API Key这里才能真正发送提问
</div>
<div class="flex-1 overflow-y-auto space-y-4 pr-1 pb-2"> <div class="flex-1 overflow-y-auto space-y-4 pr-1 pb-2">
<div <div
v-for="message in messages" v-for="message in messages"
@@ -173,7 +152,19 @@ const handleKeydown = (event) => {
</div> </div>
</div> </div>
<div class="mt-3 relative shrink-0"> <div class="mt-4 shrink-0 space-y-3">
<div class="flex gap-2 overflow-x-auto hide-scrollbar pb-1">
<button
v-for="item in quickPrompts"
:key="item"
class="shrink-0 rounded-full bg-white border border-stone-100 px-3 py-2 text-xs font-bold text-stone-600 shadow-sm"
@click="handleQuickPrompt(item)"
>
{{ item }}
</button>
</div>
<div class="relative">
<textarea <textarea
v-model="prompt" v-model="prompt"
rows="2" rows="2"
@@ -195,4 +186,5 @@ const handleKeydown = (event) => {
</button> </button>
</div> </div>
</div> </div>
</div>
</template> </template>

View File

@@ -12,6 +12,8 @@ import {
getEntryTypeLabel, getEntryTypeLabel,
getTransferSummary, getTransferSummary,
isStoredValueAccountType, isStoredValueAccountType,
shouldCountAsExpense,
shouldCountAsIncome,
} from '../config/ledger.js' } from '../config/ledger.js'
import { getCategoryLabel, getCategoryMeta } from '../config/transactionCategories.js' import { getCategoryLabel, getCategoryMeta } from '../config/transactionCategories.js'
import { bootstrapApp } from '../services/appBootstrap.js' import { bootstrapApp } from '../services/appBootstrap.js'
@@ -104,6 +106,28 @@ const periodExpense = computed(() => {
}, 0) }, 0)
}) })
const currentMonthIncome = computed(() =>
(sortedTransactions.value || []).reduce((sum, tx) => {
const date = new Date(tx.date)
const now = new Date()
if (date.getFullYear() !== now.getFullYear() || date.getMonth() !== now.getMonth()) return sum
return shouldCountAsIncome(tx) ? sum + tx.amount : sum
}, 0),
)
const currentMonthExpense = computed(() =>
Math.abs(
(sortedTransactions.value || []).reduce((sum, tx) => {
const date = new Date(tx.date)
const now = new Date()
if (date.getFullYear() !== now.getFullYear() || date.getMonth() !== now.getMonth()) return sum
return shouldCountAsExpense(tx) ? sum + tx.amount : sum
}, 0),
),
)
const currentMonthNet = computed(() => currentMonthIncome.value - currentMonthExpense.value)
const budgetUsage = computed(() => { const budgetUsage = computed(() => {
const expense = Math.abs(periodExpense.value || 0) const expense = Math.abs(periodExpense.value || 0)
const budget = monthlyBudget.value || 0 const budget = monthlyBudget.value || 0
@@ -111,7 +135,6 @@ const budgetUsage = computed(() => {
return Math.min(expense / budget, 1) return Math.min(expense / budget, 1)
}) })
const balance = computed(() => (totalIncome.value || 0) + (totalExpense.value || 0))
const remainingBudget = computed(() => const remainingBudget = computed(() =>
Math.max((monthlyBudget.value || 0) - Math.abs(periodExpense.value || 0), 0), Math.max((monthlyBudget.value || 0) - Math.abs(periodExpense.value || 0), 0),
) )
@@ -191,10 +214,6 @@ const goList = () => {
router.push({ name: 'list' }) router.push({ name: 'list' })
} }
const goAnalysis = () => {
router.push({ name: 'analysis' })
}
const openTransactionDetail = (tx) => { const openTransactionDetail = (tx) => {
if (!tx?.id) return if (!tx?.id) return
uiStore.openAddEntry(tx.id) uiStore.openAddEntry(tx.id)
@@ -223,38 +242,37 @@ const openTransactionDetail = (tx) => {
</div> </div>
<div <div
class="w-full h-48 rounded-3xl bg-gradient-warm text-white p-6 shadow-xl shadow-orange-200/50 relative overflow-hidden group transition-all hover:shadow-orange-300/50" class="w-full rounded-[32px] bg-gradient-warm text-white p-6 shadow-xl shadow-orange-200/50 relative overflow-hidden"
> >
<div class="absolute -right-10 -top-10 w-40 h-40 bg-white opacity-10 rounded-full blur-2xl" /> <div class="absolute -right-10 -top-10 w-40 h-40 bg-white opacity-10 rounded-full blur-2xl" />
<div class="absolute left-0 bottom-0 w-full h-1/2 bg-gradient-to-t from-black/10 to-transparent" /> <div class="absolute left-0 bottom-0 w-full h-1/2 bg-gradient-to-t from-black/10 to-transparent" />
<div class="relative z-10 flex flex-col h-full justify-between"> <div class="relative z-10 space-y-5">
<div> <div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="text-white/80 text-xs font-bold tracking-widest uppercase">当前结余</p> <div>
<button <p class="text-white/80 text-xs font-bold tracking-widest uppercase">本月净结余</p>
class="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-md hover:bg-white/30 transition" <p class="text-[11px] text-white/70 mt-1">仅统计本月已入账的收入和支出</p>
>
<i class="ph-bold ph-eye" />
</button>
</div> </div>
<h2 class="text-4xl font-extrabold mt-2"> <span class="px-3 py-1 rounded-full bg-white/15 text-[11px] font-bold">本月口径</span>
{{ formatCurrency(balance) }} </div>
<h2 class="text-4xl font-extrabold mt-3">
{{ currentMonthNet >= 0 ? '' : '-' }}{{ formatCurrency(currentMonthNet) }}
</h2> </h2>
</div> </div>
<div class="flex gap-8"> <div class="grid grid-cols-2 gap-3">
<div> <div class="rounded-2xl bg-white/10 px-4 py-3 backdrop-blur-sm">
<p class="text-white/70 text-xs mb-0.5 flex items-center gap-1"> <p class="text-white/70 text-xs mb-0.5 flex items-center gap-1">
<i class="ph-bold ph-arrow-down-left" /> 收入 <i class="ph-bold ph-arrow-down-left" /> 收入
</p> </p>
<p class="font-bold text-lg">{{ formatCurrency(totalIncome) }}</p> <p class="font-bold text-lg">{{ formatCurrency(currentMonthIncome) }}</p>
</div> </div>
<div> <div class="rounded-2xl bg-white/10 px-4 py-3 backdrop-blur-sm">
<p class="text-white/70 text-xs mb-0.5 flex items-center gap-1"> <p class="text-white/70 text-xs mb-0.5 flex items-center gap-1">
<i class="ph-bold ph-arrow-up-right" /> 支出 <i class="ph-bold ph-arrow-up-right" /> 支出
</p> </p>
<p class="font-bold text-lg">{{ formatCurrency(Math.abs(totalExpense)) }}</p> <p class="font-bold text-lg">{{ formatCurrency(currentMonthExpense) }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -437,22 +455,5 @@ const openTransactionDetail = (tx) => {
</div> </div>
</div> </div>
<div
class="bg-white p-5 rounded-3xl shadow-sm border border-stone-100 flex flex-col justify-between h-32 relative overflow-hidden group"
@click="goAnalysis"
>
<div
class="absolute right-[-10px] top-[-10px] w-20 h-20 bg-purple-50 rounded-full blur-xl group-hover:bg-purple-100 transition"
/>
<div
class="w-10 h-10 rounded-full bg-purple-100 text-purple-600 flex items-center justify-center z-10"
>
<i class="ph-fill ph-robot text-xl" />
</div>
<div class="z-10">
<h4 class="font-bold text-stone-700">AI 财务顾问</h4>
<p class="text-xs text-stone-400 mt-1">现在可直接对话分析账本并查看自动分类建议和预算提醒</p>
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -117,7 +117,7 @@ const hasData = computed(() => groupedTransactions.value.length > 0)
<button <button
v-for="chip in categoryChips" v-for="chip in categoryChips"
:key="chip.value" :key="chip.value"
class="shrink-0 min-h-[44px] px-4 py-2.5 rounded-[22px] text-sm font-bold flex items-center gap-2 border transition whitespace-nowrap" class="shrink-0 min-h-[44px] px-4 py-2.5 rounded-[22px] text-sm font-bold inline-flex items-center justify-center gap-2 border transition whitespace-nowrap leading-none"
:class=" :class="
filters.category === chip.value filters.category === chip.value
? 'bg-gradient-warm text-white border-transparent shadow-sm shadow-orange-200/40' ? 'bg-gradient-warm text-white border-transparent shadow-sm shadow-orange-200/40'
@@ -125,8 +125,8 @@ const hasData = computed(() => groupedTransactions.value.length > 0)
" "
@click="setCategory(chip.value)" @click="setCategory(chip.value)"
> >
<i :class="['text-base', chip.icon]" /> <i :class="['text-base leading-none', chip.icon]" />
{{ chip.label }} <span class="inline-flex items-center leading-none">{{ chip.label }}</span>
</button> </button>
</div> </div>
@@ -188,25 +188,25 @@ const hasData = computed(() => groupedTransactions.value.length > 0)
</p> </p>
<span <span
v-if="resolveLedgerBadge(item)" v-if="resolveLedgerBadge(item)"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-bold" class="inline-flex items-center justify-center gap-1 px-2 py-1 rounded-full border text-[10px] font-bold leading-none"
:class="resolveLedgerBadge(item).className" :class="resolveLedgerBadge(item).className"
> >
<i :class="['ph-bold text-[10px]', resolveLedgerBadge(item).icon]" /> <i :class="['ph-bold text-[10px] leading-none', resolveLedgerBadge(item).icon]" />
{{ resolveLedgerBadge(item).label }} <span class="inline-flex items-center leading-none">{{ resolveLedgerBadge(item).label }}</span>
</span> </span>
<span <span
v-if="resolveAiBadge(item)" v-if="resolveAiBadge(item)"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-bold" class="inline-flex items-center justify-center gap-1 px-2 py-1 rounded-full border text-[10px] font-bold leading-none"
:class="resolveAiBadge(item).className" :class="resolveAiBadge(item).className"
> >
<i <i
:class="[ :class="[
'ph-bold text-[10px]', 'ph-bold text-[10px] leading-none',
resolveAiBadge(item).icon, resolveAiBadge(item).icon,
resolveAiBadge(item).iconClassName, resolveAiBadge(item).iconClassName,
]" ]"
/> />
{{ resolveAiBadge(item).label }} <span class="inline-flex items-center leading-none">{{ resolveAiBadge(item).label }}</span>
</span> </span>
</div> </div>
<p class="text-xs leading-5 text-stone-400"> <p class="text-xs leading-5 text-stone-400">