Files
echo/src/views/SettingsView.vue

292 lines
11 KiB
Vue
Raw Normal View History

<script setup>
import { computed, onMounted, ref } from 'vue'
import NotificationBridge, { isNativeNotificationBridgeAvailable } from '../lib/notificationBridge'
import { useSettingsStore } from '../stores/settings'
// 从 Vite 注入的版本号(来源于 package.json用于在设置页展示
// eslint-disable-next-line no-undef
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'
const settingsStore = useSettingsStore()
const fileInputRef = ref(null)
const notificationCaptureEnabled = computed({
get: () => settingsStore.notificationCaptureEnabled,
set: (value) => settingsStore.setNotificationCaptureEnabled(value),
})
const aiAutoCategoryEnabled = computed({
get: () => settingsStore.aiAutoCategoryEnabled,
set: (value) => settingsStore.setAiAutoCategoryEnabled(value),
})
const nativeBridgeReady = isNativeNotificationBridgeAvailable()
const notificationPermissionGranted = ref(true)
const checkingPermission = ref(false)
const checkNotificationPermission = async () => {
if (!nativeBridgeReady) return
checkingPermission.value = true
try {
const result = await NotificationBridge.hasPermission()
notificationPermissionGranted.value = !!result?.granted
} catch {
notificationPermissionGranted.value = false
} finally {
checkingPermission.value = false
}
}
const toggleNotificationCapture = async () => {
const next = !notificationCaptureEnabled.value
notificationCaptureEnabled.value = next
if (next && nativeBridgeReady) {
await checkNotificationPermission()
if (!notificationPermissionGranted.value) {
try {
await NotificationBridge.openNotificationSettings()
} catch {
// 插件调用失败时忽略,由下方文案提示用户
}
}
}
}
const toggleAiAutoCategory = () => {
aiAutoCategoryEnabled.value = !aiAutoCategoryEnabled.value
}
const handleEditProfileName = () => {
const current = settingsStore.profileName || 'Echo 用户'
const input = window.prompt('修改昵称', current)
if (input == null) return
settingsStore.setProfileName(input)
}
const handleAvatarClick = () => {
if (fileInputRef.value) {
fileInputRef.value.click()
}
}
const handleAvatarChange = (event) => {
const [file] = event.target.files || []
if (!file) return
if (!file.type || !file.type.startsWith('image/')) {
window.alert('请选择图片文件作为头像')
return
}
const reader = new FileReader()
reader.onload = () => {
if (typeof reader.result === 'string') {
settingsStore.setProfileAvatar(reader.result)
}
}
reader.readAsDataURL(file)
}
const handleExportData = () => {
window.alert('导出功能即将上线:届时可以一键导出 CSV / Excel。当前版本建议先通过截图或复制方式备份关键信息。')
}
const handleClearCache = () => {
const ok = window.confirm('确认清除缓存吗?这不会删除正式账本数据,但会重置筛选偏好等本地设置。')
if (!ok) return
try {
// 暂时只清理本地设置缓存,避免误删账本
localStorage.removeItem('settings')
window.alert('已清除设置缓存,部分偏好将在下次启动时重置。')
} catch {
window.alert('清除缓存失败,可稍后重试。')
}
}
onMounted(() => {
void checkNotificationPermission()
})
2025-11-24 17:23:46 +08:00
</script>
<template>
<div class="space-y-6 animate-fade-in pb-10">
<h2 class="text-2xl font-extrabold text-stone-800">设置</h2>
<!-- 个人信息 / 简短说明 -->
2025-11-24 17:23:46 +08:00
<div class="bg-white rounded-3xl p-5 flex items-center gap-4 shadow-sm border border-stone-100">
<div class="relative">
<img
:src="
settingsStore.profileAvatar ||
'https://api.dicebear.com/7.x/avataaars/svg?seed=Echo'
"
alt="Avatar"
class="w-16 h-16 rounded-full bg-stone-100 object-cover cursor-pointer"
@click="handleAvatarClick"
/>
<button
class="absolute bottom-0 right-0 w-6 h-6 rounded-full bg-white flex items-center justify-center text-[10px] text-stone-500 border border-stone-100 shadow-sm"
@click.stop="handleAvatarClick"
>
<i class="ph-bold ph-pencil-simple" />
</button>
<input
ref="fileInputRef"
type="file"
accept="image/*"
class="hidden"
@change="handleAvatarChange"
/>
</div>
2025-11-24 17:23:46 +08:00
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="font-bold text-lg text-stone-800">
{{ settingsStore.profileName }}
</h3>
<button
class="text-[11px] text-stone-400 underline-offset-2 hover:text-stone-600"
@click="handleEditProfileName"
>
编辑
</button>
</div>
<p class="text-xs text-stone-400">本地优先 · 数据只存这台设备</p>
2025-11-24 17:23:46 +08:00
</div>
</div>
<!-- 自动化 & AI -->
2025-11-24 17:23:46 +08:00
<section>
<h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2">
自动化 &amp; AI
</h3>
<div class="bg-white rounded-3xl overflow-hidden shadow-sm border border-stone-100">
<!-- 自动捕获支付通知 -->
<div class="flex flex-col gap-1 border-b border-stone-50">
<div class="flex items-center justify-between p-4">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center"
>
<i class="ph-fill ph-bell-ringing" />
</div>
<div>
<p class="font-bold text-stone-700 text-sm">自动捕获支付通知</p>
<p class="text-[11px] text-stone-400 mt-0.5">
监听支付宝 / 微信 / 银行 App 通知在本机自动生成记账草稿
</p>
</div>
2025-11-24 17:23:46 +08:00
</div>
<button
class="w-11 h-6 rounded-full px-0.5 flex items-center transition-all duration-200"
:class="
notificationCaptureEnabled
? 'bg-orange-400 justify-end'
: 'bg-stone-200 justify-start'
"
@click="toggleNotificationCapture"
>
<div class="w-4 h-4 bg-white rounded-full shadow-sm" />
</button>
2025-11-24 17:23:46 +08:00
</div>
<div v-if="notificationCaptureEnabled" class="px-4 pb-4">
2025-11-24 17:23:46 +08:00
<div
v-if="!notificationPermissionGranted"
class="bg-orange-50 border border-orange-200 rounded-2xl px-3 py-2 flex items-start gap-2"
2025-11-24 17:23:46 +08:00
>
<i class="ph-bold ph-warning text-orange-500 mt-0.5" />
<div class="text-[11px] text-orange-700 leading-relaxed">
<p>系统尚未授予 Echo通知使用权否则无法自动捕获通知</p>
<button
class="mt-1 text-[11px] font-bold text-orange-600 underline"
@click="NotificationBridge.openNotificationSettings()"
>
去系统设置开启
</button>
</div>
2025-11-24 17:23:46 +08:00
</div>
<p v-else class="text-[11px] text-emerald-600 flex items-center gap-1">
<i class="ph-bold ph-check-circle" />
通知权限已开启Echo 会在收到新通知后自动刷新首页
</p>
2025-11-24 17:23:46 +08:00
</div>
</div>
<!-- AI 自动分类开关预留 -->
<div class="flex flex-col gap-1 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-full bg-purple-50 text-purple-500 flex items-center justify-center"
>
<i class="ph-fill ph-magic-wand" />
</div>
<div>
<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>
<button
class="w-11 h-6 rounded-full px-0.5 flex items-center transition-all duration-200"
:class="aiAutoCategoryEnabled ? 'bg-orange-400 justify-end' : 'bg-stone-200 justify-start'"
@click="toggleAiAutoCategory"
>
<div class="w-4 h-4 bg-white rounded-full shadow-sm" />
</button>
2025-11-24 17:23:46 +08:00
</div>
<p v-if="aiAutoCategoryEnabled" class="px-4 pb-1 text-[11px] text-purple-600">
当前版本仅记录偏好后续接入云端 AI 后会自动生效
</p>
2025-11-24 17:23:46 +08:00
</div>
</div>
</section>
<!-- 通用设置 -->
2025-11-24 17:23:46 +08:00
<section>
<h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2">
通用
</h3>
<div class="bg-white rounded-3xl overflow-hidden shadow-sm border border-stone-100">
<button
class="flex w-full items-center justify-between p-4 border-b border-stone-50 active:bg-stone-50 transition text-left"
@click="handleExportData"
2025-11-24 17:23:46 +08:00
>
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-full bg-orange-50 text-orange-500 flex items-center justify-center"
>
<i class="ph-fill ph-export" />
</div>
<div>
<p class="font-bold text-stone-700 text-sm">导出账单数据预留</p>
<p class="text-[11px] text-stone-400 mt-0.5">未来支持导出为 CSV / Excel 文件</p>
</div>
2025-11-24 17:23:46 +08:00
</div>
<i class="ph-bold ph-caret-right text-stone-300" />
</button>
<button
class="flex w-full items-center justify-between p-4 active:bg-stone-50 transition text-left"
@click="handleClearCache"
>
2025-11-24 17:23:46 +08:00
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-full bg-red-50 text-red-500 flex items-center justify-center"
>
<i class="ph-fill ph-trash" />
</div>
<div>
<p class="font-bold text-stone-700 text-sm">清除缓存设置</p>
<p class="text-[11px] text-stone-400 mt-0.5">仅清理本地偏好不影响正式账本数据</p>
</div>
2025-11-24 17:23:46 +08:00
</div>
<span class="text-xs text-stone-400"> KB</span>
</button>
2025-11-24 17:23:46 +08:00
</div>
</section>
<div class="text-center pt-4">
<p class="text-xs text-stone-300">Echo · Local-first · v{{ appVersion }}</p>
2025-11-24 17:23:46 +08:00
</div>
</div>
</template>