-
-
-
diff --git a/src/config/notificationRules.js b/src/config/notificationRules.js
new file mode 100644
index 0000000..527239d
--- /dev/null
+++ b/src/config/notificationRules.js
@@ -0,0 +1,153 @@
+const notificationRules = [
+ {
+ id: 'alipay-expense',
+ label: '支付宝消费',
+ channels: ['支付宝', 'Alipay'],
+ keywords: ['支付', '扣款', '支出', '消费', '成功支付'],
+ requiredTextPatterns: [],
+ direction: 'expense',
+ defaultCategory: 'Food',
+ autoCapture: false,
+ merchantPattern: {
+ pattern: '(?:向|收款方|商户)\\s*([\\u4e00-\\u9fa5A-Za-z0-9\\s&-]+)',
+ flags: 'i',
+ },
+ defaultMerchant: '',
+ },
+ {
+ id: 'alipay-income',
+ label: '支付宝收款',
+ channels: ['支付宝', 'Alipay'],
+ keywords: ['到账', '收入', '收款', '入账'],
+ requiredTextPatterns: [],
+ direction: 'income',
+ defaultCategory: 'Income',
+ autoCapture: true,
+ merchantPattern: {
+ pattern: '(?:来自|收款方|付款方|来源)\\s*([\\u4e00-\\u9fa5A-Za-z0-9\\s&-]+)',
+ flags: 'i',
+ },
+ defaultMerchant: '',
+ },
+ {
+ id: 'wechat-expense',
+ label: '微信消费',
+ channels: ['微信支付', '微信', 'WeChat'],
+ keywords: ['支付', '支出', '扣款', '消费成功'],
+ requiredTextPatterns: [],
+ direction: 'expense',
+ defaultCategory: 'Groceries',
+ autoCapture: false,
+ merchantPattern: {
+ pattern: '(?:向|收款方|商户)\\s*([\\u4e00-\\u9fa5A-Za-z0-9\\s&-]+)',
+ flags: '',
+ },
+ defaultMerchant: '',
+ },
+ {
+ id: 'wechat-income',
+ label: '微信收款',
+ channels: ['微信支付', '微信', 'WeChat'],
+ keywords: ['收款', '到账', '入账', '转入'],
+ requiredTextPatterns: [],
+ direction: 'income',
+ defaultCategory: 'Income',
+ autoCapture: true,
+ merchantPattern: {
+ pattern: '(?:来自|收款方|付款方|来源)\\s*([\\u4e00-\\u9fa5A-Za-z0-9\\s&-]+)',
+ flags: '',
+ },
+ defaultMerchant: '',
+ },
+ {
+ id: 'bank-income',
+ label: '银行入账',
+ channels: [
+ '招商银行',
+ '中国银行',
+ '建设银行',
+ '工商银行',
+ '农业银行',
+ '交通银行',
+ '浦发银行',
+ '兴业银行',
+ '光大银行',
+ '中信银行',
+ '广发银行',
+ '平安银行',
+ ],
+ keywords: ['工资', '薪资', '代发', '到账', '入账', '收入'],
+ requiredTextPatterns: [],
+ direction: 'income',
+ defaultCategory: 'Income',
+ autoCapture: true,
+ merchantPattern: {
+ pattern: '(?:来自|付款方|来源)\\s*([\\u4e00-\\u9fa5A-Za-z0-9\\s&-]+)',
+ flags: '',
+ },
+ defaultMerchant: '',
+ },
+ {
+ id: 'bank-expense',
+ label: '银行卡支出',
+ channels: [
+ '招商银行',
+ '中国银行',
+ '建设银行',
+ '工商银行',
+ '农业银行',
+ '交通银行',
+ '浦发银行',
+ '兴业银行',
+ '光大银行',
+ '中信银行',
+ '广发银行',
+ '平安银行',
+ '借记卡',
+ '信用卡',
+ ],
+ keywords: ['支出', '消费', '扣款', '刷卡消费', '银联消费'],
+ requiredTextPatterns: [],
+ direction: 'expense',
+ defaultCategory: 'Expense',
+ autoCapture: false,
+ merchantPattern: {
+ pattern: '(?:商户|商家|消费商户)\\s*([\\u4e00-\\u9fa5A-Za-z0-9\\s&-]+)',
+ flags: '',
+ },
+ defaultMerchant: '',
+ },
+ {
+ id: 'transit-card-expense',
+ label: '公交出行',
+ channels: [
+ '杭州通互联互通卡',
+ '杭州通',
+ '北京一卡通',
+ '上海公共交通卡',
+ '深圳通',
+ '苏州公交卡',
+ '交通联合',
+ '乘车码',
+ '地铁',
+ '公交',
+ 'Mi Pay',
+ 'Huawei Pay',
+ '华为钱包',
+ '小米钱包',
+ 'OPPO 钱包',
+ ],
+ keywords: [],
+ requiredTextPatterns: ['扣费', '扣款', '支付', '乘车码', '车费', '票价', '本次乘车', '实付', '优惠后'],
+ direction: 'expense',
+ defaultCategory: 'Transport',
+ autoCapture: true,
+ merchantPattern: {
+ pattern: '([\\u4e00-\\u9fa5]{2,}\\s*(?:-|->|→|至|到)\\s*[\\u4e00-\\u9fa5]{2,})',
+ flags: '',
+ },
+ defaultMerchant: '公交/地铁出行',
+ },
+]
+
+export default notificationRules
diff --git a/src/config/transactionCategories.js b/src/config/transactionCategories.js
new file mode 100644
index 0000000..56e6087
--- /dev/null
+++ b/src/config/transactionCategories.js
@@ -0,0 +1,90 @@
+export const TRANSACTION_CATEGORIES = [
+ {
+ value: 'Food',
+ label: '餐饮',
+ icon: 'ph-bowl-food',
+ bg: 'bg-orange-100',
+ color: 'text-orange-600',
+ budgetable: true,
+ },
+ {
+ value: 'Transport',
+ label: '通勤',
+ icon: 'ph-taxi',
+ bg: 'bg-blue-100',
+ color: 'text-blue-600',
+ budgetable: true,
+ },
+ {
+ value: 'Health',
+ label: '健康',
+ icon: 'ph-heartbeat',
+ bg: 'bg-rose-100',
+ color: 'text-rose-600',
+ budgetable: true,
+ },
+ {
+ value: 'Groceries',
+ label: '买菜',
+ icon: 'ph-basket',
+ bg: 'bg-amber-100',
+ color: 'text-amber-600',
+ budgetable: true,
+ },
+ {
+ value: 'Entertainment',
+ label: '娱乐',
+ icon: 'ph-game-controller',
+ bg: 'bg-fuchsia-100',
+ color: 'text-fuchsia-600',
+ budgetable: true,
+ },
+ {
+ value: 'Expense',
+ label: '一般支出',
+ icon: 'ph-receipt',
+ bg: 'bg-stone-100',
+ color: 'text-stone-600',
+ budgetable: true,
+ },
+ {
+ value: 'Income',
+ label: '收入',
+ icon: 'ph-wallet',
+ bg: 'bg-emerald-100',
+ color: 'text-emerald-600',
+ budgetable: false,
+ },
+ {
+ value: 'Uncategorized',
+ label: '其他',
+ icon: 'ph-note-pencil',
+ bg: 'bg-stone-100',
+ color: 'text-stone-500',
+ budgetable: true,
+ },
+]
+
+export const DEFAULT_TRANSACTION_CATEGORY = 'Food'
+
+export const CATEGORY_CHIPS = [
+ { label: '全部', value: 'all', icon: 'ph-asterisk' },
+ ...TRANSACTION_CATEGORIES.map(({ label, value, icon }) => ({ label, value, icon })),
+]
+
+export const CATEGORY_META = TRANSACTION_CATEGORIES.reduce((acc, category) => {
+ acc[category.value] = category
+ return acc
+}, {})
+
+export const BUDGET_CATEGORY_OPTIONS = TRANSACTION_CATEGORIES.filter((category) => category.budgetable)
+
+export const DEFAULT_CATEGORY_BUDGETS = BUDGET_CATEGORY_OPTIONS.reduce((acc, category) => {
+ acc[category.value] = 0
+ return acc
+}, {})
+
+export const getCategoryMeta = (category) =>
+ CATEGORY_META[category] || CATEGORY_META.Uncategorized
+
+export const getCategoryLabel = (category) => getCategoryMeta(category).label
diff --git a/src/services/appBootstrap.js b/src/services/appBootstrap.js
new file mode 100644
index 0000000..204ebae
--- /dev/null
+++ b/src/services/appBootstrap.js
@@ -0,0 +1,15 @@
+import { useTransactionStore } from '../stores/transactions'
+
+let bootstrapPromise = null
+
+export const bootstrapApp = async () => {
+ if (!bootstrapPromise) {
+ const transactionStore = useTransactionStore()
+ bootstrapPromise = transactionStore.ensureInitialized().catch((error) => {
+ bootstrapPromise = null
+ throw error
+ })
+ }
+
+ return bootstrapPromise
+}
diff --git a/src/services/notificationRuleService.js b/src/services/notificationRuleService.js
index 0ae46ad..6158033 100644
--- a/src/services/notificationRuleService.js
+++ b/src/services/notificationRuleService.js
@@ -1,150 +1,32 @@
-const fallbackRules = [
- // 支付宝消费:日常吃饭、网购等
- {
- id: 'alipay-expense',
- label: '支付宝消费',
- channels: ['支付宝', 'Alipay'],
- keywords: ['支付', '扣款', '支出', '消费', '成功支付'],
- direction: 'expense',
- defaultCategory: 'Food',
- autoCapture: false,
- merchantPattern: /向\s*([\u4e00-\u9fa5A-Za-z0-9\s&]+)/i,
- },
- // 支付宝收款 / 到账
- {
- id: 'alipay-income',
- label: '支付宝收款',
- channels: ['支付宝', 'Alipay'],
- keywords: ['到账', '收入', '收款', '入账'],
- direction: 'income',
- defaultCategory: 'Income',
- autoCapture: true,
- merchantPattern: /(?:来自|收款方)\s*([\u4e00-\u9fa5A-Za-z0-9\s&]+)/i,
- },
- // 微信消费
- {
- id: 'wechat-expense',
- label: '微信消费',
- channels: ['微信支付', '微信', 'WeChat'],
- keywords: ['支付', '支出', '扣款', '消费成功'],
- direction: 'expense',
- defaultCategory: 'Groceries',
- autoCapture: false,
- merchantPattern: /向\s*([\u4e00-\u9fa5A-Za-z0-9\s&]+)/,
- },
- // 微信收款 / 转账入账
- {
- id: 'wechat-income',
- label: '微信收款',
- channels: ['微信支付', '微信', 'WeChat'],
- keywords: ['收款', '到账', '入账', '转入'],
- direction: 'income',
- defaultCategory: 'Income',
- autoCapture: true,
- merchantPattern: /(?:来自|收款方)\s*([\u4e00-\u9fa5A-Za-z0-9\s&]+)/,
- },
- // 银行工资 / 常规入账
- {
- id: 'bank-income',
- label: '工资到账',
- channels: [
- '招商银行',
- '中国银行',
- '建设银行',
- '工商银行',
- '农业银行',
- '交通银行',
- '浦发银行',
- '兴业银行',
- '光大银行',
- '中信银行',
- '广发银行',
- '平安银行',
- ],
- keywords: ['工资', '薪资', '代发', '到账', '入账', '收入'],
- direction: 'income',
- defaultCategory: 'Income',
- autoCapture: true,
- merchantPattern: /(?:来自|付款方|来源)\s*([\u4e00-\u9fa5A-Za-z0-9\s&]+)/,
- },
- // 银行消费 / 借记卡扣款
- {
- id: 'bank-expense',
- label: '银行卡支出',
- channels: [
- '招商银行',
- '中国银行',
- '建设银行',
- '工商银行',
- '农业银行',
- '交通银行',
- '浦发银行',
- '兴业银行',
- '光大银行',
- '中信银行',
- '广发银行',
- '平安银行',
- '借记卡',
- '信用卡',
- ],
- keywords: ['支出', '消费', '扣款', '刷卡消费', '银联消费'],
- direction: 'expense',
- defaultCategory: 'Expense',
- autoCapture: false,
- merchantPattern: /(?:商户|商家|消费商户)\s*([\u4e00-\u9fa5A-Za-z0-9\s&]+)/,
- },
- // 手机公交卡 / 乘车码扣款(地铁、公交等),按交通分类
- {
- id: 'transit-card-expense',
- label: '公交出行',
- channels: [
- '杭州通互联互通卡',
- '杭州通',
- '北京一卡通',
- '上海公共交通卡',
- '深圳通',
- '苏州公交卡',
- '交通联合',
- '乘车码',
- '地铁',
- '公交',
- 'Mi Pay',
- 'Huawei Pay',
- '华为钱包',
- '小米钱包',
- 'OPPO 钱包',
- ],
- // 对公交卡类通知,只要来源匹配即可视为出行支出,不强制正文关键字
- keywords: [],
- direction: 'expense',
- defaultCategory: 'Transport',
- autoCapture: true,
- // 优先从正文中提取「文三路->祥园路」之类的起点-终点信息作为“商户”
- merchantPattern: /([\u4e00-\u9fa5]{2,}\s*(?:-|—|>|→|至|到)\s*[\u4e00-\u9fa5]{2,})/,
- // 若无法提取线路,则使用统一名称
- defaultMerchant: '公交/地铁出行',
- },
+import notificationRulesSource from '../config/notificationRules.js'
+
+const fallbackRules = notificationRulesSource.map((rule) => ({
+ ...rule,
+ merchantPattern: rule.merchantPattern?.pattern
+ ? new RegExp(rule.merchantPattern.pattern, rule.merchantPattern.flags || '')
+ : null,
+}))
+
+const amountPatterns = [
+ /(?:¥|¥|RMB|CNY|人民币)\s*(-?\d+(?:\.\d{1,2})?)/i,
+ /(-?\d+(?:\.\d{1,2})?)\s*(?:元|块|块钱)/,
+ /(?:金额|实付|扣费|扣款|消费|支出|收入|收款|到账|入账|转入|转出|支付|票价|车费|本次乘车)\D{0,8}?(-?\d+(?:\.\d{1,2})?)/,
+ /(-?\d+(?:\.\d{1,2})?)\s*(?:元|块|块钱)?\s*(?:已支付|已收款|到账|入账)/,
]
let cachedRules = [...fallbackRules]
let remoteRuleFetcher = async () => []
-/**
- * 允许后续接入服务端规则同步,只需在应用启动时调用并注入实际的 fetch 函数。
- * 服务端可以返回一组规则对象,形如:
- * {
- * id: string;
- * label?: string;
- * enabled?: boolean; // false 时在本地完全忽略此规则
- * priority?: number; // 预留优先级字段(当前仅在服务端使用)
- * channels?: string[]; // 匹配通知来源(标题 / 包名)
- * keywords?: string[]; // 匹配通知正文的关键字
- * direction?: 'income' | 'expense';
- * defaultCategory?: string;
- * autoCapture?: boolean;
- * }
- * @param {(currentRules: Array) => Promise
} fetcher
- */
+const buildEmptyTransaction = (notification, options = {}) => ({
+ id: options.id,
+ merchant: options.merchant || 'Unknown',
+ category: options.category || 'Uncategorized',
+ amount: 0,
+ date: new Date(options.date || notification?.createdAt || new Date().toISOString()).toISOString(),
+ note: notification?.text || '',
+ syncStatus: 'pending',
+})
+
export const setRemoteRuleFetcher = (fetcher) => {
remoteRuleFetcher = typeof fetcher === 'function' ? fetcher : remoteRuleFetcher
}
@@ -167,47 +49,44 @@ export const loadNotificationRules = async () => {
}
} catch (err) {
cachedRules = [...cachedRules]
- console.warn('[rules] 使用本地规则,远程规则获取失败:', err)
+ console.warn('[rules] using cached fallback rules because remote sync failed:', err)
}
return cachedRules
}
export const getCachedNotificationRules = () => cachedRules
-// 从通知文本中提取交易金额,兼容「¥」「¥」「元」「人民币」等常见形式
const extractAmount = (text = '') => {
- const primary = text.match(
- /(?:[¥¥]|人民币)\s*(-?\d+(?:\.\d{1,2})?)|(-?\d+(?:\.\d{1,2})?)\s*元/,
- )
- const value = primary?.[1] || primary?.[2]
- if (value) {
- return Math.abs(parseFloat(value))
+ const normalized = String(text).replace(/,/g, '').trim()
+
+ for (const pattern of amountPatterns) {
+ const match = normalized.match(pattern)
+ const value = match?.[1]
+ if (!value) continue
+ const parsed = Math.abs(Number.parseFloat(value))
+ if (Number.isFinite(parsed) && parsed > 0) {
+ return parsed
+ }
}
- const loose = text.match(/(-?\d+(?:\.\d{1,2})?)/)
- return loose?.[1] ? Math.abs(parseFloat(loose[1])) : 0
+
+ return 0
}
-// 判断字符串是否更像是「金额」而不是商户名,用于避免把「1.00元」当成商户
const looksLikeAmount = (raw = '') => {
const value = String(raw).trim()
if (!value) return false
- const amountPattern = /^[-\d.,]+\s*(?:元|块钱|¥|¥|人民币)?$/
- return amountPattern.test(value)
+ return /^[-\d.,]+\s*(?:元|块|块钱|¥|¥|人民币)?$/i.test(value)
}
const extractMerchant = (text, rule) => {
const content = text || ''
if (rule?.merchantPattern instanceof RegExp) {
- const m = content.match(rule.merchantPattern)
- if (m?.[1]) return m[1].trim()
+ const matched = content.match(rule.merchantPattern)
+ if (matched?.[1]) return matched[1].trim()
}
- // 通用的「站点 A -> 站点 B」线路模式(公交/地铁通知),
- // 即使未命中专用规则也尝试提取,避免退化为金额或「Unknown」
- const routeMatch = content.match(
- /([\u4e00-\u9fa5]{2,}\s*(?:-|—|>|→|至|到|->)\s*[\u4e00-\u9fa5]{2,})/,
- )
+ const routeMatch = content.match(/([\u4e00-\u9fa5]{2,}\s*(?:-|->|→|至|到)\s*[\u4e00-\u9fa5]{2,})/)
if (routeMatch?.[1]) {
return routeMatch[1].trim()
}
@@ -218,16 +97,14 @@ const extractMerchant = (text, rule) => {
]
for (const pattern of genericPatterns) {
- const m = content.match(pattern)
- if (m?.[1]) return m[1].trim()
+ const matched = content.match(pattern)
+ if (matched?.[1]) return matched[1].trim()
}
- // 针对「……支出(消费支付宝-上海拉扎斯信息科技有限公司)3.23元」这类银行通知,
- // 优先尝试从括号中的「支付宝-商户名」结构中提取真正的商户名
const parenMatch = content.match(/\(([^)]+)\)/)
if (parenMatch?.[1]) {
const inner = parenMatch[1]
- const payMatch = inner.match(/(?:支付宝|微信支付|微信)[-—\s]*([^\d元¥¥]+)$/)
+ const payMatch = inner.match(/(?:支付宝|微信支付|微信)[-\s]*([^\d元¥¥]+)$/)
if (payMatch?.[1]) {
const candidate = payMatch[1].trim()
if (candidate && !looksLikeAmount(candidate)) {
@@ -247,32 +124,48 @@ const extractMerchant = (text, rule) => {
const matchRule = (notification, rule) => {
if (!rule || rule.enabled === false) return false
+
const channel = notification?.channel || ''
const text = notification?.text || ''
- // 频道命中:优先用通知标题/来源匹配,退而求其次用正文包含匹配(兼容 adb shell / 部分 ROM)
const channelHit =
!rule.channels?.length ||
- rule.channels.some((item) => channel.includes(item) || text.includes(item))
+ rule.channels.some((item) => channel.includes(item)) ||
+ (!channel && rule.channels.some((item) => text.includes(item)))
const keywordHit = !rule.keywords?.length || rule.keywords.some((word) => text.includes(word))
- return channelHit && keywordHit
+ const requiredTextHit =
+ !rule.requiredTextPatterns?.length ||
+ rule.requiredTextPatterns.some((word) => text.includes(word))
+
+ return channelHit && keywordHit && requiredTextHit
}
-/**
- * 根据当前规则集生成交易草稿,并返回命中的规则信息
- * @param {{ id: string, channel: string, text: string, createdAt: string }} notification
- * @param {{ ruleSet?: Array, overrides?: object }} options
- * @returns {{ transaction: import('../types/transaction').Transaction, rule?: object, requiresConfirmation: boolean }}
- */
export const transformNotificationToTransaction = (notification, options = {}) => {
const ruleSet = options.ruleSet?.length ? options.ruleSet : cachedRules
const rule = ruleSet.find((item) => matchRule(notification, item))
- const amountFromText = extractAmount(notification?.text)
- const direction = options.direction || rule?.direction || 'expense'
+
+ if (!rule) {
+ return {
+ transaction: buildEmptyTransaction(notification, options),
+ rule: undefined,
+ requiresConfirmation: false,
+ }
+ }
+
+ const amountFromText = extractAmount(notification?.text || '')
+ if (amountFromText <= 0) {
+ return {
+ transaction: buildEmptyTransaction(notification, options),
+ rule: undefined,
+ requiresConfirmation: false,
+ }
+ }
+
+ const direction = options.direction || rule.direction || 'expense'
const normalizedAmount =
direction === 'income' ? Math.abs(amountFromText) : -Math.abs(amountFromText)
let merchant = options.merchant || extractMerchant(notification?.text || '', rule) || 'Unknown'
- if (merchant === 'Unknown' && rule?.defaultMerchant) {
+ if (merchant === 'Unknown' && rule.defaultMerchant) {
merchant = rule.defaultMerchant
}
@@ -281,8 +174,8 @@ export const transformNotificationToTransaction = (notification, options = {}) =
const transaction = {
id: options.id,
merchant,
- category: options.category || rule?.defaultCategory || 'Uncategorized',
- amount: Number.isFinite(normalizedAmount) ? normalizedAmount : 0,
+ category: options.category || rule.defaultCategory || 'Uncategorized',
+ amount: normalizedAmount,
date: new Date(date).toISOString(),
note: notification?.text || '',
syncStatus: 'pending',
@@ -290,7 +183,7 @@ export const transformNotificationToTransaction = (notification, options = {}) =
const requiresConfirmation =
options.requiresConfirmation ??
- !(rule?.autoCapture && transaction.amount !== 0 && merchant !== 'Unknown')
+ !(rule.autoCapture && transaction.amount !== 0 && merchant !== 'Unknown')
return { transaction, rule, requiresConfirmation }
}
diff --git a/src/stores/settings.js b/src/stores/settings.js
index b046abf..304baed 100644
--- a/src/stores/settings.js
+++ b/src/stores/settings.js
@@ -1,5 +1,6 @@
-import { defineStore } from 'pinia'
+import { defineStore } from 'pinia'
import { ref } from 'vue'
+import { DEFAULT_CATEGORY_BUDGETS } from '../config/transactionCategories.js'
export const useSettingsStore = defineStore(
'settings',
@@ -7,17 +8,10 @@ export const useSettingsStore = defineStore(
const notificationCaptureEnabled = ref(true)
const aiAutoCategoryEnabled = ref(false)
const monthlyBudget = ref(12000)
- const budgetResetCycle = ref('monthly') // monthly | weekly | none | custom
- const budgetMonthlyResetDay = ref(1) // 每月第几天重置,1-28
- const budgetCustomStartDate = ref('') // 自定义起始日(ISO 日期字符串)
- const categoryBudgets = ref({
- Food: 0,
- Transport: 0,
- Health: 0,
- Groceries: 0,
- Entertainment: 0,
- Uncategorized: 0,
- })
+ const budgetResetCycle = ref('monthly')
+ const budgetMonthlyResetDay = ref(1)
+ const budgetCustomStartDate = ref('')
+ const categoryBudgets = ref({ ...DEFAULT_CATEGORY_BUDGETS })
const categoryBudgetEnabled = ref(false)
const profileName = ref('Echo 用户')
const profileAvatar = ref('')
@@ -115,4 +109,3 @@ export const useSettingsStore = defineStore(
},
},
)
-
diff --git a/src/views/AddEntryView.vue b/src/views/AddEntryView.vue
index 7220b1f..1b4fda6 100644
--- a/src/views/AddEntryView.vue
+++ b/src/views/AddEntryView.vue
@@ -1,5 +1,10 @@
-
@@ -169,10 +172,10 @@ transactionStore.ensureInitialized()
- {{ editingId ? '编辑记录' : '记录新消费' }}
+ {{ editingId ? '编辑记录' : '记录新流水' }}
@@ -214,7 +217,7 @@ transactionStore.ensureInitialized()
input-class="text-2xl font-bold"
>
- ¥
+ 楼
@@ -229,16 +232,16 @@ transactionStore.ensureInitialized()
@@ -258,7 +261,7 @@ transactionStore.ensureInitialized()
v-model="form.note"
rows="2"
class="mt-2 w-full rounded-2xl border border-stone-200 px-4 py-3 text-sm focus:outline-none focus:border-stone-400 resize-none"
- placeholder="可记录口味、心情或健康状态"
+ placeholder="可记录口味、心情或其他背景信息"
/>
@@ -272,7 +275,7 @@ transactionStore.ensureInitialized()
:disabled="saving"
@click="submitForm"
>
- {{ saving ? '保存中...' : editingId ? '保存修改' : '立即入账' }}
+ {{ saving ? '保存中...' : editingId ? '保存修改' : '立即记账' }}