fix: 修复文件编码问题,确保导入语句格式正确
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
package com.echo.app.notification
|
package com.echo.app.notification
|
||||||
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|||||||
16
public/default-avatar.svg
Normal file
16
public/default-avatar.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="24" y1="20" x2="136" y2="140" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFB36B"/>
|
||||||
|
<stop offset="1" stop-color="#F06449"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ring" x1="38" y1="36" x2="120" y2="124" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFF7ED" stop-opacity="0.95"/>
|
||||||
|
<stop offset="1" stop-color="#FFE7D4" stop-opacity="0.72"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="8" y="8" width="144" height="144" rx="48" fill="url(#bg)"/>
|
||||||
|
<circle cx="80" cy="80" r="48" fill="url(#ring)"/>
|
||||||
|
<path d="M61 52H101V64H75V75H97V87H75V108H61V52Z" fill="#8A3412"/>
|
||||||
|
<path d="M103 52H117V108H103V52Z" fill="#8A3412" fill-opacity="0.25"/>
|
||||||
|
</svg>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { transformNotificationToTransaction } from '../src/services/notificationRuleService.js'
|
import { transformNotificationToTransaction } from '../src/services/notificationRuleService.js'
|
||||||
|
|
||||||
const cases = [
|
const cases = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const notificationRules = [
|
const notificationRules = [
|
||||||
{
|
{
|
||||||
id: 'alipay-expense',
|
id: 'alipay-expense',
|
||||||
label: '支付宝消费',
|
label: '支付宝消费',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import '@phosphor-icons/web/regular'
|
import '@phosphor-icons/web/regular'
|
||||||
import '@phosphor-icons/web/bold'
|
import '@phosphor-icons/web/bold'
|
||||||
@@ -10,7 +10,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
|||||||
import { defineCustomElements as jeepSqliteCE } from 'jeep-sqlite/loader'
|
import { defineCustomElements as jeepSqliteCE } from 'jeep-sqlite/loader'
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
|
||||||
// 注册 jeep-sqlite 自定义元素并保证 Web 端具备 Capacitor 环境
|
// 注册 jeep-sqlite 自定义元素,并为 Web 环境挂载 Capacitor 对象。
|
||||||
window.Capacitor = Capacitor
|
window.Capacitor = Capacitor
|
||||||
jeepSqliteCE(window)
|
jeepSqliteCE(window)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import notificationRulesSource from '../config/notificationRules.js'
|
import notificationRulesSource from '../config/notificationRules.js'
|
||||||
|
|
||||||
const fallbackRules = notificationRulesSource.map((rule) => ({
|
const fallbackRules = notificationRulesSource.map((rule) => ({
|
||||||
...rule,
|
...rule,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// 分析页目前为静态占位,实现基础对话 UI,后续接入真实 AI 逻辑
|
// 分析页当前仍是占位页,先展示未来 AI 财务顾问的交互方向。
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -12,13 +12,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-extrabold text-stone-800">AI 财务顾问</h2>
|
<h2 class="text-xl font-extrabold text-stone-800">AI 财务顾问</h2>
|
||||||
<p class="text-xs text-stone-400">Powered by Gemini</p>
|
<p class="text-xs text-stone-400">即将接入对话分析与自动分类</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Area -->
|
|
||||||
<div class="flex-1 overflow-y-auto space-y-5 pr-1 pb-2">
|
<div class="flex-1 overflow-y-auto space-y-5 pr-1 pb-2">
|
||||||
<!-- Bot Message -->
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<div
|
<div
|
||||||
class="w-8 h-8 rounded-full bg-stone-100 flex items-center justify-center text-stone-400 mt-1 shrink-0"
|
class="w-8 h-8 rounded-full bg-stone-100 flex items-center justify-center text-stone-400 mt-1 shrink-0"
|
||||||
@@ -28,30 +26,28 @@
|
|||||||
<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%]"
|
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>你好 Alex!👋 我分析了你本周的饮食记录。</p>
|
<p>我已经整理了你最近 30 天的消费记录。</p>
|
||||||
<div class="mt-3 bg-stone-50 rounded-xl p-3 border border-stone-100">
|
<div class="mt-3 bg-stone-50 rounded-xl p-3 border border-stone-100">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<i class="ph-fill ph-warning text-orange-400" />
|
<i class="ph-fill ph-chart-line text-orange-400" />
|
||||||
<span class="font-bold text-stone-700 text-xs">高热量警告</span>
|
<span class="font-bold text-stone-700 text-xs">本月摘要</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs">
|
<p class="text-xs">
|
||||||
周二和周四的晚餐摄入热量超标 30%,主要是因为“炸鸡”和“奶茶”。
|
餐饮与通勤是主要支出来源,夜间消费频率明显高于白天,预算消耗速度偏快。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3">建议今晚尝试清淡饮食,比如蔬菜沙拉或三文鱼。</p>
|
<p class="mt-3">下一步会支持自动分类、标签建议、异常支出提醒和自然语言问答。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Message -->
|
|
||||||
<div class="flex gap-3 flex-row-reverse">
|
<div class="flex gap-3 flex-row-reverse">
|
||||||
<div
|
<div
|
||||||
class="bg-stone-700 text-white p-4 rounded-2xl rounded-tr-none shadow-lg text-sm max-w-[85%]"
|
class="bg-stone-700 text-white p-4 rounded-2xl rounded-tr-none shadow-lg text-sm max-w-[85%]"
|
||||||
>
|
>
|
||||||
如果是自己做饭,有什么推荐的食谱吗?要简单的。
|
帮我总结这周花钱最多的三类项目。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bot Message 2 -->
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<div
|
<div
|
||||||
class="w-8 h-8 rounded-full bg-stone-100 flex items-center justify-center text-stone-400 mt-1 shrink-0"
|
class="w-8 h-8 rounded-full bg-stone-100 flex items-center justify-center text-stone-400 mt-1 shrink-0"
|
||||||
@@ -61,22 +57,20 @@
|
|||||||
<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%]"
|
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>
|
<p>示例输出会像这样:</p>
|
||||||
<p class="font-bold text-stone-800 mt-2 mb-1">🥑 牛油果虾仁拌饭</p>
|
<ul class="list-disc list-inside text-xs space-y-1 marker:text-orange-400 mt-2">
|
||||||
<ul class="list-disc list-inside text-xs space-y-1 marker:text-orange-400">
|
<li>餐饮:¥428,集中在晚餐和外卖。</li>
|
||||||
<li>热量:约 450 kcal</li>
|
<li>通勤:¥156,工作日为主。</li>
|
||||||
<li>耗时:10 分钟</li>
|
<li>一般支出:¥98,主要是零碎网购和订阅扣费。</li>
|
||||||
<li>成本:约 ¥18</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input Area -->
|
|
||||||
<div class="mt-2 relative shrink-0">
|
<div class="mt-2 relative shrink-0">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="输入消息..."
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -87,4 +81,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactio
|
|||||||
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
|
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
|
||||||
useTransactionEntry()
|
useTransactionEntry()
|
||||||
|
|
||||||
|
const defaultAvatar = '/default-avatar.svg'
|
||||||
let appStateListener = null
|
let appStateListener = null
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await bootstrapApp()
|
await bootstrapApp()
|
||||||
await syncNotifications()
|
await syncNotifications()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// App 浠庡悗鍙板洖鍒板墠鍙版椂锛岃嚜鍔ㄥ埛鏂版湰鍦拌处鏈拰閫氱煡闃熷垪
|
// App 回到前台时,刷新本地账本和待处理通知。
|
||||||
appStateListener = await App.addListener('appStateChange', async ({ isActive }) => {
|
appStateListener = await App.addListener('appStateChange', async ({ isActive }) => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
await transactionStore.hydrateTransactions()
|
await transactionStore.hydrateTransactions()
|
||||||
@@ -33,7 +35,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Web 鐜鎴栨彃浠朵笉鍙敤鏃跺拷鐣? console.warn('[notifications] appStateChange listener failed', error)
|
console.warn('[notifications] appStateChange listener failed', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,50 +49,47 @@ onBeforeUnmount(() => {
|
|||||||
const monthlyBudget = computed(() => settingsStore.monthlyBudget || 0)
|
const monthlyBudget = computed(() => settingsStore.monthlyBudget || 0)
|
||||||
const budgetResetCycle = computed(() => settingsStore.budgetResetCycle || 'monthly')
|
const budgetResetCycle = computed(() => settingsStore.budgetResetCycle || 'monthly')
|
||||||
|
|
||||||
const currentDayLabel = computed(
|
const currentDayLabel = computed(() =>
|
||||||
() =>
|
new Intl.DateTimeFormat('zh-CN', {
|
||||||
new Intl.DateTimeFormat('zh-CN', {
|
month: 'long',
|
||||||
month: 'long',
|
day: 'numeric',
|
||||||
day: 'numeric',
|
weekday: 'long',
|
||||||
weekday: 'long',
|
}).format(new Date()),
|
||||||
}).format(new Date()),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const periodStart = computed(() => {
|
const periodStart = computed(() => {
|
||||||
const cycle = budgetResetCycle.value
|
const cycle = budgetResetCycle.value
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
if (cycle === 'weekly') {
|
if (cycle === 'weekly') {
|
||||||
const day = now.getDay() || 7 // 鍛ㄤ竴=1锛屽懆鏃?7
|
const day = now.getDay() || 7
|
||||||
const diff = day - 1
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - (day - 1))
|
||||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cycle === 'monthly') {
|
if (cycle === 'monthly') {
|
||||||
const day = settingsStore.budgetMonthlyResetDay || 1
|
const day = settingsStore.budgetMonthlyResetDay || 1
|
||||||
const safeDay = Math.min(Math.max(Number(day) || 1, 1), 28)
|
const safeDay = Math.min(Math.max(Number(day) || 1, 1), 28)
|
||||||
const candidate = new Date(now.getFullYear(), now.getMonth(), safeDay)
|
const candidate = new Date(now.getFullYear(), now.getMonth(), safeDay)
|
||||||
if (now >= candidate) {
|
if (now >= candidate) return candidate
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
return new Date(now.getFullYear(), now.getMonth() - 1, safeDay)
|
return new Date(now.getFullYear(), now.getMonth() - 1, safeDay)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cycle === 'custom') {
|
if (cycle === 'custom') {
|
||||||
if (!settingsStore.budgetCustomStartDate) return null
|
if (!settingsStore.budgetCustomStartDate) return null
|
||||||
return new Date(settingsStore.budgetCustomStartDate)
|
return new Date(settingsStore.budgetCustomStartDate)
|
||||||
}
|
}
|
||||||
// none锛氫笉闄愬埗鍛ㄦ湡
|
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const periodExpense = computed(() => {
|
const periodExpense = computed(() => {
|
||||||
const cycle = budgetResetCycle.value
|
const cycle = budgetResetCycle.value
|
||||||
const start = periodStart.value
|
const start = periodStart.value
|
||||||
if (cycle === 'none' || !start) {
|
if (cycle === 'none' || !start) return totalExpense.value || 0
|
||||||
return totalExpense.value || 0
|
|
||||||
}
|
|
||||||
return (sortedTransactions.value || []).reduce((sum, tx) => {
|
return (sortedTransactions.value || []).reduce((sum, tx) => {
|
||||||
if (tx.amount >= 0) return sum
|
if (tx.amount >= 0) return sum
|
||||||
const date = new Date(tx.date)
|
return new Date(tx.date) >= start ? sum + tx.amount : sum
|
||||||
return date >= start ? sum + tx.amount : sum
|
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -108,17 +107,22 @@ const remainingBudget = computed(() =>
|
|||||||
|
|
||||||
const todayIncomeValue = computed(() => todaysIncome.value || 0)
|
const todayIncomeValue = computed(() => todaysIncome.value || 0)
|
||||||
const todayExpenseValue = computed(() => Math.abs(todaysExpense.value || 0))
|
const todayExpenseValue = computed(() => Math.abs(todaysExpense.value || 0))
|
||||||
|
const recentTransactions = computed(() => latestTransactions.value.slice(0, 4))
|
||||||
|
|
||||||
const formatCurrency = (value) => `楼 ${Math.abs(value || 0).toFixed(2)}`
|
const cycleLabel = computed(() => {
|
||||||
const formatAmount = (value) =>
|
if (budgetResetCycle.value === 'weekly') return '每周重置'
|
||||||
`${value >= 0 ? '+' : '-'} 楼${Math.abs(value || 0).toFixed(2)}`
|
if (budgetResetCycle.value === 'none') return '不重置'
|
||||||
|
if (budgetResetCycle.value === 'custom') return '自定义'
|
||||||
|
return '每月重置'
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatCurrency = (value) => `¥${Math.abs(value || 0).toFixed(2)}`
|
||||||
|
const formatAmount = (value) => `${value >= 0 ? '+' : '-'} ¥${Math.abs(value || 0).toFixed(2)}`
|
||||||
const formatTime = (value) =>
|
const formatTime = (value) =>
|
||||||
new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(
|
new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(
|
||||||
new Date(value),
|
new Date(value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const recentTransactions = computed(() => latestTransactions.value.slice(0, 4))
|
|
||||||
|
|
||||||
const goSettings = () => {
|
const goSettings = () => {
|
||||||
router.push({ name: 'settings' })
|
router.push({ name: 'settings' })
|
||||||
}
|
}
|
||||||
@@ -144,18 +148,15 @@ const openTransactionDetail = (tx) => {
|
|||||||
<p class="text-xs text-stone-400 font-bold tracking-wider">
|
<p class="text-xs text-stone-400 font-bold tracking-wider">
|
||||||
{{ currentDayLabel }}
|
{{ currentDayLabel }}
|
||||||
</p>
|
</p>
|
||||||
<h1 class="text-2xl font-extrabold text-stone-800">浠婃棩姒傝</h1>
|
<h1 class="text-2xl font-extrabold text-stone-800">今日概览</h1>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 rounded-full bg-orange-50 p-0.5 cursor-pointer border border-orange-100"
|
class="w-10 h-10 rounded-full bg-orange-50 p-0.5 cursor-pointer border border-orange-100"
|
||||||
@click="goSettings"
|
@click="goSettings"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="
|
:src="settingsStore.profileAvatar || defaultAvatar"
|
||||||
settingsStore.profileAvatar ||
|
class="rounded-full w-full h-full object-cover"
|
||||||
'https://api.dicebear.com/7.x/avataaars/svg?seed=Echo'
|
|
||||||
"
|
|
||||||
class="rounded-full"
|
|
||||||
alt="User"
|
alt="User"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +171,7 @@ const openTransactionDetail = (tx) => {
|
|||||||
<div class="relative z-10 flex flex-col h-full justify-between">
|
<div class="relative z-10 flex flex-col h-full justify-between">
|
||||||
<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>
|
<p class="text-white/80 text-xs font-bold tracking-widest uppercase">当前结余</p>
|
||||||
<button
|
<button
|
||||||
class="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-md hover:bg-white/30 transition"
|
class="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-md hover:bg-white/30 transition"
|
||||||
>
|
>
|
||||||
@@ -185,13 +186,13 @@ const openTransactionDetail = (tx) => {
|
|||||||
<div class="flex gap-8">
|
<div class="flex gap-8">
|
||||||
<div>
|
<div>
|
||||||
<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(totalIncome) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<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(Math.abs(totalExpense)) }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +202,7 @@ const openTransactionDetail = (tx) => {
|
|||||||
|
|
||||||
<div class="bg-white rounded-2xl p-4 shadow-sm border border-stone-100">
|
<div class="bg-white rounded-2xl p-4 shadow-sm border border-stone-100">
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<span class="text-xs font-bold text-stone-500">鏈湡棰勭畻</span>
|
<span class="text-xs font-bold text-stone-500">本期预算</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs font-bold text-stone-800">
|
<span class="text-xs font-bold text-stone-800">
|
||||||
{{ Math.round(budgetUsage * 100) }}%
|
{{ Math.round(budgetUsage * 100) }}%
|
||||||
@@ -210,7 +211,8 @@ const openTransactionDetail = (tx) => {
|
|||||||
class="text-[10px] text-stone-400 underline-offset-2 hover:text-stone-600"
|
class="text-[10px] text-stone-400 underline-offset-2 hover:text-stone-600"
|
||||||
@click="goSettings"
|
@click="goSettings"
|
||||||
>
|
>
|
||||||
鍘昏缃? </button>
|
去设置
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-stone-100 rounded-full h-2.5 overflow-hidden">
|
<div class="w-full bg-stone-100 rounded-full h-2.5 overflow-hidden">
|
||||||
@@ -220,37 +222,27 @@ const openTransactionDetail = (tx) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between mt-2 text-[10px] text-stone-400">
|
<div class="flex justify-between mt-2 text-[10px] text-stone-400">
|
||||||
<span>宸茬敤 {{ formatCurrency(Math.abs(periodExpense)) }}</span>
|
<span>已用 {{ formatCurrency(Math.abs(periodExpense)) }}</span>
|
||||||
<span>鍓╀綑 {{ formatCurrency(remainingBudget) }}</span>
|
<span>剩余 {{ formatCurrency(remainingBudget) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between mt-1 text-[10px] text-stone-400">
|
<div class="flex justify-between mt-1 text-[10px] text-stone-400">
|
||||||
<span>
|
<span>周期:{{ cycleLabel }}</span>
|
||||||
周期:
|
|
||||||
{{
|
|
||||||
budgetResetCycle === 'weekly'
|
|
||||||
? '每周重置'
|
|
||||||
: budgetResetCycle === 'none'
|
|
||||||
? '不重置'
|
|
||||||
: budgetResetCycle === 'custom'
|
|
||||||
? '自定义'
|
|
||||||
: '每月重置'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
class="text-[10px] text-stone-400 underline-offset-2 hover:text-stone-600"
|
class="text-[10px] text-stone-400 underline-offset-2 hover:text-stone-600"
|
||||||
@click="goSettings"
|
@click="goSettings"
|
||||||
>
|
>
|
||||||
鍘昏缃? </button>
|
去设置
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="bg-white rounded-2xl p-4 border border-stone-100 shadow-sm">
|
<div class="bg-white rounded-2xl p-4 border border-stone-100 shadow-sm">
|
||||||
<p class="text-xs text-stone-400 mb-2">浠婃棩鏀跺叆</p>
|
<p class="text-xs text-stone-400 mb-2">今日收入</p>
|
||||||
<p class="text-xl font-bold text-emerald-600">{{ formatCurrency(todayIncomeValue) }}</p>
|
<p class="text-xl font-bold text-emerald-600">{{ formatCurrency(todayIncomeValue) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-2xl p-4 border border-stone-100 shadow-sm">
|
<div class="bg-white rounded-2xl p-4 border border-stone-100 shadow-sm">
|
||||||
<p class="text-xs text-stone-400 mb-2">浠婃棩鏀嚭</p>
|
<p class="text-xs text-stone-400 mb-2">今日支出</p>
|
||||||
<p class="text-xl font-bold text-rose-600">- {{ formatCurrency(todayExpenseValue) }}</p>
|
<p class="text-xl font-bold text-rose-600">- {{ formatCurrency(todayExpenseValue) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,7 +254,7 @@ const openTransactionDetail = (tx) => {
|
|||||||
class="text-xs font-bold text-gradient-warm"
|
class="text-xs font-bold text-gradient-warm"
|
||||||
@click="goList"
|
@click="goList"
|
||||||
>
|
>
|
||||||
鏌ョ湅鍏ㄩ儴
|
查看全部
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="recentTransactions.length" class="space-y-4">
|
<div v-if="recentTransactions.length" class="space-y-4">
|
||||||
@@ -300,22 +292,19 @@ const openTransactionDetail = (tx) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm text-stone-400 text-center py-4">
|
<div v-else class="text-sm text-stone-400 text-center py-4">
|
||||||
杩樻病鏈夎褰曪紝蹇幓娣诲姞绗竴绗旀秷璐瑰惂锝?
|
还没有记录,去添加第一笔流水吧。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-end">
|
<div class="flex justify-between items-end">
|
||||||
<h3 class="font-bold text-lg text-stone-700">寰呯‘璁ら€氱煡</h3>
|
<h3 class="font-bold text-lg text-stone-700">待确认通知</h3>
|
||||||
<span class="text-[10px] bg-orange-100 text-orange-600 px-2 py-1 rounded-full font-bold">
|
<span class="text-[10px] bg-orange-100 text-orange-600 px-2 py-1 rounded-full font-bold">
|
||||||
鑷姩鎹曡幏
|
自动捕获
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="notifications.length" class="space-y-3">
|
||||||
v-if="notifications.length"
|
|
||||||
class="space-y-3"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="notif in notifications"
|
v-for="notif in notifications"
|
||||||
:key="notif.id"
|
:key="notif.id"
|
||||||
@@ -340,60 +329,38 @@ const openTransactionDetail = (tx) => {
|
|||||||
:disabled="processingId === notif.id"
|
:disabled="processingId === notif.id"
|
||||||
@click="confirmNotification(notif.id)"
|
@click="confirmNotification(notif.id)"
|
||||||
>
|
>
|
||||||
鍏ヨ处
|
入账
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="text-[11px] text-stone-400"
|
class="text-[11px] text-stone-400"
|
||||||
@click="dismissNotification(notif.id)"
|
@click="dismissNotification(notif.id)"
|
||||||
>
|
>
|
||||||
蹇界暐
|
忽略
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm text-stone-400 text-center py-4 border border-dashed border-stone-100 rounded-2xl">
|
<div v-else class="text-sm text-stone-400 text-center py-4 border border-dashed border-stone-100 rounded-2xl">
|
||||||
鏆傛棤鏂伴€氱煡锛屼韩鍙楁參鐢熸椿 鈽曪笍
|
暂无新通知,先安心生活。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<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
|
<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"
|
class="absolute right-[-10px] top-[-10px] w-20 h-20 bg-purple-50 rounded-full blur-xl group-hover:bg-purple-100 transition"
|
||||||
@click="goAnalysis"
|
/>
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-full bg-purple-100 text-purple-600 flex items-center justify-center z-10"
|
||||||
>
|
>
|
||||||
<div
|
<i class="ph-fill ph-robot text-xl" />
|
||||||
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 class="z-10">
|
||||||
<div
|
<h4 class="font-bold text-stone-700">AI 财务顾问</h4>
|
||||||
class="bg-white p-5 rounded-3xl shadow-sm border border-stone-100 flex flex-col justify-between h-32 relative overflow-hidden group"
|
<p class="text-xs text-stone-400 mt-1">查看消费总结、自动分类建议和可执行的预算提醒。</p>
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute right-[-10px] top-[-10px] w-20 h-20 bg-emerald-50 rounded-full blur-xl group-hover:bg-emerald-100 transition"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="w-10 h-10 rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center z-10"
|
|
||||||
>
|
|
||||||
<i class="ph-fill ph-carrot text-xl" />
|
|
||||||
</div>
|
|
||||||
<div class="z-10">
|
|
||||||
<h4 class="font-bold text-stone-700">鐑噺绠$悊</h4>
|
|
||||||
<p class="text-xs text-stone-400 mt-1">浠婃棩鍓╀綑 750 kcal</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,14 +94,7 @@ const hasAnyCategoryBudget = computed(() =>
|
|||||||
Object.values(categoryBudgets.value || {}).some((v) => (v || 0) > 0),
|
Object.values(categoryBudgets.value || {}).some((v) => (v || 0) > 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
const budgetCategories = [
|
const budgetCategories = BUDGET_CATEGORY_OPTIONS
|
||||||
{ value: 'Food', label: '餐饮' },
|
|
||||||
{ value: 'Transport', label: '通勤' },
|
|
||||||
{ value: 'Health', label: '健康' },
|
|
||||||
{ value: 'Groceries', label: '买菜' },
|
|
||||||
{ value: 'Entertainment', label: '娱乐' },
|
|
||||||
{ value: 'Uncategorized', label: '其他' },
|
|
||||||
]
|
|
||||||
const handleAvatarClick = () => {
|
const handleAvatarClick = () => {
|
||||||
if (fileInputRef.value) {
|
if (fileInputRef.value) {
|
||||||
fileInputRef.value.click()
|
fileInputRef.value.click()
|
||||||
@@ -155,7 +148,7 @@ onMounted(() => {
|
|||||||
<img
|
<img
|
||||||
:src="
|
:src="
|
||||||
settingsStore.profileAvatar ||
|
settingsStore.profileAvatar ||
|
||||||
'https://api.dicebear.com/7.x/avataaars/svg?seed=Echo'
|
'/default-avatar.svg'
|
||||||
"
|
"
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
class="w-16 h-16 rounded-full bg-stone-100 object-cover cursor-pointer"
|
class="w-16 h-16 rounded-full bg-stone-100 object-cover cursor-pointer"
|
||||||
@@ -390,9 +383,9 @@ onMounted(() => {
|
|||||||
<i class="ph-fill ph-magic-wand" />
|
<i class="ph-fill ph-magic-wand" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-bold text-stone-700 text-sm">AI 自动分类(预留)</p>
|
<p class="font-bold text-stone-700 text-sm">AI 自动分类与标签(预留)</p>
|
||||||
<p class="text-[11px] text-stone-400 mt-0.5">
|
<p class="text-[11px] text-stone-400 mt-0.5">
|
||||||
开启后,未来会优先使用云端 AI 对商户进行分类与标签分析。
|
开启后,未来会自动补全分类、标签,并生成可复用的商户画像。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,7 +398,7 @@ onMounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="aiAutoCategoryEnabled" class="px-4 pb-1 text-[11px] text-purple-600">
|
<p v-if="aiAutoCategoryEnabled" class="px-4 pb-1 text-[11px] text-purple-600">
|
||||||
当前版本仅记录偏好,后续接入云端 AI 后会自动生效。
|
当前版本仅记录偏好,后续接入 AI 能力后会自动生效。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -460,3 +453,4 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// 从 package.json 注入版本号,便于前端展示
|
// 从 package.json 注入版本号,供前端设置页展示。
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const appVersion = (process && process.env && process.env.npm_package_version) || '0.0.0'
|
const appVersion = (process && process.env && process.env.npm_package_version) || '0.0.0'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
define: {
|
define: {
|
||||||
|
|||||||
Reference in New Issue
Block a user