fix: 修复文件编码问题,确保导入语句格式正确

This commit is contained in:
2026-03-12 10:37:07 +08:00
parent bf15918136
commit 8cbe01dd9c
10 changed files with 111 additions and 142 deletions

View File

@@ -1,4 +1,4 @@
package com.echo.app.notification
package com.echo.app.notification
import kotlin.math.abs

16
public/default-avatar.svg Normal file
View 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>

View File

@@ -1,4 +1,4 @@
import assert from 'node:assert/strict'
import assert from 'node:assert/strict'
import { transformNotificationToTransaction } from '../src/services/notificationRuleService.js'
const cases = [

View File

@@ -1,4 +1,4 @@
const notificationRules = [
const notificationRules = [
{
id: 'alipay-expense',
label: '支付宝消费',

View File

@@ -1,4 +1,4 @@
import { createApp } from 'vue'
import { createApp } from 'vue'
import './style.css'
import '@phosphor-icons/web/regular'
import '@phosphor-icons/web/bold'
@@ -10,7 +10,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { defineCustomElements as jeepSqliteCE } from 'jeep-sqlite/loader'
import { Capacitor } from '@capacitor/core'
// 注册 jeep-sqlite 自定义元素并保证 Web 端具备 Capacitor 环境
// 注册 jeep-sqlite 自定义元素,并为 Web 环境挂载 Capacitor 对象。
window.Capacitor = Capacitor
jeepSqliteCE(window)

View File

@@ -1,4 +1,4 @@
import notificationRulesSource from '../config/notificationRules.js'
import notificationRulesSource from '../config/notificationRules.js'
const fallbackRules = notificationRulesSource.map((rule) => ({
...rule,

View File

@@ -1,5 +1,5 @@
<script setup>
// 分析页目前为静态占位,实现基础对话 UI后续接入真实 AI 逻辑
<script setup>
// 分析页当前仍是占位页,先展示未来 AI 财务顾问的交互方向。
</script>
<template>
@@ -12,13 +12,11 @@
</div>
<div>
<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>
<!-- Chat Area -->
<div class="flex-1 overflow-y-auto space-y-5 pr-1 pb-2">
<!-- Bot Message -->
<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"
@@ -28,30 +26,28 @@
<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>你好 Alex👋 我分析了你本周的饮食记录</p>
<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-warning text-orange-400" />
<span class="font-bold text-stone-700 text-xs">高热量警告</span>
<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">
周二和周四的晚餐摄入热量超标 30%主要是因为炸鸡奶茶
餐饮与通勤是主要支出来源夜间消费频率明显高于白天预算消耗速度偏快
</p>
</div>
<p class="mt-3">建议今晚尝试清淡饮食比如蔬菜沙拉或三文鱼</p>
<p class="mt-3">下一步会支持自动分类标签建议异常支出提醒和自然语言问答</p>
</div>
</div>
<!-- User Message -->
<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%]"
>
如果是自己做饭有什么推荐的食谱吗要简单的
帮我总结这周花钱最多的三类项目
</div>
</div>
<!-- Bot Message 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"
@@ -61,22 +57,20 @@
<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>
<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">
<li>热量 450 kcal</li>
<li>耗时10 分钟</li>
<li>成本 ¥18</li>
<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>
</div>
</div>
</div>
<!-- Input Area -->
<div class="mt-2 relative shrink-0">
<input
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"
/>
<button
@@ -87,4 +81,3 @@
</div>
</div>
</template>

View File

@@ -19,13 +19,15 @@ const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactio
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
useTransactionEntry()
const defaultAvatar = '/default-avatar.svg'
let appStateListener = null
onMounted(async () => {
await bootstrapApp()
await syncNotifications()
try {
// App 浠庡悗鍙板洖鍒板墠鍙版椂锛岃嚜鍔ㄥ埛鏂版湰鍦拌处鏈拰閫氱煡闃熷垪
// App 回到前台时,刷新本地账本和待处理通知。
appStateListener = await App.addListener('appStateChange', async ({ isActive }) => {
if (isActive) {
await transactionStore.hydrateTransactions()
@@ -33,7 +35,7 @@ onMounted(async () => {
}
})
} 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 budgetResetCycle = computed(() => settingsStore.budgetResetCycle || 'monthly')
const currentDayLabel = computed(
() =>
new Intl.DateTimeFormat('zh-CN', {
month: 'long',
day: 'numeric',
weekday: 'long',
}).format(new Date()),
const currentDayLabel = computed(() =>
new Intl.DateTimeFormat('zh-CN', {
month: 'long',
day: 'numeric',
weekday: 'long',
}).format(new Date()),
)
const periodStart = computed(() => {
const cycle = budgetResetCycle.value
const now = new Date()
if (cycle === 'weekly') {
const day = now.getDay() || 7 // 鍛ㄤ竴=1锛屽懆鏃?7
const diff = day - 1
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff)
const day = now.getDay() || 7
return new Date(now.getFullYear(), now.getMonth(), now.getDate() - (day - 1))
}
if (cycle === 'monthly') {
const day = settingsStore.budgetMonthlyResetDay || 1
const safeDay = Math.min(Math.max(Number(day) || 1, 1), 28)
const candidate = new Date(now.getFullYear(), now.getMonth(), safeDay)
if (now >= candidate) {
return candidate
}
if (now >= candidate) return candidate
return new Date(now.getFullYear(), now.getMonth() - 1, safeDay)
}
if (cycle === 'custom') {
if (!settingsStore.budgetCustomStartDate) return null
return new Date(settingsStore.budgetCustomStartDate)
}
// none锛氫笉闄愬埗鍛ㄦ湡
return null
})
const periodExpense = computed(() => {
const cycle = budgetResetCycle.value
const start = periodStart.value
if (cycle === 'none' || !start) {
return totalExpense.value || 0
}
if (cycle === 'none' || !start) return totalExpense.value || 0
return (sortedTransactions.value || []).reduce((sum, tx) => {
if (tx.amount >= 0) return sum
const date = new Date(tx.date)
return date >= start ? sum + tx.amount : sum
return new Date(tx.date) >= start ? sum + tx.amount : sum
}, 0)
})
@@ -108,17 +107,22 @@ const remainingBudget = computed(() =>
const todayIncomeValue = computed(() => todaysIncome.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 formatAmount = (value) =>
`${value >= 0 ? '+' : '-'}${Math.abs(value || 0).toFixed(2)}`
const cycleLabel = computed(() => {
if (budgetResetCycle.value === 'weekly') return '每周重置'
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) =>
new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(
new Date(value),
)
const recentTransactions = computed(() => latestTransactions.value.slice(0, 4))
const goSettings = () => {
router.push({ name: 'settings' })
}
@@ -144,18 +148,15 @@ const openTransactionDetail = (tx) => {
<p class="text-xs text-stone-400 font-bold tracking-wider">
{{ currentDayLabel }}
</p>
<h1 class="text-2xl font-extrabold text-stone-800">浠婃棩姒傝</h1>
<h1 class="text-2xl font-extrabold text-stone-800">今日概览</h1>
</div>
<div
class="w-10 h-10 rounded-full bg-orange-50 p-0.5 cursor-pointer border border-orange-100"
@click="goSettings"
>
<img
:src="
settingsStore.profileAvatar ||
'https://api.dicebear.com/7.x/avataaars/svg?seed=Echo'
"
class="rounded-full"
:src="settingsStore.profileAvatar || defaultAvatar"
class="rounded-full w-full h-full object-cover"
alt="User"
/>
</div>
@@ -170,7 +171,7 @@ const openTransactionDetail = (tx) => {
<div class="relative z-10 flex flex-col h-full justify-between">
<div>
<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
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>
<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 class="font-bold text-lg">{{ formatCurrency(totalIncome) }}</p>
</div>
<div>
<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 class="font-bold text-lg">{{ formatCurrency(Math.abs(totalExpense)) }}</p>
</div>
@@ -201,7 +202,7 @@ const openTransactionDetail = (tx) => {
<div class="bg-white rounded-2xl p-4 shadow-sm border border-stone-100">
<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">
<span class="text-xs font-bold text-stone-800">
{{ Math.round(budgetUsage * 100) }}%
@@ -210,7 +211,8 @@ const openTransactionDetail = (tx) => {
class="text-[10px] text-stone-400 underline-offset-2 hover:text-stone-600"
@click="goSettings"
>
鍘昏? </button>
去设置
</button>
</div>
</div>
<div class="w-full bg-stone-100 rounded-full h-2.5 overflow-hidden">
@@ -220,37 +222,27 @@ const openTransactionDetail = (tx) => {
/>
</div>
<div class="flex justify-between mt-2 text-[10px] text-stone-400">
<span>宸茬敤 {{ formatCurrency(Math.abs(periodExpense)) }}</span>
<span> {{ formatCurrency(remainingBudget) }}</span>
<span>已用 {{ formatCurrency(Math.abs(periodExpense)) }}</span>
<span>剩余 {{ formatCurrency(remainingBudget) }}</span>
</div>
<div class="flex justify-between mt-1 text-[10px] text-stone-400">
<span>
周期
{{
budgetResetCycle === 'weekly'
? '每周重置'
: budgetResetCycle === 'none'
? '不重置'
: budgetResetCycle === 'custom'
? '自定义'
: '每月重置'
}}
</span>
<span>周期{{ cycleLabel }}</span>
<button
class="text-[10px] text-stone-400 underline-offset-2 hover:text-stone-600"
@click="goSettings"
>
鍘昏? </button>
去设置
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<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>
</div>
<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>
</div>
</div>
@@ -262,7 +254,7 @@ const openTransactionDetail = (tx) => {
class="text-xs font-bold text-gradient-warm"
@click="goList"
>
鏌ョ湅鍏ㄩ儴
查看全部
</button>
</div>
<div v-if="recentTransactions.length" class="space-y-4">
@@ -300,22 +292,19 @@ const openTransactionDetail = (tx) => {
</div>
</div>
<div v-else class="text-sm text-stone-400 text-center py-4">
杩樻病鏈夎褰曪紝蹇幓娣诲姞绗竴绗旀秷璐瑰惂锝?
还没有记录去添加第一笔流水吧
</div>
</div>
<div class="space-y-3">
<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>
</div>
<div
v-if="notifications.length"
class="space-y-3"
>
<div v-if="notifications.length" class="space-y-3">
<div
v-for="notif in notifications"
:key="notif.id"
@@ -340,60 +329,38 @@ const openTransactionDetail = (tx) => {
:disabled="processingId === notif.id"
@click="confirmNotification(notif.id)"
>
鍏ヨ处
入账
</button>
<button
class="text-[11px] text-stone-400"
@click="dismissNotification(notif.id)"
>
蹇界暐
忽略
</button>
</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>
</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
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"
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"
>
<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>
<i class="ph-fill ph-robot text-xl" />
</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"
>
<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 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>
</template>

View File

@@ -94,14 +94,7 @@ const hasAnyCategoryBudget = computed(() =>
Object.values(categoryBudgets.value || {}).some((v) => (v || 0) > 0),
)
const budgetCategories = [
{ value: 'Food', label: '餐饮' },
{ value: 'Transport', label: '通勤' },
{ value: 'Health', label: '健康' },
{ value: 'Groceries', label: '买菜' },
{ value: 'Entertainment', label: '娱乐' },
{ value: 'Uncategorized', label: '其他' },
]
const budgetCategories = BUDGET_CATEGORY_OPTIONS
const handleAvatarClick = () => {
if (fileInputRef.value) {
fileInputRef.value.click()
@@ -155,7 +148,7 @@ onMounted(() => {
<img
:src="
settingsStore.profileAvatar ||
'https://api.dicebear.com/7.x/avataaars/svg?seed=Echo'
'/default-avatar.svg'
"
alt="Avatar"
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" />
</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">
开启后未来会优先使用云端 AI 对商户进行分类与标签分析
开启后未来会自动补全分类标签并生成可复用的商户画像
</p>
</div>
</div>
@@ -405,7 +398,7 @@ onMounted(() => {
</button>
</div>
<p v-if="aiAutoCategoryEnabled" class="px-4 pb-1 text-[11px] text-purple-600">
当前版本仅记录偏好后续接入云端 AI 后会自动生效
当前版本仅记录偏好后续接入 AI 能力后会自动生效
</p>
</div>
</div>
@@ -460,3 +453,4 @@ onMounted(() => {
</div>
</template>

View File

@@ -1,11 +1,10 @@
import { defineConfig } from 'vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 从 package.json 注入版本号,便于前端展示
// 从 package.json 注入版本号,供前端设置页展示
// eslint-disable-next-line no-undef
const appVersion = (process && process.env && process.env.npm_package_version) || '0.0.0'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
define: {