feat: 添加 UI 状态管理,支持新增记录底部弹窗功能,优化设置和主页交互
This commit is contained in:
@@ -2,9 +2,12 @@
|
||||
import { onMounted } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import BottomDock from './components/BottomDock.vue'
|
||||
import AddEntryView from './views/AddEntryView.vue'
|
||||
import { useTransactionStore } from './stores/transactions'
|
||||
import { useUiStore } from './stores/ui'
|
||||
|
||||
const transactionStore = useTransactionStore()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
onMounted(() => {
|
||||
transactionStore.ensureInitialized()
|
||||
@@ -25,6 +28,9 @@ onMounted(() => {
|
||||
|
||||
<!-- 底部 Dock 导航 -->
|
||||
<BottomDock />
|
||||
|
||||
<!-- 新增记录底部弹窗:在任意 Tab 上浮层显示 -->
|
||||
<AddEntryView v-if="uiStore.addEntryVisible" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
|
||||
// 使用路由信息来高亮当前 Tab,而不是从父组件传递状态
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const currentTab = computed(() => route.name)
|
||||
|
||||
@@ -14,7 +16,7 @@ const goTab = (name) => {
|
||||
}
|
||||
|
||||
const openAdd = () => {
|
||||
router.push({ name: 'add' })
|
||||
uiStore.openAddEntry()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ const nativeBridgeReady = isNativeNotificationBridgeAvailable()
|
||||
let permissionChecked = false
|
||||
let bootstrapped = false
|
||||
let nativeNotificationListener = null
|
||||
let pollTimer = null
|
||||
const POLL_INTERVAL_MS = 60000
|
||||
|
||||
const ensureRulesReady = async () => {
|
||||
if (ruleSet.value.length) return ruleSet.value
|
||||
@@ -147,6 +149,15 @@ export const useTransactionEntry = () => {
|
||||
console.warn('[notifications] 无法注册 notificationPosted 监听', error)
|
||||
})
|
||||
}
|
||||
|
||||
// 前台轮询兜底:即使偶发漏掉原生事件,也能定期从队列补拉一次
|
||||
if (!pollTimer && typeof setInterval === 'function') {
|
||||
pollTimer = setInterval(() => {
|
||||
if (!syncing.value) {
|
||||
void syncNotifications()
|
||||
}
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import ListView from '../views/ListView.vue'
|
||||
import SettingsView from '../views/SettingsView.vue'
|
||||
import AddEntryView from '../views/AddEntryView.vue'
|
||||
|
||||
// TODO: 后续补充真实的分析页实现,这里先占位
|
||||
const AnalysisView = () => import('../views/AnalysisView.vue').catch(() => null)
|
||||
@@ -34,12 +33,6 @@ const router = createRouter({
|
||||
component: SettingsView,
|
||||
meta: { tab: 'settings' },
|
||||
},
|
||||
{
|
||||
path: '/add',
|
||||
name: 'add',
|
||||
component: AddEntryView,
|
||||
meta: { overlay: true },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -187,26 +187,59 @@ const extractAmount = (text = '') => {
|
||||
return loose?.[1] ? Math.abs(parseFloat(loose[1])) : 0
|
||||
}
|
||||
|
||||
// 判断字符串是否更像是「金额」而不是商户名,用于避免把「1.00元」当成商户
|
||||
const looksLikeAmount = (raw = '') => {
|
||||
const value = String(raw).trim()
|
||||
if (!value) return false
|
||||
const amountPattern = /^[-\d.,]+\s*(?:元|块钱|¥|¥|人民币)?$/
|
||||
return amountPattern.test(value)
|
||||
}
|
||||
|
||||
const extractMerchant = (text, rule) => {
|
||||
const content = text || ''
|
||||
|
||||
if (rule?.merchantPattern instanceof RegExp) {
|
||||
const m = text.match(rule.merchantPattern)
|
||||
const m = content.match(rule.merchantPattern)
|
||||
if (m?.[1]) return m[1].trim()
|
||||
}
|
||||
|
||||
// 通用的「站点 A -> 站点 B」线路模式(公交/地铁通知),
|
||||
// 即使未命中专用规则也尝试提取,避免退化为金额或「Unknown」
|
||||
const routeMatch = content.match(
|
||||
/([\u4e00-\u9fa5]{2,}\s*(?:-|—|>|→|至|到|->)\s*[\u4e00-\u9fa5]{2,})/,
|
||||
)
|
||||
if (routeMatch?.[1]) {
|
||||
return routeMatch[1].trim()
|
||||
}
|
||||
|
||||
const genericPatterns = [
|
||||
/向\s*([\u4e00-\u9fa5A-Za-z0-9\-\s&]+)/,
|
||||
/商户[::]?\s*([\u4e00-\u9fa5A-Za-z0-9\-\s&]+)/,
|
||||
]
|
||||
|
||||
for (const pattern of genericPatterns) {
|
||||
const m = text.match(pattern)
|
||||
const m = content.match(pattern)
|
||||
if (m?.[1]) return m[1].trim()
|
||||
}
|
||||
|
||||
const parts = text.split(/:|:/)
|
||||
// 针对「……支出(消费支付宝-上海拉扎斯信息科技有限公司)3.23元」这类银行通知,
|
||||
// 优先尝试从括号中的「支付宝-商户名」结构中提取真正的商户名
|
||||
const parenMatch = content.match(/\(([^)]+)\)/)
|
||||
if (parenMatch?.[1]) {
|
||||
const inner = parenMatch[1]
|
||||
const payMatch = inner.match(/(?:支付宝|微信支付|微信)[-—\s]*([^\d元¥¥]+)$/)
|
||||
if (payMatch?.[1]) {
|
||||
const candidate = payMatch[1].trim()
|
||||
if (candidate && !looksLikeAmount(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parts = content.split(/:|:/)
|
||||
if (parts.length > 1) {
|
||||
const candidate = parts[1].split(/[,,\s]/)[0]
|
||||
if (candidate) return candidate.trim()
|
||||
if (candidate && !looksLikeAmount(candidate)) return candidate.trim()
|
||||
}
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
@@ -6,6 +6,9 @@ export const useSettingsStore = defineStore(
|
||||
() => {
|
||||
const notificationCaptureEnabled = ref(true)
|
||||
const aiAutoCategoryEnabled = ref(false)
|
||||
const monthlyBudget = ref(12000)
|
||||
const profileName = ref('Echo 用户')
|
||||
const profileAvatar = ref('')
|
||||
|
||||
const setNotificationCaptureEnabled = (value) => {
|
||||
notificationCaptureEnabled.value = !!value
|
||||
@@ -15,16 +18,41 @@ export const useSettingsStore = defineStore(
|
||||
aiAutoCategoryEnabled.value = !!value
|
||||
}
|
||||
|
||||
const setMonthlyBudget = (value) => {
|
||||
const numeric = Number(value)
|
||||
monthlyBudget.value = Number.isFinite(numeric) && numeric >= 0 ? numeric : 0
|
||||
}
|
||||
|
||||
const setProfileName = (value) => {
|
||||
profileName.value = (value || '').trim() || 'Echo 用户'
|
||||
}
|
||||
|
||||
const setProfileAvatar = (value) => {
|
||||
profileAvatar.value = value || ''
|
||||
}
|
||||
|
||||
return {
|
||||
notificationCaptureEnabled,
|
||||
aiAutoCategoryEnabled,
|
||||
monthlyBudget,
|
||||
profileName,
|
||||
profileAvatar,
|
||||
setNotificationCaptureEnabled,
|
||||
setAiAutoCategoryEnabled,
|
||||
setMonthlyBudget,
|
||||
setProfileName,
|
||||
setProfileAvatar,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
paths: ['notificationCaptureEnabled', 'aiAutoCategoryEnabled'],
|
||||
paths: [
|
||||
'notificationCaptureEnabled',
|
||||
'aiAutoCategoryEnabled',
|
||||
'monthlyBudget',
|
||||
'profileName',
|
||||
'profileAvatar',
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
26
src/stores/ui.js
Normal file
26
src/stores/ui.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 负责全局 UI 状态,例如新增记录的底部弹窗
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const addEntryVisible = ref(false)
|
||||
const editingTransactionId = ref('')
|
||||
|
||||
const openAddEntry = (id = '') => {
|
||||
editingTransactionId.value = id || ''
|
||||
addEntryVisible.value = true
|
||||
}
|
||||
|
||||
const closeAddEntry = () => {
|
||||
addEntryVisible.value = false
|
||||
editingTransactionId.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
addEntryVisible,
|
||||
editingTransactionId,
|
||||
openAddEntry,
|
||||
closeAddEntry,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useTransactionStore } from '../stores/transactions'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const transactionStore = useTransactionStore()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const editingId = computed(() => route.query.id || '')
|
||||
const editingId = computed(() => uiStore.editingTransactionId || '')
|
||||
const feedback = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
@@ -58,7 +57,7 @@ const hydrateForm = () => {
|
||||
watch(editingId, hydrateForm, { immediate: true })
|
||||
|
||||
const closePanel = () => {
|
||||
router.back()
|
||||
uiStore.closeAddEntry()
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
@@ -67,8 +66,8 @@ const validate = () => {
|
||||
return false
|
||||
}
|
||||
const parsedAmount = Number(form.amount)
|
||||
if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {
|
||||
feedback.value = '请输入有效的金额'
|
||||
if (!Number.isFinite(parsedAmount) || parsedAmount < 0) {
|
||||
feedback.value = '金额不能为负数'
|
||||
return false
|
||||
}
|
||||
feedback.value = ''
|
||||
@@ -115,7 +114,9 @@ transactionStore.ensureInitialized()
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0 z-50 flex flex-col justify-end bg-stone-900/30 backdrop-blur-sm">
|
||||
<div class="bg-white w-full rounded-t-[32px] p-6 pb-10 shadow-2xl">
|
||||
<div
|
||||
class="bg-white w-full rounded-t-[32px] p-6 pb-10 shadow-2xl max-h-[80vh] overflow-y-auto hide-scrollbar"
|
||||
>
|
||||
<div class="w-12 h-1.5 bg-stone-200 rounded-full mx-auto mb-6" />
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
import { computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { App } from '@capacitor/app'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTransactionStore } from '../stores/transactions'
|
||||
import { useTransactionEntry } from '../composables/useTransactionEntry'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
|
||||
const router = useRouter()
|
||||
const transactionStore = useTransactionStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const uiStore = useUiStore()
|
||||
const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactions } =
|
||||
storeToRefs(transactionStore)
|
||||
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
|
||||
@@ -14,10 +20,13 @@ const { notifications, confirmNotification, dismissNotification, processingId, s
|
||||
let appStateListener = null
|
||||
|
||||
onMounted(async () => {
|
||||
await transactionStore.ensureInitialized()
|
||||
await syncNotifications()
|
||||
try {
|
||||
// App 从后台回到前台时,自动同步一次通知和交易列表
|
||||
// App 从后台回到前台时,自动刷新本地账本和通知队列
|
||||
appStateListener = await App.addListener('appStateChange', async ({ isActive }) => {
|
||||
if (isActive) {
|
||||
await transactionStore.hydrateTransactions()
|
||||
await syncNotifications()
|
||||
}
|
||||
})
|
||||
@@ -34,8 +43,7 @@ onBeforeUnmount(() => {
|
||||
appStateListener = null
|
||||
})
|
||||
|
||||
const emit = defineEmits(['changeTab'])
|
||||
const monthlyBudget = 12000
|
||||
const monthlyBudget = computed(() => settingsStore.monthlyBudget || 0)
|
||||
|
||||
const currentDayLabel = computed(
|
||||
() =>
|
||||
@@ -48,13 +56,14 @@ const currentDayLabel = computed(
|
||||
|
||||
const budgetUsage = computed(() => {
|
||||
const expense = Math.abs(totalExpense.value || 0)
|
||||
if (!monthlyBudget) return 0
|
||||
return Math.min(expense / monthlyBudget, 1)
|
||||
const budget = monthlyBudget.value || 0
|
||||
if (!budget) return 0
|
||||
return Math.min(expense / budget, 1)
|
||||
})
|
||||
|
||||
const balance = computed(() => (totalIncome.value || 0) + (totalExpense.value || 0))
|
||||
const remainingBudget = computed(() =>
|
||||
Math.max(monthlyBudget - Math.abs(totalExpense.value || 0), 0),
|
||||
Math.max((monthlyBudget.value || 0) - Math.abs(totalExpense.value || 0), 0),
|
||||
)
|
||||
|
||||
const todayIncomeValue = computed(() => todaysIncome.value || 0)
|
||||
@@ -82,7 +91,34 @@ const categoryMeta = {
|
||||
|
||||
const getCategoryMeta = (category) => categoryMeta[category] || categoryMeta.default
|
||||
|
||||
transactionStore.ensureInitialized()
|
||||
const handleEditBudget = () => {
|
||||
const current = monthlyBudget.value || 0
|
||||
const input = window.prompt('设置本月预算(元)', current ? String(current) : '')
|
||||
if (input == null) return
|
||||
const numeric = Number(input)
|
||||
if (!Number.isFinite(numeric) || numeric < 0) {
|
||||
window.alert('请输入合法的预算金额')
|
||||
return
|
||||
}
|
||||
settingsStore.setMonthlyBudget(numeric)
|
||||
}
|
||||
|
||||
const goSettings = () => {
|
||||
router.push({ name: 'settings' })
|
||||
}
|
||||
|
||||
const goList = () => {
|
||||
router.push({ name: 'list' })
|
||||
}
|
||||
|
||||
const goAnalysis = () => {
|
||||
router.push({ name: 'analysis' })
|
||||
}
|
||||
|
||||
const openTransactionDetail = (tx) => {
|
||||
if (!tx?.id) return
|
||||
uiStore.openAddEntry(tx.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -96,10 +132,13 @@ transactionStore.ensureInitialized()
|
||||
</div>
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-orange-50 p-0.5 cursor-pointer border border-orange-100"
|
||||
@click="emit('changeTab', 'settings')"
|
||||
@click="goSettings"
|
||||
>
|
||||
<img
|
||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=Echo"
|
||||
:src="
|
||||
settingsStore.profileAvatar ||
|
||||
'https://api.dicebear.com/7.x/avataaars/svg?seed=Echo'
|
||||
"
|
||||
class="rounded-full"
|
||||
alt="User"
|
||||
/>
|
||||
@@ -147,9 +186,17 @@ transactionStore.ensureInitialized()
|
||||
<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-800">
|
||||
{{ Math.round(budgetUsage * 100) }}%
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-bold text-stone-800">
|
||||
{{ Math.round(budgetUsage * 100) }}%
|
||||
</span>
|
||||
<button
|
||||
class="text-[10px] text-stone-400 underline-offset-2 hover:text-stone-600"
|
||||
@click="handleEditBudget"
|
||||
>
|
||||
调整
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-stone-100 rounded-full h-2.5 overflow-hidden">
|
||||
<div
|
||||
@@ -179,7 +226,7 @@ transactionStore.ensureInitialized()
|
||||
<h3 class="font-bold text-stone-700">最新流水</h3>
|
||||
<button
|
||||
class="text-xs font-bold text-gradient-warm"
|
||||
@click="emit('changeTab', 'list')"
|
||||
@click="goList"
|
||||
>
|
||||
查看全部
|
||||
</button>
|
||||
@@ -188,7 +235,8 @@ transactionStore.ensureInitialized()
|
||||
<div
|
||||
v-for="tx in recentTransactions"
|
||||
:key="tx.id"
|
||||
class="flex items-center justify-between"
|
||||
class="flex items-center justify-between cursor-pointer active:bg-stone-50 rounded-2xl px-2 -mx-2 py-1 transition"
|
||||
@click="openTransactionDetail(tx)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
@@ -277,7 +325,7 @@ transactionStore.ensureInitialized()
|
||||
<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="emit('changeTab', 'analysis')"
|
||||
@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"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useTransactionStore } from '../stores/transactions'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
|
||||
const router = useRouter()
|
||||
const transactionStore = useTransactionStore()
|
||||
const uiStore = useUiStore()
|
||||
const { groupedTransactions, filters, loading } = storeToRefs(transactionStore)
|
||||
const deletingId = ref('')
|
||||
|
||||
@@ -36,7 +36,7 @@ const formatTime = (value) =>
|
||||
)
|
||||
|
||||
const handleEdit = (item) => {
|
||||
router.push({ name: 'add', query: { id: item.id } })
|
||||
uiStore.openAddEntry(item.id)
|
||||
}
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
@@ -63,7 +63,7 @@ transactionStore.ensureInitialized()
|
||||
<h2 class="text-2xl font-extrabold text-stone-800">账单明细</h2>
|
||||
<button
|
||||
class="flex items-center gap-2 bg-white border border-stone-100 px-3 py-1.5 rounded-full shadow-sm text-xs font-bold text-stone-500"
|
||||
@click="router.push({ name: 'add' })"
|
||||
@click="uiStore.openAddEntry()"
|
||||
>
|
||||
<i class="ph-bold ph-plus" /> 新增
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,12 @@ 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,
|
||||
@@ -51,6 +56,35 @@ 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。当前版本建议先通过截图或复制方式备份关键信息。')
|
||||
}
|
||||
@@ -78,13 +112,42 @@ onMounted(() => {
|
||||
|
||||
<!-- 个人信息 / 简短说明 -->
|
||||
<div class="bg-white rounded-3xl p-5 flex items-center gap-4 shadow-sm border border-stone-100">
|
||||
<img
|
||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=Echo"
|
||||
alt="Avatar"
|
||||
class="w-16 h-16 rounded-full bg-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>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-lg text-stone-800">Echo 用户</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,7 +285,7 @@ onMounted(() => {
|
||||
</section>
|
||||
|
||||
<div class="text-center pt-4">
|
||||
<p class="text-xs text-stone-300">Echo · Local-first Beta</p>
|
||||
<p class="text-xs text-stone-300">Echo · Local-first · v{{ appVersion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// 从 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: {
|
||||
__APP_VERSION__: JSON.stringify(appVersion),
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user