Compare commits

...

29 Commits

Author SHA1 Message Date
zhengchen.tao 1655f11514 fix(numpad): 全部按键 @click 换 @pointerdown.left,零延迟
Build Docker Image / build (push) Successful in 23m41s
Build Docker Image / deploy (push) Successful in 1m2s
第一阶段(touch-action: manipulation)只解了浏览器双击缩放延迟,但 F7
内部 tap 合成 + active-state 检测仍让 @click 比 pointerdown 慢一拍。
完全弃用 @click:

- 16 个按键(数字 0-9、运算 ×−+、C 清空、小数点/双零、OK 确认)改 @pointerdown.left
- 触屏 button=0 始终满足 .left 修饰符
- 桌面右键 button=2 不会误触发
- F7 active-state 视觉反馈基于独立 touchstart/touchend 监听,按下效果保留
- 退格键的 pointer 事件方案在上个 commit 已实现,本次不动

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:24:16 +08:00
zhengchen.tao 0fb2dfdc63 fix(numpad): 退格键按下零延迟 + 500ms 长按清空
Build Docker Image / build (push) Has been cancelled
Build Docker Image / deploy (push) Has been cancelled
弃用 @click + @taphold 组合(F7 需 ~750ms 判别 tap vs hold,期间抑制 click 导致单点延迟),改用原生 pointerdown/up/cancel/leave + 自管定时器:

- pointerdown 立即调 backspace()
- 同时启动 500ms 定时器,到点 clear()
- 任何抬起/移出/取消事件取消定时器
- sheet 关闭时也清

行为:单击立即删一位;按住不放先删一位、约 500ms 后清空全部。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:14:11 +08:00
zhengchen.tao d629dfe18c docs: README fork notice 改动清单翻译为英文(面向 GitHub mirror 受众)
FORK.md 仍保持中文。
2026-05-07 15:47:00 +08:00
zhengchen.tao 76a274e1cc docs: README 移除已回滚的「交易时间点击标题默认日期选择器」条目
与 FORK.md item #8 的回滚状态对齐(commit 78818470)。
2026-05-07 15:32:24 +08:00
zhengchen.tao 291bd86c94 LICENSE: fork 版权署名顺序订正 Tao Zhengchen → Zhengchen Tao 2026-05-05 18:57:09 +08:00
zhengchen.tao 8658e849e7 ci: 镜像路径 dev/ezbookkeeping → zhengchen.tao/ezbookkeeping
仓库已转移到 zhengchen.tao 命名空间,build-image.yml 里的 OCI source label /
image tags / build summary 三处硬编码路径同步更新,否则 push 会推到不存在的
dev 命名空间。

NAS 上 nas-infra/ezbookkeeping/docker-compose.yml 的 image 路径仍需手动更新。
2026-05-05 18:20:30 +08:00
zhengchen.tao 6d0329210f FORK.md 进度表同步:item #8 标记已回滚 2026-05-05 18:10:00 +08:00
zhengchen.tao c57c1e8490 revert(transaction-time): 回滚 #header label click 改 'date' 模式
实际无可见效果——用户点的是下方 #title 里的日期/时间文本(上游 commit 368322f9
已实现按点击内容路由),#header label 行很少被点。改回上游行为。

FORK.md item #8 标记为已回滚。
2026-05-05 18:09:31 +08:00
zhengchen.tao 2425c358e3 docs: 扩充 README fork notice,列出主要定制项;LICENSE 增加 fork 修改的版权行 2026-05-05 18:05:18 +08:00
zhengchen.tao 373ccba9d6 build-image.yml 注释微调(再次触发,验证 DSM API 1.43 锁定后 build 通)
"等" → "等等",顺便利用这次 push 验证 nas-infra cd71de1 把
DOCKER_API_VERSION=1.43 加到 runner 容器 env 后,docker login 是否过。
2026-05-04 01:18:07 +08:00
zhengchen.tao ce345f79ab build-image.yml 注释微调(触发 concurrency cancel-in-progress 测试)
把 "重新打包旧 commit / 用自定义 tag / 等" 里多余的斜杠改成顿号,
顺便利用这次 push 验证上一条 d92e4fe3 的 in-progress run 是否被取消。
2026-05-04 00:40:43 +08:00
zhengchen.tao 6baf668696 build/deploy 合并为单 workflow 双 job,删除 deploy.yml
原 workflow_run 链触发会在 Actions 列表产生两条独立 run,UX 割裂。
合并后单 run + dependency graph 显式串联 build → deploy。

代价:失去"不 rebuild 只 redeploy"的 UI 单点触发,临时只想
重启容器需直接 ssh NAS 跑 docker compose up -d。

paths-ignore 同步移除已不存在的 deploy.yml 项。
2026-05-04 00:37:44 +08:00
zhengchen.tao 9ef0e62b05 fix(deploy): 加 docker login 步骤,否则 pull 私有 registry 401
build-image 跑过 docker login,但 deploy 是独立 workflow 容器,
凭据不继承。需要在 deploy 这边也登一次同一个 PACKAGES_TOKEN。

deploy.yml 在 paths-ignore 里,这次提交不会触发 build。但会触发
deploy 自己(不在 paths-ignore,且 workflow_dispatch 仍可手动)。
要测的话手动 dispatch Deploy Docker Image 即可。
2026-05-02 23:39:15 +08:00
zhengchen.tao 7df2d49c56 build-image paths-ignore 加 deploy.yml / sync-upstream.yml
改 deploy.yml 或 sync-upstream.yml 都不影响镜像内容(前者是部署
脚本、后者是 main reset 脚本),原本会触发整套 ~10 min 的 build
是浪费。把它俩加进 paths-ignore 跳过。

build-image.yml 自己保留触发(不在 ignore 里),这样改 workflow
能 self-test 验证改动有效。
2026-05-02 21:23:18 +08:00
zhengchen.tao 65d52571de build/deploy workflow 加 concurrency cancel-in-progress
并发组 = workflow name + ref。同分支连续 push 时:
- 新 run 入组发现已有 in-progress run → 立即取消旧的,新的开跑
- 最终只构建 + 部署最新代码,省 CI 时间
- 不同分支的 build/deploy 互不干扰(虽然当前只 custom 用)
- build 与 deploy 是两个独立 workflow name,互不影响(build 跑时
  deploy 不会被取消,反之亦然)

CLAUDE.md 同步加"并发取消策略"段说明该行为。
2026-05-02 21:18:39 +08:00
zhengchen.tao 4bdd2c7195 deploy.yml 移除 CUSTOM_DEPLOY_SCRIPTS 变量,部署脚本内联
原 vars.CUSTOM_DEPLOY_SCRIPTS 来自 deploy.yml 上游模板设计,本意是
"通用 hook,让一份 deploy.yml 复用到不同项目"。本仓库只有一个项目
一种部署场景,这层抽象纯属累赘:
- 改部署逻辑得去 Gitea UI 点 Variables,没法 PR review
- git log 看不到部署逻辑改动
- 脚本到底跑啥得对照 yml + Variable 两处

直接把 docker compose pull/up 内联进 deploy.yml,单一事实源在 git。

支持私有 / 公开两种 nas-infra:
- secrets.NAS_INFRA_TOKEN 设了 → 用 token clone(私有适用)
- 没设 → 裸 URL clone(公开适用)

CLAUDE.md 同步更新 deploy.yml 的描述。
2026-05-02 21:10:35 +08:00
zhengchen.tao 9da91ad54f deploy.yml 改自动触发:build 成功后 workflow_run 链式触发
- on.workflow_run: 监听 Build Docker Image 完成事件,分支限 custom
- if 条件:仅在 build 成功时跑 deploy(失败时跳过,避免部署半成品)
- workflow_dispatch 保留作为手动备选(重新部署当前镜像 / 应急脚本)
- 脚本生成改 > 覆盖(原 >> 会累积历史脚本)+ 加 set -e 失败即停
- 加 Deploy summary 步骤把触发链路信息写入 GITHUB_STEP_SUMMARY
  方便从 UI 看到本次 deploy 跟在哪次 build 后面

CLAUDE.md 同步更新 workflow 清单 + 流程图:现在 push → build →
deploy 全自动 CD,仅需在 repo Variables 里配 CUSTOM_DEPLOY_SCRIPTS
脚本内容才能产生实际部署效果。
2026-05-02 21:07:09 +08:00
zhengchen.tao 55c175acca ci: 注释 paths-ignore 对 empty commit 的 vacuously-skip 行为
empirical discovery:c6bb0c85 那个 --allow-empty commit 推送后没有
触发 build。证实 Gitea Actions(同 GitHub Actions)对 paths-ignore
+ empty commit 的处理是"vacuously matches ignore list, skip"。

把这个 quirk 直接注释在 workflow 文件里,后续自己或 Claude 看到
build 没触发时不用再怀疑 trigger 配置错了,知道是 empty commit 的
正常行为。
2026-05-02 20:49:17 +08:00
zhengchen.tao 3ed37e7719 ci: trigger auto-build to verify push trigger works
empty commit, no code changes. 用于验证 build-image.yml 的 push trigger
确实在 commit 推送后自动跑起来,不需要去 Actions UI 手动点。

如果这次 push 后 Gitea Actions 没有自动出现新 run,说明 paths-ignore
对 empty commit 的过滤行为是"vacuously skip",需要至少一个非 ignore 路径
的真实改动才能触发。
2026-05-02 20:47:05 +08:00
zhengchen.tao 32a49be913 build-image workflow 改自动触发 + 保留手动备选
- on.push.branches: [custom] —— 推送 custom 自动跑
- on.push.paths-ignore:屏蔽 *.md / .gitignore / LICENSE / screenshot/**
  避免 doc-only 改动浪费 ~10 min 构建
- on.workflow_dispatch 保留作为应急通道(重打旧 commit / 自定义 tag)
- checkout ref 兼容两种触发:${{ inputs.branch || github.ref_name }}
  - workflow_dispatch:用用户填的 branch(默认 custom)
  - push:fallback 到 github.ref_name(即触发分支,永远是 custom)
- Build summary 加"触发方式"行,便于区分本次是自动还是手动

预期:本提交本身就会触发自动构建(改了 .gitea/workflows/build-image.yml
不在 paths-ignore 内),相当于 self-test。
2026-05-02 20:41:22 +08:00
zhengchen.tao ebcc03d3d0 docs: ci 分支已删除,更新 CLAUDE.md 与 build summary 措辞
ci 分支于 2026-05-02 删除(默认分支已切到 custom,workflow 文件已
迁回 custom)。更新各处反映"两分支模型 main + custom"的最终状态:

- CLAUDE.md "三个分支" → "两个分支",ci 段改写为"已退役 + 历史
  说明",给后续 Claude 解释 git log 里 555ecc1a 这条迁移提交
- 同步历史里 2026-05-02(后续) 那条加上"随后删掉 ci 分支"的事实
- build-image.yml 的 Build summary 步骤移除"UI 顶部 commit 是 ci"
  的警示注释(workflow 已在 custom,runs 列表 commit 直接就是
  代码 commit,不再需要这条解释)
2026-05-02 20:34:38 +08:00
zhengchen.tao 0be04287c8 docs: CLAUDE.md 更新为 workflow 已迁到 custom 的事实
- 三分支表更新:custom 是 default branch + 持有 workflow,ci
  降级为过渡态历史分支
- 改写"为什么 workflow 在 custom 不在独立分支"段,记录设计决策
  演进的真实理由(runs 列表 UX 优于 meta/code 分离的设计美感)
- workflow 清单从 5 项缩到 3 项,记录 docker-release/snapshot
  已删的事实
- "给后续 Claude" 提示中"不要把工作流提交到 custom" 改为相反
  方向(直接在 custom 改 workflow)
- 同步历史补 555ecc1a (workflow 迁移) 与 75b4d78d (numpad fix)
2026-05-02 18:38:00 +08:00
zhengchen.tao dfbc2b1440 ci: workflow 文件迁到 custom 分支
之前 workflow 在 ci 分支,导致每次 dispatch 后 Gitea Actions 列表
显示的 commit 都是 ci 分支的 workflow 文件 commit,不是被实际构建
的 custom 代码 commit,UX 上误导性强。

挪到 custom 后:
- runs 列表的 commit 字段直接显示真实代码 commit
- workflow_dispatch UI 自动从默认分支(待手动切到 custom)发现
  workflow
- rebase 上游时 workflow 文件随 custom 一起平移,无额外操作

同步移除上游残留的 docker-release.yml / docker-snapshot.yml:
- 触发依赖 secrets.DOCKER_REPO(未配),sync-upstream 推 main
  /tags 时空跑失败
- ci 上已禁用,但文件留着是噪声,本次清掉

ci 分支 .gitea/workflows/ 暂保留作过渡,待用户在 Gitea UI 把
默认分支切到 custom + 验证 build 跑通后,再单独 cleanup ci。
2026-05-02 18:35:53 +08:00
zhengchen.tao 47b5641597 docs: CLAUDE.md 解释 ci/custom 不需保持一致 + UI commit 不等于构建 commit
补充三分支拓扑后的"FAQ 段":

- ci 与 custom 内容不重叠是设计,不是 bug
- Gitea Actions UI 顶部显示的 commit 是 workflow dispatch 触发
  位置(即 ci 的 HEAD),不是构建源代码 commit
- 真实构建的代码 commit 在镜像 tag / OCI revision label /
  workflow 末尾 Build summary 三处都能看到,看 summary 区即可

附两条可选替代方案(workflow 挪 custom / 加 push trigger 自动构建),
说明当前选择的中间路径理由。
2026-05-02 18:26:40 +08:00
zhengchen.tao 11da502f75 fix(numpad): 修小键盘点击卡顿,touch-action: none → manipulation
诊断:用户反馈仅小键盘点击有延迟感,其他按钮正常。范围缩小后定位到
.numpad-button 上的 touch-action: none(上游 e178a079 引入)与 F7
内部 tap 事件处理叠加,让 click 事件合成慢一拍。backspace(自定义
.numpad-backspace-button 类)不受影响,刚好印证范围。

修复:改为 touch-action: manipulation(W3C 标准"快速点击"值),禁双
击缩放消除老 300ms 延迟,但保留 click 事件正常合成。

FORK.md #11 状态:🔍 调查中 → 🟢 已完成,附真因记录避免后续误诊。
2026-05-02 18:09:40 +08:00
zhengchen.tao 29c164439c docs: CLAUDE.md 加 ci 分支 workflow 清单 + 链 FORK.md
ci 分支 5 个 workflow(sync-upstream / build-image / deploy /
docker-snapshot / docker-release)全部列表化,含触发条件、职责、
当前状态。明确"日常只用 sync-upstream + build-image 两个,
其他三个要么按需配置要么后续清理"。

custom 分支说明里加链接指 FORK.md(feature 维度的清单),
顶部说明区域厘清"meta(CLAUDE.md)vs 改动清单(FORK.md)vs
通用决策框架(不入库)"三层关系。
2026-05-02 17:58:43 +08:00
zhengchen.tao 989ffef156 docs: rename MY_REQUIREMENTS.md → FORK.md, drop stale DEPLOY.md
- 重命名为 FORK.md,对接更通用的 fork-doc 命名约定
- DEPLOY.md 内容全过期(ghcr.io 镜像、myrequirement 分支、
  docker run 风格部署),全部已废 — 部署文档现在两层:
  nas-infra/README.md(compose level)+ CLAUDE.md(CI 排查)
- README.md 顶部加一行 fork notice,链到 FORK.md 与 CLAUDE.md
  (单行变更,rebase 友好)
- FORK.md 顶部加关联文档表
2026-05-02 17:58:34 +08:00
zhengchen.tao c929e950e1 docs: CLAUDE.md 加 backend 测试踩坑 + 通用排查原则
CI 故障排查路径表加一行"测试/lint 失败 → 先看 Dockerfile ARG",
强调先对齐上游 CI 跳过开关(BUILD_PIPELINE / CHECK_3RD_API /
SKIP_TESTS)再考虑改测试代码。同步历史补 2026-05-02 第三层修复。
2026-05-02 16:07:58 +08:00
zhengchen.tao 6b8d9fcb13 docs: 加仓库级 CLAUDE.md(分支拓扑 + 同步流程 + CI 排查路径)
记录 fork 工作流(main 锚 tag / custom 改动 / ci 工作流),
2026-05-01 rebase 与 2026-05-02 build 修复历史,
以及给后续 Claude 会话的 CI 故障分流路径表。
2026-05-02 15:35:47 +08:00
12 changed files with 445 additions and 294 deletions
+167
View File
@@ -0,0 +1,167 @@
name: Build Docker Image
on:
# 自动触发:push 到 custom 分支时跑(force-push 后的 rebase 也会触发,可接受)
# paths-ignore:纯文档/配置改动跳过,避免浪费 ~10 分钟构建
# ⚠️ 已知 quirk2026-05-02 验证):empty commitgit commit --allow-empty
# 不会触发 paths-ignore 过滤的 workflowGitea 把 zero-paths-changed 当作
# "vacuously matches ignore list" 跳过。要强制触发必须至少改一个非 ignore 路径
# 的真实文件(改这个 yml 自己最稳)。
push:
branches: [custom]
paths-ignore:
- '**.md'
- '.gitignore'
- 'LICENSE'
- 'screenshot/**'
# sync-upstream.yml 改的是 main reset 逻辑,跟 build 无关
# build-image.yml 自己留着会触发,作为 workflow 改动的 self-test
- '.gitea/workflows/sync-upstream.yml'
# 手动触发:保留作为应急通道(重新打包旧 commit、用自定义 tag 等等)
# 注意:手动触发也会跑 deploy job —— 如果只想 build 不部署,临时把 deploy
# job 注释掉或在 deploy 里加 if 条件
workflow_dispatch:
inputs:
branch:
description: '要打包的分支(仅手动触发生效)'
required: true
default: 'custom'
tag:
description: '镜像 tag(留空则用 commit short hash'
required: false
default: ''
# 并发控制:同一分支的连续 push 只跑最新的,旧 in-progress run 会被取消
# 例:连续 3 次 push,第 1 次 build 跑了 30s,第 2 次开始 → 取消第 1,第 2 跑;
# 期间第 3 次又来 → 取消第 2,第 3 跑。最后只构建+部署最新代码,省 CI 时间。
# group 包含 ref 是为了不同分支的 build 互不干扰(虽然当前只有 custom 用)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout target branch
uses: actions/checkout@v4
with:
# workflow_dispatch 时用用户填的 branchpush 触发时 inputs.branch 为空,
# fallback 到 github.ref_name(即触发的分支名,push 到 custom 时就是 custom
ref: ${{ inputs.branch || github.ref_name }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
# 钉到 v0.13.2(自带 runc 1.1.x),避免 runc 1.2+ 的 procfs 安全检查
# 在 DSM 老内核(4.4.x)上撞 openat2/fsmount 不存在导致 build 失败
driver-opts: |
image=moby/buildkit:v0.13.2
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.zhengchentao.win
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Determine image tag and revision
id: meta
run: |
if [ -n "${{ inputs.tag }}" ]; then
IMAGE_TAG="${{ inputs.tag }}"
else
IMAGE_TAG="$(git rev-parse --short HEAD)"
fi
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "full_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "==> Image tag: $IMAGE_TAG"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
# 上游 Dockerfile 用 BUILD_PIPELINE 作为 CI 跳过开关:
# 设为 "1" 时 pkg/exchangerates 跳过依赖第三方 API 的活测试
# (加拿大银行/乌兹别克央行 API 国内不稳,跑就超时)
# CHECK_3RD_API 留空 → 三方 API 测试不跑;想跑设 "1"
build-args: |
BUILD_PIPELINE=1
# OCI 标签:
# - source 让 Gitea 收包时自动把镜像关联到对应 repo(不再需要手动去
# "包设置 → 链接到仓库")
# - revision 把构建时的 commit full SHA 烙进镜像 manifest
# docker inspect 能反推回源码版本
labels: |
org.opencontainers.image.source=https://git.zhengchentao.win/zhengchen.tao/ezbookkeeping
org.opencontainers.image.revision=${{ steps.meta.outputs.full_sha }}
tags: |
git.zhengchentao.win/zhengchen.tao/ezbookkeeping:${{ steps.meta.outputs.image_tag }}
git.zhengchentao.win/zhengchen.tao/ezbookkeeping:latest
- name: Build summary
# 把构建出的镜像 tag 与源 commit 显式列在 Action run summary 区,
# 方便从 UI 一眼看到本次 build 产出。always() 保证 build 失败也输出。
if: always()
run: |
{
echo "## Build Summary"
echo ""
echo "| 项 | 值 |"
echo "|---|---|"
echo "| 触发方式 | \`${{ github.event_name }}\` |"
echo "| 源分支 | \`${{ inputs.branch || github.ref_name }}\` |"
echo "| 源 commit (full) | \`${{ steps.meta.outputs.full_sha }}\` |"
echo "| 源 commit (short) | \`${{ steps.meta.outputs.image_tag }}\` |"
echo "| 镜像 tag | \`git.zhengchentao.win/zhengchen.tao/ezbookkeeping:${{ steps.meta.outputs.image_tag }}\` + \`:latest\` |"
} >> "$GITHUB_STEP_SUMMARY"
deploy:
# needs: build 串起来 —— build 失败 deploy 自动跳过,无需 if 条件
needs: build
runs-on: ubuntu-latest
steps:
# 登录 Gitea Container Registry,否则 docker compose pull 私有镜像 401。
# 跟 build job 那步是同一个 PACKAGES_TOKEN,但每个 job 跑在独立 runner 上,
# 凭据不会从 build job 继承,必须在这里再登一次。
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.zhengchentao.win
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Pull and restart ezbookkeeping
# 部署逻辑直接内联在这。runner 容器挂了 host docker.sock
# 所以这里 docker 命令直接操作的是宿主机 docker daemon
# 容器层面相当于 "ssh 到 NAS 跑 docker compose"。
#
# NAS_INFRA_TOKEN secret 仅在 nas-infra 是私有仓库时需要;
# 公开仓库不设这个 secret 也能拉。
env:
NAS_INFRA_TOKEN: ${{ secrets.NAS_INFRA_TOKEN }}
run: |
set -e
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# 决定 clone URL:有 token 用 token(私有),没有用裸 URL(公开)
if [ -n "$NAS_INFRA_TOKEN" ]; then
CLONE_URL="https://x-access-token:${NAS_INFRA_TOKEN}@git.zhengchentao.win/dev/nas-infra.git"
else
CLONE_URL="https://git.zhengchentao.win/dev/nas-infra.git"
fi
git clone --depth 1 "$CLONE_URL" "$TMPDIR/nas-infra"
cd "$TMPDIR/nas-infra/ezbookkeeping"
docker compose pull
docker compose up -d
# 简单 health:列容器状态 + 输出最近日志
sleep 3
docker compose ps
docker compose logs --tail=30 ezbookkeeping
-17
View File
@@ -1,17 +0,0 @@
name: Deploy Docker Image
on:
workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Execute custom script
run: |
cat >> deploy.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_DEPLOY_SCRIPTS }}
EOF
chmod +x deploy.sh
./deploy.sh
-64
View File
@@ -1,64 +0,0 @@
name: Docker Release
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up the environment
id: setup
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_RELEASE_PLATFORMS }}
push: true
build-args: |
RELEASE_BUILD=1
BUILD_PIPELINE=1
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
BUILD_DATE=${{ steps.setup.outputs.build_date }}
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-63
View File
@@ -1,63 +0,0 @@
name: Docker Snapshot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
type=sha,format=short,prefix=SNAPSHOT-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up the environment
id: setup
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
push: true
build-args: |
BUILD_PIPELINE=1
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
BUILD_DATE=${{ steps.setup.outputs.build_date }}
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+39
View File
@@ -0,0 +1,39 @@
name: Sync from upstream
on:
workflow_dispatch:
inputs:
tag:
description: '要同步的 release tag(留空则同步到 upstream/main 的最新 tag'
required: false
default: ''
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.SYNC_TOKEN }}
- name: Sync main to release tag
run: |
git config user.name "gitea-actions"
git config user.email "actions@gitea.local"
git remote add upstream https://git.zhengchentao.win/mirror/ezbookkeeping.git
git fetch upstream --tags
if [ -n "${{ inputs.tag }}" ]; then
TARGET="${{ inputs.tag }}"
else
TARGET=$(git tag -l --sort=-v:refname | head -n 1)
fi
echo "==> Syncing main to $TARGET"
git rev-parse "$TARGET" || { echo "❌ Tag $TARGET not found"; exit 1; }
git checkout -B main origin/main
git reset --hard "$TARGET"
git push origin main --force-with-lease
git push origin --tags
+127
View File
@@ -0,0 +1,127 @@
# CLAUDE.md
本项目是 [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) 的个人 fork。
> **本文件**:仓库分支模型、上游同步流程、CI 故障排查 —— meta 层
> **[`FORK.md`](FORK.md)**fork 相对上游的具体改动清单(feature 维度 + 进度状态)
> **个人笔记**:通用 fork 工作流决策框架在 `fork-工作流决策框架.md`(不入库)
本文件只记录**这个仓库的具体事实**,避免 Claude 会话误判。
---
## 仓库拓扑
```
github.com/mayswind/ezbookkeeping (上游)
│ Gitea pull mirror(后台异步)
git.zhengchentao.win/mirror/ezbookkeeping (只读镜像)
│ CI workflow 拉过来
git.zhengchentao.win/dev/ezbookkeeping origin,本地唯一 remote
```
本地 `git remote -v` 只有 origin 一项,**没有手工配 upstream**。上游同步通过 custom 分支上的 workflow 在服务端完成,不是本地操作。
---
## 两个分支的职责(必须先理解,否则会改错地方)
| 分支 | 职责 | force push? |
|---|---|---|
| `main` | **锚定上游 release tag**(当前 v1.4.0)。被 `.gitea/workflows/sync-upstream.yml` `git reset --hard <tag>` 覆写。**别在 main 上做任何改动** | 是(由 CI 做) |
| `custom` | **所有个人改动 + workflow 文件都在这**:信用额度功能、UI 调整、个人需求清单、`.gitea/workflows/*.yml` 等。具体改动清单见 [`FORK.md`](FORK.md)。日常开发分支,**default branch** | 是(rebase 后人工做) |
⚠️ **default branch 是 `custom`**`git clone` 默认 checkout custom,直接是开发分支。
### 历史:曾经存在过的 ci 分支(已退役)
2026-05-02 之前曾经有第三个分支 `ci`,最初设计是把 `.gitea/workflows/*.yml` 单独放它上面以"meta/code 分离"。两周后发现 Gitea Actions runs 列表显示的 commit 是 workflow 文件所在 commit(即 ci 的 HEAD),不是被构建的代码 commit,UX 误导性强。
把 workflow 挪回 custom 之后:
- runs 列表 commit = 真实代码 commit ✅
- `git clone` 默认落 custom 直接是开发分支 ✅
- rebase 上游时 workflow 跟 custom 一起平移 ✅
- 代价:失去"workflow 与代码完全独立"的设计美感 —— 这个分离原本就是过度设计
**ci 分支于 2026-05-02 删除**,仅保留这段说明给后续 Claude 会话理解 git log 里"workflow 文件迁到 custom"这条提交(commit `555ecc1a`)的来龙去脉。**workflow 改动直接在 custom 上做**。
---
## custom 分支 workflow 清单
`.gitea/workflows/` 当前有 2 个 workflow2026-05-04 起 build+deploy 合并为单 workflow 双 job):
| 文件 | 触发 | 干什么 | 状态 |
|---|---|---|---|
| `sync-upstream.yml` | 手动(`workflow_dispatch`,可填 tag | 服务端把 `dev/main` 强制 reset 到 mirror 上的指定 release tag(默认最新),然后 `push --force-with-lease` + 推 tags | ✅ 在用 |
| `build-image.yml` | **自动**push 到 custom 触发,`paths-ignore` 屏蔽 `**.md` / `.gitignore` / `LICENSE` / `screenshot/**` / `sync-upstream.yml`+ 手动备选 | **两个 job 串联在同一 run 里**:①`build` job 装 buildkit v0.13.2 → 登录 Gitea registry → 构建镜像(带 OCI 标签 source/revisionGitea 自动关联包到 repo)→ push 到 `git.zhengchentao.win/dev/ezbookkeeping:<hash>``:latest``build-args: BUILD_PIPELINE=1` 跳过活 API 测试。②`deploy` job (`needs: build`) 登录 registry → clone nas-infra → `docker compose pull && up -d` 重启 ezbookkeeping。私有 nas-infra 需要 `secrets.NAS_INFRA_TOKEN`,公开仓库不需要。UI 上 Actions 列表显示一条 runrun 详情里 dependency graph 显示 build → deploy | ✅ 日常发布通道 + 自动 CD |
**已删**
- `docker-snapshot.yml` / `docker-release.yml`2026-05-02,依赖未配的 `secrets.DOCKER_REPO`,永远失败)
- `deploy.yml`2026-05-04,合并进 `build-image.yml` 作为第二个 job,理由:原先 `workflow_run` 链触发会在 Actions 列表产生两条独立 runUX 割裂;合并后单 run + dependency graph 看 build/deploy 状态一目了然)
需要时再从 git 历史 cherry-pick 回来。
---
## 同步发布流程(rebase 模型)
1. 上游出新 release(如 v1.4.0)→ Gitea pull mirror 自动把 tag 同步到 mirror
2. 人工触发 `Sync from upstream` workflow → 服务端把 dev/main reset 到该 tag
3. 本地 `git fetch && git checkout custom && git rebase origin/main`
4. 解冲突(如有)→ 验证 → `git push --force-with-lease origin custom`
5. **build-image workflow 自动触发**force-push 也算 push 事件),构建新镜像;不需要手动点
日常 feature commit 流程(全自动 CD):
1. 在 custom 上改代码 → commit → push
2. **自动触发 build job**(除非只改了 `**.md` / `.gitignore` / `LICENSE` / `screenshot/**` / `sync-upstream.yml`
3. build 成功 → **同 run 内 deploy job 接力跑**`needs: build` 串联):clone nas-infra → docker compose pull → up -d
4. 整条 push → build → deploy 链路无人工介入,UI 上是单条 run
**并发取消策略**`build-image.yml` 设了 `concurrency.cancel-in-progress: true`,连续多次 push 时**只构建+部署最新那一次**(同 run 里 build/deploy 是原子单元,一起取消)。例:连续 3 次 push 间隔 30 秒,第 1 次 build 跑到 30%、第 2 次到来取消它、第 3 次又取消第 2,最终只 build + deploy 第 3 次的代码。省 CI 时间又保证最终一致性。
如果想跳过 build/deploy(例如手动多次 push 调试),commit 时只改文档相关文件即可(落在 paths-ignore 范围内)。如果想强制重打某个旧 commit,去 Actions UI 手动触发 `Build Docker Image`,填要打包的 branch / tag —— 注意手动触发也会跑 deploy job,**没有"只重新部署不重新 build"的单点入口了**(合并的代价,原 `deploy.yml` 那条路径已废)。临时只想重启容器:直接到 NAS 上 `docker compose up -d` 或在 Actions UI 临时禁用 deploy job。
**为什么 rebase 不 merge**:个人项目,无团队协作语义要保留,线性历史更清爽。
---
## 给后续 Claude 会话的明确提示
- 用户说"我的分支" / "切换到我的分支" → 指 `custom`
- 用户说"rebase main" → 指 `git rebase origin/main`,目标是把 custom 的改动叠到最新上游 tag 之上
- **不要在 `main` 分支上提交任何东西**(会被 CI 覆写)
- **workflow 文件改动直接在 custom 上做**2026-05-02 起,不再是 ci 分支)
- force-push custom 是常规操作,但每次用 `--force-with-lease`,不直接 `--force`
- 如果发现本地配了 upstream remote,那是历史遗留,不要依赖;以 origin/main 为准
- `.claude/``.gitignore` 里(个人本地配置不入库),但 `CLAUDE.md` 本身入库
---
## 同步历史
- **2026-05-01**rebase custom → origin/main (v1.4.0)。22 个 custom-only 提交(含一个旧的 `Merge branch 'main' into myrequirement` commit)压平为 21 个线性提交。已 force-push origin/custom`08c69042``fe265259`)。
- **2026-05-02**:修 Gitea Actions `Build Docker Image` 工作流。三层故障,全部不在本仓库代码里:
- **TLS 雷**`docker login` 走 host 进程不命中 PREROUTING REDIRECT,且 v6 撞 DSM nginx 的 CF Origin Cert。NAS 侧修:iptables 补 OUTPUT 对称规则 + `/etc/hosts` 显式 v4 兜底。详见 obsidian vault [[NAS/notes/内网证书路径]] §三.5/§三.6
- **buildkit 内核兼容**runc 1.2+ 撞 DSM 4.4 内核。`.gitea/workflows/build-image.yml``moby/buildkit:v0.13.2`commit `acdbb5bf`
- **backend 单元测试撞活 API**`pkg/exchangerates/``TestExchangeRatesApiLatestExchangeRateHandler_*` 跑活 API(加拿大银行 / 乌兹别克央行),国内访问超时。upstream Dockerfile 已设 `ARG BUILD_PIPELINE`,测试代码看到 `BUILD_PIPELINE=1 && CHECK_3RD_API!=1` 时早退。修:workflow 加 `build-args: BUILD_PIPELINE=1`commit `2dd8f099`),对齐上游 GH Actions
- **2026-05-02 (后续)**workflow 文件从 ci 分支迁到 customdefault branch 切到 customcommit `555ecc1a`),随后**删掉 ci 分支**。原因:Gitea Actions runs 列表的 commit 字段一直显示 ci 的 workflow commit,不是被构建的代码 commit,UX 误导性强。挪到 custom 后列表直接显示真实代码 commit。同时清理上游残留的 `docker-release.yml` / `docker-snapshot.yml`(依赖未配的 `secrets.DOCKER_REPO`,永远失败)。仓库回到朴素的 main + custom 双分支模型
- **2026-05-02 (numpad fix)**FORK.md #11 定位 + 修复。小键盘点击卡顿真因是 `.numpad-button``touch-action: none`(上游 e178a079 引入)与 F7 tap 处理叠加,改为 `touch-action: manipulation`commit `75b4d78d`
- **2026-05-04**:把 `deploy.yml` 合并进 `build-image.yml` 作为第二个 job`needs: build`),删除 `deploy.yml`。原先 `workflow_run` 链路会在 Actions 列表产生两条独立 runbuild 完一条、deploy 又一条),用户视角割裂;合并后 UI 列表单条 runrun 详情里 dependency graph 显示 build → deploy 串联。代价:失去"不 rebuild 只 redeploy"的 UI 单点触发,临时只想重启容器需直接 ssh NAS 跑 compose。`paths-ignore` 移除已不存在的 `deploy.yml`
## 给后续 Claude 会话:CI 故障排查路径
如果 Gitea Actions build 又炸,按 NAS 域问题 vs 仓库代码问题分别排查:
| 现象 | 大概率位置 | 文档 |
|---|---|---|
| `Login to Gitea Container Registry` 步骤报 `x509: certificate signed by unknown authority` | NAS 网络层(iptables / dnsmasq / DSM nginx 占 443 | obsidian vault `NAS/notes/内网证书路径.md` + `NAS/notes/IPv6 设计.md` |
| `Build and push` 步骤里 `RUN ...` 在第二条之内就炸 `unsafe procfs detected` 之类 | buildkit/runc 与 DSM 内核版本 | `.gitea/workflows/build-image.yml``driver-opts` |
| `Failed to pass unit testing` / `Failed to pass lint checking`build.sh 报) | **先看 Dockerfile 顶部 `ARG`**,多半是 CI 跳过开关没传(如 `BUILD_PIPELINE` / `CHECK_3RD_API` / `SKIP_TESTS`)。**不要先去改测试代码** | `Dockerfile` 顶部 ARG + `.gitea/workflows/build-image.yml``build-args` |
| `actions/checkout` 报 fetch 失败 | Gitea SSH/HTTPS 路径或 token 权限 | gitea-runner 的 `GITEA_RUNNER_REGISTRATION_TOKEN` + NPM `git.zhengchentao.win` 的 Advanced 配置 |
| Dockerfile 里某条指令业务逻辑报错 | 真正的代码问题 | 本仓库 `Dockerfile` |
**通用排查原则**build.sh 报的"测试失败 / lint 失败"先看是不是上游已经设计了 CI 跳过路径。Dockerfile 的 `ARG` + `build.sh` 内的 `os.Getenv()` 检查通常成对出现(如 `BUILD_PIPELINE=1` → 跳过 3rd API 测试,`SKIP_TESTS=...` → 跳过指定测试名)。对齐上游 `.github/actions/` 下的传参,绝大多数情况能直接对齐。
-115
View File
@@ -1,115 +0,0 @@
# 部署说明
## 镜像地址
```
ghcr.io/zhengchentao/ezbookkeeping:latest
```
每次向 `myrequirement` 分支推送代码,GitHub Actions 自动构建并推送新镜像。
---
## 首次迁移(从官方镜像换成自定义镜像)
### 1. 备份容器内配置文件到宿主机
```bash
sudo docker cp ezbookkeeping:/ezbookkeeping/conf/ezbookkeeping.ini /opt/ezbookkeeping/ezbookkeeping.ini
```
> 这样之后删容器也不会丢配置。
### 2. 停止并删除旧容器
```bash
docker stop ezbookkeeping && docker rm ezbookkeeping
```
> 只删容器本身,数据目录不受影响。
### 3. 登录 GitHub Container Registry(只需一次)
在 GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) 生成 token,勾选 `read:packages`,然后:
```bash
echo 你的TOKEN | docker login ghcr.io -u zhengchentao --password-stdin
```
### 4. 拉取新镜像
```bash
docker pull ghcr.io/zhengchentao/ezbookkeeping:latest
```
### 5. 启动容器
```bash
docker run -d \
--name ezbookkeeping \
--restart unless-stopped \
-p 8080:8080 \
-v /opt/ezbookkeeping/data:/ezbookkeeping/data \
-v /opt/ezbookkeeping/ezbookkeeping.ini:/ezbookkeeping/conf/ezbookkeeping.ini \
-e EBK_MCP_ENABLE_MCP=true \
-e EBK_SECURITY_ENABLE_API_TOKEN=true \
ghcr.io/zhengchentao/ezbookkeeping:latest
```
**参数说明:**
| 参数 | 含义 |
|------|------|
| `-d` | 后台运行 |
| `--restart unless-stopped` | 服务器重启后自动启动 |
| `-p 8080:8080` | 端口映射 |
| `-v .../data:...` | 挂载数据目录(数据库、图片等) |
| `-v .../ezbookkeeping.ini:...` | 挂载配置文件 |
| `-e EBK_*` | 环境变量覆盖配置 |
### 6. 确认运行正常
```bash
docker ps # 确认容器在运行
docker logs ezbookkeeping # 查看启动日志,确认无报错
```
---
## 后续更新(代码有改动时)
```bash
# 拉取最新镜像
docker pull ghcr.io/zhengchentao/ezbookkeeping:latest
# 停止并删除旧容器
docker stop ezbookkeeping && docker rm ezbookkeeping
# 重新启动(与首次启动命令相同)
docker run -d \
--name ezbookkeeping \
--restart unless-stopped \
-p 8080:8080 \
-v /opt/ezbookkeeping/data:/ezbookkeeping/data \
-v /opt/ezbookkeeping/ezbookkeeping.ini:/ezbookkeeping/conf/ezbookkeeping.ini \
-e EBK_MCP_ENABLE_MCP=true \
-e EBK_SECURITY_ENABLE_API_TOKEN=true \
ghcr.io/zhengchentao/ezbookkeeping:latest
```
---
## 常用运维命令
```bash
# 查看运行中的容器
docker ps
# 查看容器实时日志(Ctrl+C 退出)
docker logs -f ezbookkeeping
# 进入容器内部排查问题
docker exec -it ezbookkeeping sh
# 查看磁盘占用
docker system df
```
+33 -15
View File
@@ -1,6 +1,11 @@
# ezBookkeeping 个人需求清单 # ezBookkeeping 个人 fork 改动清单
> 基于 fork 版本的定制开发需求,持续更新 > 本文件记录这个 fork 相对上游 [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) 的所有定制改动 + 进度状态
> 关联文档:
> - [`CLAUDE.md`](CLAUDE.md) —— 仓库分支模型 / 上游同步流程 / CI 排查路径(meta 层)
> - 部署:见自家 NAS infra repo `git.zhengchentao.win/dev/nas-infra` 的 READMEcompose-level
>
> 标注:❌ 难/暂缓 | ❓ 待定 | 🔍 调查中 | 🟢 已完成 > 标注:❌ 难/暂缓 | ❓ 待定 | 🔍 调查中 | 🟢 已完成
--- ---
@@ -79,7 +84,8 @@
1 2 3 + 1 2 3 +
C 0 . OK C 0 . OK
``` ```
- ⌫ 单击退格,长按清除;C 清除全部 - ⌫ 单击退格;按住不放先删一位、约 500ms 后清空全部(长按响应细节见 #11 第二阶段)
- C 一键清除全部
- 涉及文件:`src/components/mobile/NumberPadSheet.vue` - 涉及文件:`src/components/mobile/NumberPadSheet.vue`
--- ---
@@ -98,14 +104,12 @@
## 五、交易时间选择 ## 五、交易时间选择
### 8. 🟢 点击交易时间标题默认打开日期选择(仅移动端 ### 8. 点击交易时间标题默认打开日期选择(已回滚
**描述:** 在移动端记账/编辑页面点击「Transaction Time」标题行时默认弹日期选择器而非时间选择器 **描述:** 原想让点击「Transaction Time」标题行时默认弹日期选择器。
**已完成:** **为何回滚:** 改动改的是 `template #header` 那行 label 的点击 handler`'time'``'date'`),实际操作中用户点的是 `template #title` 里的日期/时间文本。上游早在 commit `368322f9` 已实现"点哪走哪"的智能路由——点日期开日期选择器、点时间开时间选择器。所以这条改动**用户视角无可见差异**,纯空改,回滚到上游行为。
- 点击标题行(`transaction-edit-datetime-header`)改为以 `'date'` 模式打开
- 点击日期部分 → 日期选择器;点击时间部分 → 时间选择器(保持不变) **留档教训:** 改 UI 行为前先把"用户实际点哪个元素"摸清楚,别只看着 DOM 结构想当然。`#header` slot 只是上方的 label 行,正常用户极少触发。
- PC 端使用统一的 `date-time-select` 组件,无此分离交互,无需修改
- 涉及文件:`src/views/mobile/transactions/EditPage.vue`
--- ---
@@ -133,10 +137,24 @@
- Tab 切换动画保持原样(设置中已有开关可控制) - Tab 切换动画保持原样(设置中已有开关可控制)
- 涉及文件:`src/styles/mobile/global.scss` - 涉及文件:`src/styles/mobile/global.scss`
### 11. 🔍 点击响应卡顿(暂调查 ### 11. 🟢 小键盘点击卡顿(三次修正
**描述:** 移动端点击按钮有延迟感,点不上的问题 **描述:** 移动端小键盘点击有延迟感
**初步判断:** 可能是接口响应慢导致,非前端交互延迟。等 #12 离线缓存方向明确后再评估 **第一阶段(2026-05-02`touch-action: none` 引发的 300ms 双击延迟:** 上游在 `.numpad-button` 上设了 `touch-action: none`commit `e178a079` "code refactor" by MaysWind),与浏览器双击缩放检测叠加后保留了老式 300ms 点击延迟
- 修复:`.numpad-button``touch-action: none` 改为 `touch-action: manipulation`W3C 标准"快速点击"值,禁双击缩放)
**第二阶段(2026-05-08)退格键 `@taphold` 等待 750ms** backspace 单点仍可感知延迟。根因是 `@click` + `@taphold` 让 F7 必须等 ~750ms 判别 tap vs hold,期间 click 被抑制。
- 修复:弃用 `@click="backspace" @taphold="clear()"`,改为原生 `pointerdown`/`pointerup`/`pointercancel`/`pointerleave` + 自管定时器
- 行为:单击立即删一位;按住不放先删一位、约 500ms 后清空全部
**第三阶段(2026-05-08)所有数字/运算键也延迟:** 第一阶段修完后用户反馈数字键仍有"等一拍"感。怀疑 F7 整套 tap 处理(含 active-state 检测、`fastClicks` 兼容代码、tap-hold 全局监听)即便不显式声明 `@taphold` 也会给 `@click` 加上判别期。
- 修复:把所有按键(数字 0-9、运算 ×−+、C 清空、小数点/双零、OK 确认)的 `@click` 全部换成 `@pointerdown.left`
- 原理:`pointerdown` 在按下瞬间触发,绕开 F7 的 tap 合成路径。`.left` 修饰符限制只响应主键(触屏 button=0 始终满足,桌面右键不会误触发)
- F7 的 `.active-state` 视觉反馈基于独立的 touchstart/touchend 监听,不依赖 `@click`,按下视觉效果保留
涉及文件:`src/components/mobile/NumberPadSheet.vue`
**附带认知:** 原 #11 假设是"全局点击响应慢"或"接口慢",与 #12 离线缓存挂钩调研。实际诊断后跟那两条都无关——纯 F7 框架 tap 合成 + 双击缩放 + taphold 检测三者叠加。最终通过完全弃用 `@click` 改 pointer 事件解决。该认知值得记录避免后续误诊路径。
--- ---
@@ -165,8 +183,8 @@
| 5 | 记住上次账户 | ❓ 待定 | | 5 | 记住上次账户 | ❓ 待定 |
| 6 | 小键盘布局 | 🟢 已完成 | | 6 | 小键盘布局 | 🟢 已完成 |
| 7 | 详情编辑/删除 | 🟢 已完成 | | 7 | 详情编辑/删除 | 🟢 已完成 |
| 8 | 点击时间默认日期 | 🟢完成 | | 8 | 点击时间默认日期 | 回滚(无效改动) |
| 9 | 分类默认展开 | 🟢 已完成 | | 9 | 分类默认展开 | 🟢 已完成 |
| 10 | 全局动画加速 | 🟢 已完成 | | 10 | 全局动画加速 | 🟢 已完成 |
| 11 | 点击卡顿优化 | 🔍 暂调查 | | 11 | 小键盘点击卡顿touch-action 修复) | 🟢 已完成 |
| 12 | 离线缓存 | ❌ 暂缓 | | 12 | 离线缓存 | ❌ 暂缓 |
+1
View File
@@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2020-2026 MaysWind (i@mayswind.net) Copyright (c) 2020-2026 MaysWind (i@mayswind.net)
Copyright (c) 2026 Zhengchen Tao (fork modifications)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+25
View File
@@ -1,4 +1,29 @@
# ezBookkeeping # ezBookkeeping
> ## Personal fork notice
>
> This repository is a personal fork of [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) (MIT) with the following custom additions on top of upstream releases:
>
> - **Credit card accounts**: credit-limit field; account list shows available credit
> - **Account-filtered transactions**: when filtering by a single account, show an account info card on top (icon / name / balance / available credit)
> - **Account editing**: edit the balance field directly; a "balance adjustment" transaction is generated automatically
> - **Add-transaction page**: live display of balance or available credit after selecting an account
> - **Numpad**: custom layout (4-column calculator style) + `touch-action` fix for tap latency
> - **Mobile animations**: generic transitions 300ms → 150ms
> - **Transaction detail**: edit / delete entries added to the mobile three-dot menu
> - **Category picker**: optional "expand all by default" on mobile (cloud-sync allowlisted)
>
> Full list with implementation details: [`FORK.md`](FORK.md)
> Branch model / upstream sync / CI troubleshooting: [`CLAUDE.md`](CLAUDE.md)
>
> All modifications are released under the same MIT License as upstream — see [`LICENSE`](LICENSE).
>
> ---
>
> Upstream README content follows below.
---
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
[![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping) [![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
[![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases) [![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases)
+52 -19
View File
@@ -8,7 +8,11 @@
</div> </div>
<div class="numpad-values"> <div class="numpad-values">
<span id="numpad-value" class="numpad-value" :class="currentDisplayNumClass" @click="onDisplayValueClick">{{ currentDisplay }}</span> <span id="numpad-value" class="numpad-value" :class="currentDisplayNumClass" @click="onDisplayValueClick">{{ currentDisplay }}</span>
<f7-button class="numpad-backspace-button" @click="backspace" @taphold="clear()"> <f7-button class="numpad-backspace-button"
@pointerdown="onBackspacePointerDown"
@pointerup="onBackspacePointerEnd"
@pointercancel="onBackspacePointerEnd"
@pointerleave="onBackspacePointerEnd">
<f7-icon class="icon-with-direction" f7="delete_left"></f7-icon> <f7-icon class="icon-with-direction" f7="delete_left"></f7-icon>
</f7-button> </f7-button>
</div> </div>
@@ -21,55 +25,55 @@
</f7-popover> </f7-popover>
<div class="numpad-buttons"> <div class="numpad-buttons">
<f7-button class="numpad-button numpad-button-num" @click="inputNum(7)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(7)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[7] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[7] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(8)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(8)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[8] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[8] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(9)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(9)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[9] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[9] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('×')"> <f7-button class="numpad-button numpad-button-function no-right-border" @pointerdown.left="setSymbol('×')">
<span class="numpad-button-text numpad-button-text-normal">&times;</span> <span class="numpad-button-text numpad-button-text-normal">&times;</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(4)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(4)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[4] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[4] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(5)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(5)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[5] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[5] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(6)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(6)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[6] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[6] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('')"> <f7-button class="numpad-button numpad-button-function no-right-border" @pointerdown.left="setSymbol('')">
<span class="numpad-button-text numpad-button-text-normal">&minus;</span> <span class="numpad-button-text numpad-button-text-normal">&minus;</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(1)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(1)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[1] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[1] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(2)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(2)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[2] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[2] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(3)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(3)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[3] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[3] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('+')"> <f7-button class="numpad-button numpad-button-function no-right-border" @pointerdown.left="setSymbol('+')">
<span class="numpad-button-text numpad-button-text-normal">&plus;</span> <span class="numpad-button-text numpad-button-text-normal">&plus;</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="clear()"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="clear()">
<span class="numpad-button-text numpad-button-text-normal">C</span> <span class="numpad-button-text numpad-button-text-normal">C</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(0)"> <f7-button class="numpad-button numpad-button-num" @pointerdown.left="inputNum(0)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[0] }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ digits[0] }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" v-if="supportDecimalSeparator" @click="inputDecimalSeparator()"> <f7-button class="numpad-button numpad-button-num" v-if="supportDecimalSeparator" @pointerdown.left="inputDecimalSeparator()">
<span class="numpad-button-text numpad-button-text-normal">{{ decimalSeparator }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ decimalSeparator }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-num" v-if="!supportDecimalSeparator" @click="inputDoubleNum(0)"> <f7-button class="numpad-button numpad-button-num" v-if="!supportDecimalSeparator" @pointerdown.left="inputDoubleNum(0)">
<span class="numpad-button-text numpad-button-text-normal">{{ `${digits[0]}${digits[0]}` }}</span> <span class="numpad-button-text numpad-button-text-normal">{{ `${digits[0]}${digits[0]}` }}</span>
</f7-button> </f7-button>
<f7-button class="numpad-button numpad-button-confirm no-right-border no-bottom-border" fill @click="confirm()"> <f7-button class="numpad-button numpad-button-confirm no-right-border no-bottom-border" fill @pointerdown.left="confirm()">
<span :class="{ 'numpad-button-text': true, 'numpad-button-text-confirm': !currentSymbol }">{{ confirmText }}</span> <span :class="{ 'numpad-button-text': true, 'numpad-button-text-confirm': !currentSymbol }">{{ confirmText }}</span>
</f7-button> </f7-button>
</div> </div>
@@ -326,6 +330,31 @@ function clear(): void {
currentSymbol.value = ''; currentSymbol.value = '';
} }
const BACKSPACE_HOLD_TO_CLEAR_MS = 500;
let backspaceClearTimer: ReturnType<typeof setTimeout> | null = null;
function onBackspacePointerDown(event: PointerEvent): void {
// 按下立刻删一位(消除 F7 taphold 判别期带来的点击延迟)
if (event.button !== undefined && event.button !== 0) {
return;
}
backspace();
if (backspaceClearTimer !== null) {
clearTimeout(backspaceClearTimer);
}
backspaceClearTimer = setTimeout(() => {
clear();
backspaceClearTimer = null;
}, BACKSPACE_HOLD_TO_CLEAR_MS);
}
function onBackspacePointerEnd(): void {
if (backspaceClearTimer !== null) {
clearTimeout(backspaceClearTimer);
backspaceClearTimer = null;
}
}
function paste(): void { function paste(): void {
showPastePopover.value = false; showPastePopover.value = false;
@@ -442,6 +471,7 @@ function onSheetOpen(): void {
} }
function onSheetClosed(): void { function onSheetClosed(): void {
onBackspacePointerEnd();
close(); close();
} }
@@ -531,7 +561,10 @@ watch(() => props.flipNegative, (newValue) => {
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
user-select: none; user-select: none;
touch-action: none; /* 上游设的 touch-action: none 在 F7 tap 处理下让 click 慢一拍(小键盘卡顿
的实际根因,不是网络/渲染)。改 manipulation:禁双击缩放 + 消除 300ms
老延迟,但保留 click 正常合成。详见 FORK.md #11 */
touch-action: manipulation;
} }
.numpad-button-num { .numpad-button-num {
+1 -1
View File
@@ -248,7 +248,7 @@
v-if="pageTypeAndMode?.type === TransactionEditPageType.Transaction" v-if="pageTypeAndMode?.type === TransactionEditPageType.Transaction"
> >
<template #header> <template #header>
<div class="transaction-edit-datetime-header" @click="showDateTimeDialog('date')">{{ tt('Transaction Time') }}</div> <div class="transaction-edit-datetime-header" @click="showDateTimeDialog('time')">{{ tt('Transaction Time') }}</div>
</template> </template>
<template #title> <template #title>
<div class="transaction-edit-datetime-title"> <div class="transaction-edit-datetime-title">