OCR 模型按需下载(P8 瘦身): - 移除 plugins/ppocr/assets/ 内置模型(det 9.8MB + rec 21MB + dict),APK 减包 ~31MB - 新增 modelDownloader.ts:优先从 HuggingFace/CDN 下载,兜底从 APK assets 拷贝 - OcrModule.kt 新增 setModelDir,支持从 filesystem 加载模型,回退 assets 兼容旧用户 - settingsStore 持久化 ocrModelVersion / ocrModelDir - 自动化页集成模型状态检查与一键下载 UI OCR 三层独立控制: - OcrProcessorConfig 从单一 aiVisionEnabled 拆分为 layer1/2/3 三个独立开关 - 设置页可按层启停(L1 正则规则 / L2 本地 OCR / L3 AI Vision) - OCR 处理增加耗时与字符数日志 日志系统升级: - logger.ts 新增 LogFileBackend 抽象,支持磁盘持久化(按日期 app-YYYY-MM-DD.log) - 新增 logBackend.ts(ExpoLogFileBackend)+ 日志中心页 settings/logs.tsx - 日志中心:实时缓冲 + 历史文件、4 级过滤、Tag/关键词搜索、JSON 展开、分享导出、7 天过期清理 - _layout.tsx 启动时初始化文件后端 + 日志脱敏(验证码/卡号) 原生浮层 UI 主题同步(P6): - 新增 floatingUiConfig.ts:JS 侧从 theme tokens + i18n 构建 FloatingUiConfig 推送原生 - 新增 FloatingUiConfigStore.kt:SharedPreferences 存储,三浮层组件读取 - FloatingBillView 重设计:颜色/文案走配置、新增币种 chip、金额校验改 BigDecimal - FloatingHelper / FloatingTip 同步适配 - _layout.tsx 新增 FloatingUiConfigSyncer,主题/语言切换自动推送 UI 与组件增强: - FormModal 新增 select/dropdown 控件、行内布局(row/flex)、联动回调 onValuesChange - 新增 Touchable 通用触摸组件、AccountCreateModal 快速建账弹窗 - 信用卡页展示账单周期/到期还款日/本期应还/剩余可用额度,关联账户改下拉选择 - AI 设置页重做:OpenAI/Gemini/DeepSeek 预设 + 默认 URL/模型 - 引导页新增 Android 权限检查步骤(无障碍/通知/短信/存储/悬浮窗) 去重优化: - 对手方匹配改为模糊包含(includes),双方均无对手方时判定低置信度重复 - DedupResult 新增 matchedItem 返回匹配对比项 文档重构: - README.md 重写为入口索引(品牌更新 + 模块概览 + 文档导航表) - 新增 AGENTS.md(AI 助手贡献指南)、docs/architecture.md(Mermaid 数据流/分层/OCR 级联图) - 新增 docs/development.md(环境/命令/编码规范/测试/提交规范)、plugins/README.md - UI 重设计文档(design spec + p1-p8)移入 docs/design/ 其他: - i18n 新增权限/信用卡详情/日志中心/AI 设置等翻译键 - ppocr Config Plugin 修复 import 注入去重;size-optimization 增强 - 新增测试:logger.test.ts、floating-ui-config.test.ts
20 KiB
UI 重设计 P6:原生浮层(悬浮球/悬浮窗)重设计 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax. 不执行任何 git add/commit(用户要求,改动留工作区)。不创建额外任务清单。
Goal: 原生浮层(FloatingHelper 悬浮球 / FloatingBillView 浮窗 / FloatingTip 提示)视觉与文案对齐新设计系统:颜色与文案由 JS 层(theme tokens + i18n)通过 bridge 下发,原生不再硬编码中文与旧靛蓝;悬浮球位置持久化;浮窗补币种字段;前台账单 Alert i18n 化并去 emoji。
背景决策(已确认):
- 原生 View 无法直接用 RN theme,采用
FloatingUiConfig下发模式:JS 侧从当前 theme + i18n 构建 config → bridgesetFloatingUiConfig()→ 原生存 SharedPreferences(JSON)→ 三个浮层组件读取。原生保留现有硬编码值作为默认值兜底(JS 未推送时行为不变)。 - 浮窗卡片跟随 App 主题(浅色=白卡近黑 accent,深色=OLED 黑卡白 accent),卡片用实色(不再半透明磨砂),保证在其他 App 上的可读性。
- 浮窗金额校验从
toDoubleOrNull改正则 + BigDecimal;自动消失 15s → 30s。 - 改动范围:
plugins/accessibility/(Kotlin)+src/services/(bridge/config)+src/app/_layout.tsx+src/services/automationPipeline.ts+src/i18n/。不碰 domain 写路径。
FloatingUiConfig 契约(T1-T5 共同遵守,先读这里)
JS → 原生:setFloatingUiConfig(config: ReadableMap): Promise<boolean>
interface FloatingUiConfig {
colors: {
accent: string; // 按钮/选中态背景(= theme accent:light #111318 / dark #F3F4F6)
accentFg: string; // accent 上的文字色(= fgInverse)
cardBg: string; // 卡片底色(= bgSecondary)
inputBg: string; // 输入框/未选中 chip 底色(= bgTertiary)
fgPrimary: string;
fgSecondary: string;
border: string;
income: string; // financial.income
expense: string; // financial.expense
transfer: string; // financial.transfer
};
labels: {
billTitle: string; // 浮窗标题
dirExpense: string; dirIncome: string; dirTransfer: string;
amountLabel: string; payeeLabel: string;
narrationLabel: string; narrationHint: string;
categoryExpense: string; // 「交易分类」
categoryIncome: string; // 「收入分类」
transferTarget: string; // 「转入账户」
accountExpense: string; // 「资金来源」
accountIncome: string; // 「存入账户」
accountTransfer: string; // 「转出账户」
openApp: string; dismiss: string; confirm: string;
ballOcr: string; // 悬浮球「识别账单」
ballRemember: string; // 「记住此页」
rememberSuccess: string; // 记住页面成功提示(不含 emoji)
rememberFail: string; // 失败提示前缀(原生拼接 ': ' + e.message)
pageRemembered: string; // BillingAccessibilityService Toast「已记住页面签名」前缀
pageSignatureExists: string; // 「该页面签名已存在」前缀
};
}
- 颜色一律
"#RRGGBB"六位数 hex(theme tokens 里有 rgba 的字段不要直接下发——accent/cardBg/fg/border 等都是纯色,确认无 rgba;border 深色主题是rgba(255,255,255,0.08),下发前换算为 hex:深色用#14202429? 不许发明值——正确做法:原生Color.parseColor支持#AARRGGBB,JS 侧把 rgba(255,255,255,0.08) 换算为#14FFFFFF(alpha 0.08≈0x14)。写一个rgbaToHex小工具处理这种情况,纯色 hex 原样通过)。 - 原生端用
Color.parseColor解析,异常时回退默认值。
原生持久化
- SharedPreferences 文件名:
floating_ui_config,单键config_json存整个 JSON。 - 新增
FloatingUiConfigStore.kt(object):save(context, ReadableMap)/load(context): FloatingUiConfig(data class,字段缺省值=现状硬编码值,保证旧 JS 行为不变)。 FloatingUiConfigdata class 字段名与上面契约一致(camelCase)。
事件契约变更(T3+T5)
showFloatingBill增加参数currency: String(放在direction之后、draftId之前)。billingConfirmed/billingOpenApp事件 map 增加putString("currency", currentCurrency)。- 浮窗金额行左侧显示当前币种 chip,点击在
listOf("CNY","USD","HKD","JPY","EUR","GBP")中循环切换;初始值 = 传入 currency(不在列表则插入到首位)。
Task 1: JS 侧 — i18n 键 + floatingUiConfig 服务 + bridge 接口 + 推送接线 + 前台 Alert i18n
Files:
-
Modify:
src/i18n/zh.ts、src/i18n/en.ts、src/services/accessibilityBridge.ts、src/app/_layout.tsx、src/services/automationPipeline.ts -
Create:
src/services/floatingUiConfig.ts、tests/floating-ui-config.test.ts -
Step 1: i18n 新增
floating.*键组(zh/en 对等,%{name}插值语法)
floating.billTitle 调整交易草稿 / Adjust Draft
floating.dirExpense 支出 / Expense (复用现有方向键也行,但浮层独立一组避免耦合)
floating.dirIncome 收入 / Income
floating.dirTransfer 转账 / Transfer
floating.amountLabel 金额 / Amount
floating.payeeLabel 交易对手 / Payee
floating.narrationLabel 描述/备注 / Narration
floating.narrationHint 输入交易叙述 / Enter narration
floating.categoryExpense 交易分类 / Category
floating.categoryIncome 收入分类 / Income Category
floating.transferTarget 转入账户 / To Account
floating.accountExpense 资金来源 / From Account
floating.accountIncome 存入账户 / To Account
floating.accountTransfer 转出账户 / From Account
floating.openApp 打开应用 / Open App
floating.dismiss 忽略 / Dismiss
floating.confirm 确认入账 / Confirm
floating.ballOcr 识别账单 / Scan Bill
floating.ballRemember 记住此页 / Remember Page
floating.rememberSuccess 已将当前页面加入识别白名单 / Page added to recognition whitelist
floating.rememberFail 记录失败 / Failed to save
floating.pageRemembered 已记住页面签名 / Page signature saved
floating.pageSignatureExists 该页面签名已存在 / Page signature already exists
(若现有键已有同文案可复用,但键组保持独立 floating.*;先 grep 避免重复键名。)
同时给前台 Alert 加键(放 automation.* 下,先读现有 automation 节):
automation.billDetectedTitle 识别到新账单 / New Bill Detected (无 emoji)
automation.billDetectedReject 拒绝/丢弃 / Discard
automation.billDetectedEdit 修改并入账 / Edit & Save
automation.billDetectedConfirm 确认入账 / Confirm
Alert 的多行消息体(日期/商户/金额/分类/账户/叙述)逐行用既有标签键拼,不为每行加新键(行标签可复用表单/详情现有键,先 grep transaction.payee 之类;没有合适的就在 automation 节加 billDetectedLine* 键)。
- Step 2:
src/services/floatingUiConfig.ts
export interface FloatingUiConfig { colors: {...}; labels: {...} } // 按契约
/** rgba(r,g,b,a) → #AARRGGBB;#RRGGBB 原样返回。 */
export function colorToHex(color: string): string;
/** 从 theme tokens + t() 构建完整 config。 */
export function buildFloatingUiConfig(theme: ThemeTokens, t: TranslateFn): FloatingUiConfig;
/** 构建并推送到原生(bridge 不可用时静默返回 false)。 */
export async function pushFloatingUiConfig(theme: ThemeTokens, t: TranslateFn): Promise<boolean>;
- Step 3: bridge 接口扩展
accessibilityBridge.ts:NativeAccessibilityBridge 加 setFloatingUiConfig(config: FloatingUiConfig): Promise<boolean>;,文件头注释方法清单补一行。showFloatingBill 签名加 currency: string(direction 之后、draftId 之前)。
-
Step 4: _layout.tsx 推送接线
-
启动时(setFloatingBallEnabled 同一处,
src/app/_layout.tsx:139附近)也pushFloatingUiConfig(...)。注意此处拿不到 useTheme 的 theme(在 ThemeProvider 外层?先读 _layout 结构确认)——若拿不到,启动这次推送可移到下一步的组件里统一做,避免重复。 -
在 ThemeProvider 内部挂一个小组件(如
FloatingUiConfigSyncer,可直接写在 _layout.tsx 里):const { theme } = useTheme(); const t = useT(); useEffect(() => { pushFloatingUiConfig(theme, t); }, [theme, t])—— 主题切换/语言切换/启动都会重推。t 引用随 locale 变化(确认 useT 返回的 t 在语言切换时引用变化,先读 src/i18n 实现;若 t 引用稳定,则依赖里加 locale)。 -
Step 5: automationPipeline.ts 前台 Alert i18n + 去 emoji
src/services/automationPipeline.ts:298 的 Alert.alert('🌟 识别到新账单', ...):标题/按钮全部改 t();消息体保留原信息结构。该文件是 service 层非组件——确认文件里如何拿 t(若没有,用 i18n.t(...) 直接调,读 src/i18n/index.ts 导出了什么)。showFloatingBill 调用处(:262)传第 7 个参数 currency(用 :225 已提取的 currency 变量)。
- Step 6: 测试 + 验证
tests/floating-ui-config.test.ts:
colorToHex:'#111318'原样;'rgba(255,255,255,0.08)'→#14FFFFFF;'rgba(17,19,24,0.4)'→#66111318(alpha 四舍五入 Math.round(a*255))buildFloatingUiConfig:用 lightTheme + 真实 zh t() 构建,断言 colors.accent === '#111318'、labels.billTitle === '调整交易草稿'、所有契约字段非空(遍历 keys)
Run: npm run typecheck && npm test → 全绿(i18n parity 测试覆盖新键)
Task 2: 原生 config 基础设施(FloatingUiConfigStore + bridge 方法 + FloatingTip/Toast 改读)
Files:
-
Create:
plugins/accessibility/android/FloatingUiConfigStore.kt -
Modify:
plugins/accessibility/android/AccessibilityBridgeModule.kt、plugins/accessibility/android/FloatingTip.kt、plugins/accessibility/android/BillingAccessibilityService.kt -
Step 1: FloatingUiConfigStore.kt
-
data class FloatingUiConfig(...):colors/labels 全部字段,默认值 = 现状硬编码(accent#5E6AD2、cardBg#FF050506? —— 不:默认值应是「现状视觉」,但现状 cardBg 是 0x8C050506 半透明。默认 cardBg 用#F2050506? 简化:默认值就用现状各硬编码颜色换算成#AARRGGBB/#RRGGBB字符串,逐字段列注释对应原 Kotlin 常量)。labels 默认值 = 现状中文硬编码文案(去 emoji)。 -
object FloatingUiConfigStore:private const val PREFS = "floating_ui_config";fun save(context: Context, map: ReadableMap)(遍历契约字段,缺失字段跳过不覆盖,JSONObject 组装后写入);fun load(context: Context): FloatingUiConfig(JSONObject 读取,缺失/解析失败逐字段回退默认;optString)。颜色字段提供fun parseColorOr(value: String, fallback: Int): Int工具(Color.parseColortry/catch)。 -
org.json 可用(Android 内置),无需新依赖。
-
Step 2: AccessibilityBridgeModule.setFloatingUiConfig
@ReactMethod
fun setFloatingUiConfig(config: ReadableMap, promise: Promise) {
try {
FloatingUiConfigStore.save(reactContext, config)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("CONFIG_SAVE_FAIL", e.message)
}
}
-
Step 3: FloatingTip/RepeatToast 改读 config
-
FloatingTip.show():背景色
0xF0333333→ config accent(不透明化:解析后or 0xFF000000.toInt()),文字色 → accentFg,ProgressBarprogressTintList设 accentFg。构造签名加可选config: FloatingUiConfig? = null,null 时内部FloatingUiConfigStore.load(context)。 -
RepeatToast 的
"⚠ $message"前缀 emoji 去掉,只留 message(⚠ 属 emoji 审计范围)。 -
FloatingTip 调用处(FloatingHelper.kt:209/211)改传 config 文案:
labels.rememberSuccess、"${labels.rememberFail}: ${e.message}"(T4 做也可以,此处先改文案读取,FloatingHelper 的彻底重设计在 T4)。 -
Step 4: BillingAccessibilityService 两处 Toast 文案
:387 "已记住页面签名:\n$sig" 与 :419 "该页面签名已存在:\n$sig" → 改读 FloatingUiConfigStore.load(this).labels 的 pageRemembered / pageSignatureExists + ":\n$sig"。
- Step 5: 编译验证
android/ 已生成。把改动的 .kt 手动拷到 android/app/src/main/java/com/beancount/mobile/accessibility/(与 plugins 目录同名文件覆盖),然后:
Run: cd android && ./gradlew :app:compileDebugKotlin -q → BUILD SUCCESSFUL
(若编译环境不可用,记录为设备验证项并在报告中说明。)
Task 3: FloatingBillView 重设计 + 币种
Files:
- Modify:
plugins/accessibility/android/FloatingBillView.kt、plugins/accessibility/android/AccessibilityBridgeModule.kt(showFloatingBill 加 currency 参数透传)
视觉规格(对齐 docs/ui-redesign-design.md §3 Bento):
-
卡片:实色 cardBg、圆角 16dp、1dp border 描边、padding 16dp(原 12/8 加宽)
-
标题:fgPrimary 13sp bold;分段选择器:选中=accent 底 accentFg 字,未选中=透明 fgSecondary 字,圆角 6dp
-
字段标签:fgSecondary 10sp bold;输入框:inputBg 底、圆角 10dp、1dp border、fgPrimary 字、hint fgSecondary
-
chips:圆角 999(用 dp(14f) 近似胶囊)、未选中=inputBg+border 描边+fgSecondary 字、选中:分类行支出/收入=accent;转账方向第一行=transfer 色;账户行按方向=income/expense 色(语义色保留,但改从 config 读)
-
按钮:高 40dp、圆角 12dp;confirm=accent 底 accentFg 字 bold;openApp/dismiss=inputBg 底 fgPrimary 字
-
所有颜色经
FloatingUiConfigStore.load(context)+ parseColorOr 兜底;所有文案经 labels -
Step 1: 构造签名 + showFloatingBill 参数
FloatingBillView 构造加 initialCurrency: String = "CNY";AccessibilityBridgeModule.showFloatingBill 加 currency: String 参数(direction 后 draftId 前)并透传。
- Step 2: 币种 chip
金额行:水平布局,左侧币种 chip(inputBg+border,fgPrimary 字,显示 currentCurrency),右侧金额输入框(weight 1)。点击 chip 在 listOf("CNY","USD","HKD","JPY","EUR","GBP") 循环(initialCurrency 不在列表则临时插到首位)。sendSaveEvent/sendOpenAppEvent 的 map 加 putString("currency", currentCurrency)。
- Step 3: 金额校验改正则 + BigDecimal
afterTextChanged:Regex("^\\d+(\\.\\d{1,2})?$") 匹配且 BigDecimal(text) > BigDecimal.ZERO 才启用保存(BigDecimal 构造 try/catch)。禁用时按钮底色 inputBg + fgSecondary 字。
- Step 4: 全面替换颜色与文案 + 自动消失 30s
逐段按上面视觉规格重写 show() 与 rebuildChips()(结构不变,只换色值来源、文案来源、尺寸);15000L → 30000L。标签按方向切换的文案(交易分类/收入分类/转入账户、资金来源/存入账户/转出账户)全部走 labels。
- Step 5: 编译验证(同 T2 Step 5 流程)
Task 4: FloatingHelper 重设计 + 位置持久化
Files:
-
Modify:
plugins/accessibility/android/FloatingHelper.kt -
Step 1: 颜色与文案走 config
-
指示竖线:accent(保持 70% 透明度:解析后 alpha 设为 0xB0)
-
菜单:实色 cardBg、圆角 12dp、1dp border
-
「识别账单」按钮:accent 底 accentFg 字(主操作);「记住此页」:inputBg 底 fgPrimary 字;按压态透明度变化保留(按下时 alpha 0.8)
-
ScanIconDrawable/PinIconDrawable 颜色:Scan 用 accentFg(在 accent 底按钮上),Pin 用 fgSecondary
-
文案 labels.ballOcr / ballRemember / rememberSuccess / rememberFail(带 ': ' + message 拼接)
-
Step 2: 位置持久化
-
SharedPreferences 复用
billing_accessibility_prefs:键floating_ball_x/floating_ball_y -
show()初始化:params.x/y = prefs.getInt(...),无键时用现状默认(0/400);拖动抬起(吸附后)写 prefs -
companion 的 lastX/lastY 保留为进程内缓存但初始从 prefs 读(或直接用局部变量+prefs,简化则删 companion——注意多实例语义,BillingAccessibilityService 单例,安全)
-
Step 3: 编译验证(同 T2 Step 5 流程)
Task 5: 币种链路 JS 消费侧
Files:
-
Modify:
src/services/automationPipeline.ts、src/app/_layout.tsx -
Step 1: billingConfirmed 处理器用 res.currency
读 src/services/automationPipeline.ts:50-110 的 handleBillingConfirmed:const currency = pending?.event?.currency || 'CNY'(:78)→ 优先 res.currency(浮窗用户改过),res.currency || pending?.event?.currency || 'CNY'。确认 res 类型定义处加 currency 字段(NativeOpenAppEvent 之类,grep 类型声明一起改)。
- Step 2: billingOpenApp 处理器
src/app/_layout.tsx:255 const currency = 'CNY' → const currency = res.currency || 'CNY'。
- Step 3: 验证
Run: npm run typecheck && npm test → 全绿
Task 6: P6 审计 + 验收
- Step 1: 审计
grep -rn "0xFF5E6AD2\|0x995E6AD2\|0xB05E6AD2\|0x1F5E6AD2\|0x405E6AD2" plugins/accessibility/android/ # 无输出(旧靛蓝清零,默认值除外——FloatingUiConfigStore 默认值允许保留并注释)
grep -rn '"[^"]*[一-鿿]' plugins/accessibility/android/*.kt | grep -v "FloatingUiConfigStore\|//" # 除 Store 默认值与注释外无硬编码中文 UI 字符串
grep -rn "⚠\|📌\|🌟" src/ plugins/ # 无输出
grep -n "currency" src/services/accessibilityBridge.ts # showFloatingBill 含 currency
- Step 2: 全量测试 + typecheck + Kotlin 编译
Run: npm test → 全绿;npm run typecheck → 无错误;cd android && ./gradlew :app:compileDebugKotlin -q → 成功
- Step 3: 手工走查(需设备)
Run: npm run android
- 设置里切换 浅/深主题 → 触发一次浮窗(可用 automation 页调试入口或真实账单)→ 浮窗颜色跟随主题
- 切换英文 → 浮窗/悬浮球/提示文案全英文
- 拖动悬浮球 → 杀进程重启无障碍服务 → 位置保持
- 浮窗点币种 chip 切换 USD → 确认入账 → 交易币种为 USD
- 金额输入
1e10/0/0.005→ 保存按钮禁用;12.50→ 可用 - 浮窗 30 秒无操作自动消失;触摸后不消失
终审修订(已实施,以实际代码为准)
终审日期:2026-07-22,审计 598 测试全绿、typecheck 干净、Kotlin BUILD SUCCESSFUL。
| # | 级别 | 问题 | 修复 |
|---|---|---|---|
| C1 | Critical | FloatingBillView 标题 setTextColor(colorAccentFg) — 浅色主题下白卡白字、深色主题下黑底黑字,标题不可见 |
改为 colorFgPrimary |
| I1 | Important | 容器边框从 accent 派生 alpha 而非用 config.border | 删除 colorContainerBorder,直接使用 colorBorder |
| I2 | Important | FloatingHelper btnRemember 背景从 fgPrimary 派生,计划要求 inputBg | 加 colorInputBg 解析,btnRemember 改从 inputBg 派生 |
| I3 | Important | 悬浮球菜单背景半透明(65% cardBg),计划要求实色 | 菜单背景直接使用 colorCardBg(full opacity),仅描边保留半透明 |
| M1 | Minor | FloatingTip 背景未强制不透明化(plan §T2 Step 3) | 加 or 0xFF000000.toInt() 保护 |
| M2 | Minor | 金额校验先 BigDecimal 解析再正则,正则先匹配可早期短路 | 调整为先 pattern.matches(text) 再 value != null && value > BigDecimal.ZERO |
偏差说明(无需修复):
- FloatingHelper 菜单背景 / 按钮背景 / 指示线的 alpha 分量组合(
(colorX and 0x00FFFFFF) or alpha)是原生浮层叠加其他 App 所必需的透明度控制,不是颜色硬编码。颜色 RGB 分量完全来自 config。 - FloatingTip 默认 fallback
#FF000000是纯黑(非靛蓝),因为 accent 在极浅色主题下是#111318(近黑),fallback 用黑色是安全的极限退化。 - ScanIconDrawable laserColor
0xFFF87171保留为语义色(扫描红光),不受主题控制。