2025-11-28 11:37:33 +08:00
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed, onMounted, ref } from 'vue'
|
|
|
|
|
|
import NotificationBridge, { isNativeNotificationBridgeAvailable } from '../lib/notificationBridge'
|
|
|
|
|
|
import { useSettingsStore } from '../stores/settings'
|
2025-12-02 17:50:26 +08:00
|
|
|
|
import EchoInput from '../components/EchoInput.vue'
|
2026-03-12 10:04:29 +08:00
|
|
|
|
import { BUDGET_CATEGORY_OPTIONS } from '../config/transactionCategories.js'
|
2025-11-28 11:37:33 +08:00
|
|
|
|
|
2025-12-02 16:22:46 +08:00
|
|
|
|
// 从 Vite 注入的版本号(来源于 package.json),用于在设置页展示
|
|
|
|
|
|
// eslint-disable-next-line no-undef
|
|
|
|
|
|
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'
|
|
|
|
|
|
|
2025-11-28 11:37:33 +08:00
|
|
|
|
const settingsStore = useSettingsStore()
|
2025-12-02 16:22:46 +08:00
|
|
|
|
const fileInputRef = ref(null)
|
2025-12-02 17:50:26 +08:00
|
|
|
|
const editingProfileName = ref(false)
|
2025-11-28 11:37:33 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 17:50:26 +08:00
|
|
|
|
const profileNameModel = computed({
|
|
|
|
|
|
get: () => settingsStore.profileName,
|
|
|
|
|
|
set: (value) => settingsStore.setProfileName(value),
|
|
|
|
|
|
})
|
2025-12-02 16:22:46 +08:00
|
|
|
|
|
2025-12-02 17:50:26 +08:00
|
|
|
|
const budgetAmount = computed({
|
|
|
|
|
|
get: () => (settingsStore.monthlyBudget || 0).toString(),
|
|
|
|
|
|
set: (value) => settingsStore.setMonthlyBudget(value),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const budgetCycle = computed({
|
|
|
|
|
|
get: () => settingsStore.budgetResetCycle,
|
|
|
|
|
|
set: (value) => settingsStore.setBudgetResetCycle(value),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const categoryBudgets = computed(() => settingsStore.categoryBudgets || {})
|
|
|
|
|
|
const categoryBudgetEnabled = computed({
|
|
|
|
|
|
get: () => settingsStore.categoryBudgetEnabled,
|
|
|
|
|
|
set: (value) => settingsStore.setCategoryBudgetEnabled(value),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const monthlyResetDayModel = computed({
|
|
|
|
|
|
get: () => (settingsStore.budgetMonthlyResetDay || 1).toString(),
|
|
|
|
|
|
set: (value) => settingsStore.setBudgetMonthlyResetDay(value),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const customStartDateModel = computed({
|
|
|
|
|
|
get: () => settingsStore.budgetCustomStartDate || '',
|
|
|
|
|
|
set: (value) => settingsStore.setBudgetCustomStartDate(value),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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: '其他' },
|
|
|
|
|
|
]
|
2025-12-02 16:22:46 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 11:37:33 +08:00
|
|
|
|
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-28 11:37:33 +08:00
|
|
|
|
<!-- 个人信息 / 简短说明 -->
|
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">
|
2025-12-02 16:22:46 +08:00
|
|
|
|
<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">
|
2025-12-02 17:50:26 +08:00
|
|
|
|
<div class="flex items-center gap-2 mb-1">
|
|
|
|
|
|
<template v-if="editingProfileName">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="profileNameModel"
|
|
|
|
|
|
class="bg-stone-50 rounded-full px-3 py-1 text-sm font-bold text-stone-800 border border-stone-200 focus:outline-none focus:border-stone-400"
|
|
|
|
|
|
placeholder="输入要显示的昵称"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<h3
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="font-bold text-lg text-stone-800"
|
|
|
|
|
|
>
|
2025-12-02 16:22:46 +08:00
|
|
|
|
{{ settingsStore.profileName }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<button
|
2025-12-02 17:50:26 +08:00
|
|
|
|
class="w-6 h-6 rounded-full bg-stone-50 flex items-center justify-center text-[10px] text-stone-500 border border-stone-100"
|
|
|
|
|
|
@click="editingProfileName = !editingProfileName"
|
2025-12-02 16:22:46 +08:00
|
|
|
|
>
|
2025-12-02 17:50:26 +08:00
|
|
|
|
<i class="ph-bold ph-pencil-simple" />
|
2025-12-02 16:22:46 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-02 17:50:26 +08:00
|
|
|
|
<p class="text-xs text-stone-400 mb-2">数据只存放在本地</p>
|
2025-11-24 17:23:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-02 17:50:26 +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 p-4 shadow-sm border border-stone-100 space-y-4">
|
|
|
|
|
|
<EchoInput
|
|
|
|
|
|
v-model="budgetAmount"
|
|
|
|
|
|
label="全部预算金额(元)"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
inputmode="decimal"
|
|
|
|
|
|
placeholder="例如 8000"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<span class="text-[11px] text-stone-400 font-bold">重置周期</span>
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
|
|
|
|
|
|
:class="
|
|
|
|
|
|
budgetCycle === 'monthly'
|
|
|
|
|
|
? 'bg-gradient-warm text-white border-transparent shadow-sm'
|
|
|
|
|
|
: 'bg-stone-50 text-stone-500 border-stone-200'
|
|
|
|
|
|
"
|
|
|
|
|
|
@click="budgetCycle = 'monthly'"
|
|
|
|
|
|
>
|
|
|
|
|
|
每月
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
|
|
|
|
|
|
:class="
|
|
|
|
|
|
budgetCycle === 'weekly'
|
|
|
|
|
|
? 'bg-gradient-warm text-white border-transparent shadow-sm'
|
|
|
|
|
|
: 'bg-stone-50 text-stone-500 border-stone-200'
|
|
|
|
|
|
"
|
|
|
|
|
|
@click="budgetCycle = 'weekly'"
|
|
|
|
|
|
>
|
|
|
|
|
|
每周
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
|
|
|
|
|
|
:class="
|
|
|
|
|
|
budgetCycle === 'none'
|
|
|
|
|
|
? 'bg-gradient-warm text-white border-transparent shadow-sm'
|
|
|
|
|
|
: 'bg-stone-50 text-stone-500 border-stone-200'
|
|
|
|
|
|
"
|
|
|
|
|
|
@click="budgetCycle = 'none'"
|
|
|
|
|
|
>
|
|
|
|
|
|
不重置
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
|
|
|
|
|
|
:class="
|
|
|
|
|
|
budgetCycle === 'custom'
|
|
|
|
|
|
? 'bg-gradient-warm text-white border-transparent shadow-sm'
|
|
|
|
|
|
: 'bg-stone-50 text-stone-500 border-stone-200'
|
|
|
|
|
|
"
|
|
|
|
|
|
@click="budgetCycle = 'custom'"
|
|
|
|
|
|
>
|
|
|
|
|
|
自定义
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="budgetCycle === 'monthly'" class="mt-3">
|
|
|
|
|
|
<EchoInput
|
|
|
|
|
|
v-model="monthlyResetDayModel"
|
|
|
|
|
|
label="每月第几天重置(1-28)"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
inputmode="numeric"
|
|
|
|
|
|
placeholder="例如 1 表示每月 1 号"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="budgetCycle === 'custom'" class="mt-3">
|
|
|
|
|
|
<EchoInput
|
|
|
|
|
|
v-model="customStartDateModel"
|
|
|
|
|
|
label="自定义起始日"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
hint="从该日期开始作为预算统计区间的起点"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pt-2 space-y-2">
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
<p class="text-[11px] text-stone-400 font-bold">分类预算(可选)</p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="w-10 h-5 rounded-full px-0.5 flex items-center transition-all duration-200"
|
|
|
|
|
|
:class="
|
|
|
|
|
|
categoryBudgetEnabled
|
|
|
|
|
|
? 'bg-orange-400 justify-end'
|
|
|
|
|
|
: 'bg-stone-200 justify-start'
|
|
|
|
|
|
"
|
|
|
|
|
|
@click="categoryBudgetEnabled = !categoryBudgetEnabled"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="w-4 h-4 bg-white rounded-full shadow-sm" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="categoryBudgetEnabled || hasAnyCategoryBudget"
|
|
|
|
|
|
class="space-y-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="cat in budgetCategories"
|
|
|
|
|
|
:key="cat.value"
|
|
|
|
|
|
class="border border-stone-100 rounded-2xl px-3 py-2 bg-stone-50/40"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EchoInput
|
|
|
|
|
|
:model-value="(categoryBudgets[cat.value] || 0) ? String(categoryBudgets[cat.value]) : ''"
|
|
|
|
|
|
:label="`${cat.label}预算(元)`"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
inputmode="decimal"
|
|
|
|
|
|
placeholder="留空则不启用该分类预算"
|
|
|
|
|
|
@update:modelValue="(val) => settingsStore.setCategoryBudget(cat.value, val)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<!-- 自动化 & 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">
|
|
|
|
|
|
自动化 & AI
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div class="bg-white rounded-3xl overflow-hidden shadow-sm border border-stone-100">
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<!-- 自动捕获支付通知 -->
|
|
|
|
|
|
<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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
|
|
|
|
|
|
<div v-if="notificationCaptureEnabled" class="px-4 pb-4">
|
2025-11-24 17:23:46 +08:00
|
|
|
|
<div
|
2025-11-28 11:37:33 +08:00
|
|
|
|
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
|
|
|
|
>
|
2025-11-28 11:37:33 +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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
</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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<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-28 11:37:33 +08:00
|
|
|
|
<!-- 通用设置 -->
|
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"
|
2025-11-28 11:37:33 +08:00
|
|
|
|
@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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<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>
|
2025-11-28 11:37:33 +08:00
|
|
|
|
<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">
|
2025-12-02 17:50:26 +08:00
|
|
|
|
<p class="text-xs text-stone-300">Echo · v{{ appVersion }}</p>
|
2025-11-24 17:23:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-03-12 10:04:29 +08:00
|
|
|
|
|