diff --git a/.gitignore b/.gitignore index 21f9b91c..219b0ae5 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,5 @@ package/ data/ storage/ log/ + +.claude/ \ No newline at end of file diff --git a/MY_REQUIREMENTS.md b/MY_REQUIREMENTS.md new file mode 100644 index 00000000..5d251566 --- /dev/null +++ b/MY_REQUIREMENTS.md @@ -0,0 +1,172 @@ +# ezBookkeeping 个人需求清单 + +> 基于 fork 版本的定制开发需求,持续更新。 +> 标注:✅ 易实现 | ⚠️ 中等难度 | ❌ 难/暂缓 | ❓ 待确认 | 🟢 已完成 + +--- + +## 一、UI / 主题 + +### 1. ⚠️ 自选主题色 +**描述:** 在设置页面增加主题色选择器,用户可自由选择应用主色调。 + +**现状:** 主题色当前硬编码为 `#c67e48`,仅支持亮色/暗色切换。 + +**实现思路:** +- 在设置页新增颜色选择器组件 +- 将所选颜色保存至 `localStorage` +- 启动时读取颜色并注入 CSS 变量(Framework7 支持动态 primary color) +- 涉及文件:`MobileApp.vue`、`src/lib/settings.ts`、设置页面 + +--- + +## 二、账户功能 + +### 2. ⚠️ 信用卡账户:额度与剩余额度 +**描述:** 为信用卡类型账户新增「信用额度」字段,在账户列表和详情中展示「已用额度」和「剩余额度」。 + +**实现思路:** +- 后端:账户数据库表新增 `credit_limit` 字段,修改 API +- 前端:账户编辑页增加额度输入,账户列表/详情显示剩余额度(= 额度 - 欠款) +- 涉及文件:后端 `pkg/models/account.go`、前端 `EditPage.vue`、`ListPage.vue` + +### 8. ⚠️ 账户交易列表顶部显示账户信息 +**描述:** 在按账户筛选的交易列表页面顶部,显示账户名称、余额(或信用卡欠款+额度)等信息卡片。 + +**实现思路:** +- 检测当前是否为单账户筛选状态 +- 顶部插入账户信息卡片组件 +- 若多账户筛选则显示多张卡片 +- 涉及文件:`src/views/mobile/transactions/ListPage.vue` + +### 9. ✅ 允许直接修改账户余额(自动插入调整记录) +**描述:** 在账户详情提供「调整余额」入口,输入目标余额后,自动计算差值并插入一条「余额调整」类型交易记录。 + +**现状:** 代码中已存在 `ModifyBalance` 交易类型,可能已有后端支持,仅需确认前端入口是否存在。 + +**实现思路:** +- 确认 `ModifyBalance` 交易类型的前端入口 +- 若无,在账户详情页增加「调整余额」按钮,跳转至记账页并预填该类型 + +--- + +## 三、记账页面 + +### 3. ✅ 记账页显示当前账户余额 / 欠款+额度 +**描述:** 在记账页面选择账户后,在账户名称旁或下方显示该账户的当前余额(普通账户)或欠款+额度(信用卡)。 + +**实现思路:** +- 纯前端改动,从 account store 读取余额信息 +- 在账户选择区域旁增加余额展示 +- 涉及文件:`src/views/mobile/transactions/EditPage.vue` + +### 11. ✅ 记录上次选择的账户 +**描述:** 新建交易时,默认选中上次使用的账户,而非每次重置为默认账户。 + +**实现思路:** +- 保存账户 ID 到 `localStorage` +- 打开记账页时读取并预选 +- 涉及文件:`src/views/mobile/transactions/EditPage.vue`、`src/lib/settings.ts` + +--- + +## 四、小键盘 + +### 6. 🟢 小键盘布局调整 +**描述:** 调整数字键盘布局。 + +**已完成:** +``` +[数值显示区 ] [ ⌫ ] ← ⌫ 在显示区右侧,左边有竖线与右列对齐 + 7 8 9 × + 4 5 6 − + 1 2 3 + + C 0 . OK +``` +- ⌫ 位于数值显示区右侧,宽度 20% 与运算符列对齐,单击退格,长按清除 +- 左下角 C:单击清除全部 +- . 在 OK 左侧 +- × 保留 +- 涉及文件:`src/components/mobile/NumberPadSheet.vue` + +--- + +## 五、交易详情 + +### 7. 🟢 交易详情菜单增加「编辑」和「删除」 +**描述:** 交易详情页三点菜单中增加编辑和删除操作。 + +**已完成:** +- 三点菜单展开后,第一项为「Edit」(跳转编辑页) +- 最后一项为红色「Delete」(确认后删除并返回) +- 从详情编辑返回后自动刷新数据 +- 涉及文件:`src/views/mobile/transactions/EditPage.vue` + +--- + +## 六、分类选择 + +### 10. ✅ 分类选择默认全部展开 +**描述:** 在记账页面选择分类时,默认将所有一级分类展开显示子分类,或在设置中可配置哪些分类默认展开。 + +**实现思路:** +- 修改 `TreeViewSelectionSheet.vue` 中展开状态的初始值 +- 可选:在设置中增加「分类默认展开」开关 +- 涉及文件:`src/components/mobile/TreeViewSelectionSheet.vue` + +--- + +## 七、性能与动画 + +### 12. 🟢 底部 Tab 切换无动画 + 全局动画加速 +**描述:** 底部 5 个主 Tab 切换无动画,其余动画加速。 + +**已完成:** +- 5 个主 Tab(首页、交易列表、账户、统计、设置)路由加 `animate: false` +- 全局动画时长从 300ms 加快至 150ms(页面跳转、Sheet、ActionSheet、Popup、Dialog、Popover) +- 涉及文件:`src/router/mobile.ts`、`src/styles/mobile/global.scss` + +### 4. 🟢 点击响应卡顿优化 +**描述:** 点击按钮有延迟感,点不上的问题。 + +**已完成:** +- `tapHoldDelay` 从默认 750ms 降至 500ms,减少长按判定等待 +- 涉及文件:`src/MobileApp.vue` + +**未完成(已放弃):** +- HomePage 预加载 tags 数据(用户已 revert) + +--- + +## 八、离线 / 缓存 + +### 5. ❌ 本地优先 / 离线数据缓存(暂缓) +**描述:** 交易数据缓存至本地,优先展示缓存数据,后台静默拉取最新数据并更新。 + +**现状:** Service Worker 已实现静态资源缓存(图片、字体、地图瓦片),但**交易业务数据**目前不做本地缓存。 + +**为何难:** +- 需引入 IndexedDB 存储交易/账户/分类数据 +- 需处理本地与服务端的数据同步、冲突解决 +- 属于架构级改动,工作量较大 + +**建议:** 先完成其他需求,此项作为后期规划。 + +--- + +## 进度总览 + +| # | 需求 | 状态 | +|---|------|------| +| 1 | 自选主题色 | ⚠️ 待做 | +| 2 | 信用卡额度 | ⚠️ 待做(需改后端) | +| 3 | 记账页显示余额 | ✅ 待做 | +| 4 | 点击卡顿优化 | 🟢 部分完成 | +| 5 | 离线缓存 | ❌ 暂缓 | +| 6 | 小键盘布局 | 🟢 已完成 | +| 7 | 详情编辑/删除 | 🟢 已完成 | +| 8 | 账户信息卡片 | ⚠️ 待做 | +| 9 | 调整余额入口 | ✅ 待做 | +| 10 | 分类默认展开 | ✅ 待做 | +| 11 | 记住上次账户 | ✅ 待做 | +| 12 | Tab 无动画 + 加速 | 🟢 已完成 | diff --git a/src/components/mobile/NumberPadSheet.vue b/src/components/mobile/NumberPadSheet.vue index e0c0b248..0e8b99bc 100644 --- a/src/components/mobile/NumberPadSheet.vue +++ b/src/components/mobile/NumberPadSheet.vue @@ -6,8 +6,11 @@
{{ hint }}
-
- {{ currentDisplay }} +
+ {{ currentDisplay }} + + +
+ + + C + + + {{ digits[0] }} + {{ decimalSeparator }} {{ `${digits[0]}${digits[0]}` }} - - {{ digits[0] }} - - - - - - {{ confirmText }} @@ -467,12 +468,15 @@ watch(() => props.flipNegative, (newValue) => { } .numpad-values { + display: flex; + align-items: center; border-bottom: 1px solid var(--f7-page-bg-color); } .numpad-value { display: flex; position: relative; + flex: 1; padding-inline-start: 16px; line-height: 1; height: var(--ebk-numpad-value-height); @@ -481,6 +485,22 @@ watch(() => props.flipNegative, (newValue) => { user-select: none; } +.numpad-backspace-button { + display: flex; + align-items: center; + justify-content: center; + width: 20%; + height: var(--ebk-numpad-value-height); + font-size: 22px; + color: var(--f7-color-black); + flex-shrink: 0; + border-left: 1px solid var(--f7-page-bg-color); +} + +.dark .numpad-backspace-button { + color: var(--f7-color-white); +} + .numpad-value-small { font-size: var(--ebk-numpad-value-small-font-size); } diff --git a/src/router/mobile.ts b/src/router/mobile.ts index 17c4a1fe..0406b063 100644 --- a/src/router/mobile.ts +++ b/src/router/mobile.ts @@ -159,7 +159,8 @@ const routes: Router.RouteParameters[] = [ { path: '/transaction/list', async: asyncResolve(TransactionListPage), - beforeEnter: [checkLogin] + beforeEnter: [checkLogin], + options: { animate: false } }, { path: '/transaction/filter/amount', @@ -184,7 +185,8 @@ const routes: Router.RouteParameters[] = [ { path: '/account/list', async: asyncResolve(AccountListPage), - beforeEnter: [checkLogin] + beforeEnter: [checkLogin], + options: { animate: false } }, { path: '/account/add', @@ -209,7 +211,8 @@ const routes: Router.RouteParameters[] = [ { path: '/statistic/transaction', async: asyncResolve(StatisticsTransactionPage), - beforeEnter: [checkLogin] + beforeEnter: [checkLogin], + options: { animate: false } }, { path: '/statistic/settings', @@ -259,7 +262,8 @@ const routes: Router.RouteParameters[] = [ { path: '/settings', async: asyncResolve(SettingsPage), - beforeEnter: [checkLogin] + beforeEnter: [checkLogin], + options: { animate: false } }, { path: '/app_lock', diff --git a/src/styles/mobile/global.scss b/src/styles/mobile/global.scss index 6379c28d..c34cc764 100644 --- a/src/styles/mobile/global.scss +++ b/src/styles/mobile/global.scss @@ -3,6 +3,15 @@ html, body { position: fixed; } +:root { + --f7-page-transition-duration: 150ms; + --f7-sheet-transition-duration: 150ms; + --f7-actions-transition-duration: 150ms; + --f7-popup-transition-duration: 150ms; + --f7-dialog-transition-duration: 150ms; + --f7-popover-transition-duration: 150ms; +} + body { -ms-user-select: none; -webkit-user-select: none; diff --git a/src/views/mobile/transactions/EditPage.vue b/src/views/mobile/transactions/EditPage.vue index 86b7509b..6f87e9e7 100644 --- a/src/views/mobile/transactions/EditPage.vue +++ b/src/views/mobile/transactions/EditPage.vue @@ -459,10 +459,12 @@ {{ tt('Add Picture') }} + {{ tt('Edit') }} {{ tt('Duplicate') }} {{ tt('Duplicate (With Time)') }} {{ tt('Duplicate (With Geographic Location)') }} {{ tt('Duplicate (With Time and Geographic Location)') }} + {{ tt('Delete') }} {{ tt('Cancel') }} @@ -640,6 +642,7 @@ const pictureInput = useTemplateRef('pictureInput'); const isSupportClipboard = !!navigator.clipboard; const loadingError = ref(null); +const isFirstEntry = ref(true); const removingPictureId = ref(null); const transactionDateTimeSheetMode = ref('time'); const showTimeInDefaultTimezone = ref(false); @@ -1319,6 +1322,28 @@ function duplicate(withTime?: boolean, withGeoLocation?: boolean): void { props.f7router.navigate(`/transaction/add?id=${transaction.value.id}&type=${transaction.value.type}&withTime=${withTime ?? false}&withGeoLocation=${withGeoLocation ?? false}`); } +function navigateToEdit(): void { + props.f7router.navigate(`/transaction/edit?id=${transaction.value.id}&type=${transaction.value.type}`); +} + +function remove(): void { + showConfirm('Are you sure you want to delete this transaction?', () => { + showLoading(); + transactionsStore.deleteTransaction({ + transaction: transaction.value, + defaultCurrency: defaultCurrency.value + }).then(() => { + hideLoading(); + props.f7router.back(); + }).catch(error => { + hideLoading(); + if (!error.processed) { + showToast(error.message || error); + } + }); + }); +} + function onPageAfterIn(): void { routeBackOnError(props.f7router, loadingError); @@ -1326,6 +1351,21 @@ function onPageAfterIn(): void { && !geoLocationStatus.value && !transaction.value.geoLocation) { updateGeoLocation(false); } + + if (isFirstEntry.value) { + isFirstEntry.value = false; + return; + } + + if (mode.value === TransactionEditPageMode.View && query['id']) { + transactionsStore.getTransaction({ transactionId: query['id'], withPictures: true }).then(t => { + setTransactionModel(t, {}, true); + }).catch(error => { + if (!error.processed) { + showToast(error.message || error); + } + }); + } } function onPageBeforeOut(): void {