DriftLedger/docs/design/ui-redesign-p6-plan.md
fengmengqi bf04400852 feat: OCR 模型按需下载 + 三层独立开关 + 日志持久化 + 原生浮层主题同步 + 文档重构
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
2026-07-23 19:03:38 +08:00

20 KiB
Raw Blame History

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 → bridge setFloatingUiConfig() → 原生存 SharedPreferencesJSON→ 三个浮层组件读取。原生保留现有硬编码值作为默认值兜底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 accentlight #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" 六位数 hextheme tokens 里有 rgba 的字段不要直接下发——accent/cardBg/fg/border 等都是纯色,确认无 rgbaborder 深色主题是 rgba(255,255,255,0.08),下发前换算为 hex深色用 #14202429? 不许发明值——正确做法:原生 Color.parseColor 支持 #AARRGGBBJS 侧把 rgba(255,255,255,0.08) 换算为 #14FFFFFFalpha 0.08≈0x14。写一个 rgbaToHex 小工具处理这种情况,纯色 hex 原样通过)。
  • 原生端用 Color.parseColor 解析,异常时回退默认值。

原生持久化

  • SharedPreferences 文件名:floating_ui_config,单键 config_json 存整个 JSON。
  • 新增 FloatingUiConfigStore.ktobjectsave(context, ReadableMap) / load(context): FloatingUiConfigdata class字段缺省值=现状硬编码值,保证旧 JS 行为不变)。
  • FloatingUiConfig data 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.tssrc/i18n/en.tssrc/services/accessibilityBridge.tssrc/app/_layout.tsxsrc/services/automationPipeline.ts

  • Create: src/services/floatingUiConfig.tstests/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.tsNativeAccessibilityBridgesetFloatingUiConfig(config: FloatingUiConfig): Promise<boolean>;,文件头注释方法清单补一行。showFloatingBill 签名加 currency: stringdirection 之后、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:298Alert.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)'#66111318alpha 四舍五入 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.ktplugins/accessibility/android/FloatingTip.ktplugins/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 FloatingUiConfigStoreprivate const val PREFS = "floating_ui_config"fun save(context: Context, map: ReadableMap)遍历契约字段缺失字段跳过不覆盖JSONObject 组装后写入);fun load(context: Context): FloatingUiConfigJSONObject 读取,缺失/解析失败逐字段回退默认;optString)。颜色字段提供 fun parseColorOr(value: String, fallback: Int): Int 工具(Color.parseColor try/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()),文字色 → accentFgProgressBar progressTintList 设 accentFg。构造签名加可选 config: FloatingUiConfig? = nullnull 时内部 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.ktplugins/accessibility/android/AccessibilityBridgeModule.ktshowFloatingBill 加 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、圆角 12dpconfirm=accent 底 accentFg 字 boldopenApp/dismiss=inputBg 底 fgPrimary 字

  • 所有颜色经 FloatingUiConfigStore.load(context) + parseColorOr 兜底;所有文案经 labels

  • Step 1: 构造签名 + showFloatingBill 参数

FloatingBillView 构造加 initialCurrency: String = "CNY"AccessibilityBridgeModule.showFloatingBillcurrency: String 参数direction 后 draftId 前)并透传。

  • Step 2: 币种 chip

金额行:水平布局,左侧币种 chipinputBg+borderfgPrimary 字,显示 currentCurrency右侧金额输入框weight 1。点击 chip 在 listOf("CNY","USD","HKD","JPY","EUR","GBP") 循环initialCurrency 不在列表则临时插到首位。sendSaveEvent/sendOpenAppEvent 的 map 加 putString("currency", currentCurrency)

  • Step 3: 金额校验改正则 + BigDecimal

afterTextChangedRegex("^\\d+(\\.\\d{1,2})?$") 匹配且 BigDecimal(text) > BigDecimal.ZERO 才启用保存BigDecimal 构造 try/catch。禁用时按钮底色 inputBg + fgSecondary 字。

  • Step 4: 全面替换颜色与文案 + 自动消失 30s

逐段按上面视觉规格重写 show() 与 rebuildChips()(结构不变,只换色值来源、文案来源、尺寸);15000L30000L。标签按方向切换的文案(交易分类/收入分类/转入账户、资金来源/存入账户/转出账户)全部走 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.tssrc/app/_layout.tsx

  • Step 1: billingConfirmed 处理器用 res.currency

src/services/automationPipeline.ts:50-110 的 handleBillingConfirmedconst 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

  1. 设置里切换 浅/深主题 → 触发一次浮窗(可用 automation 页调试入口或真实账单)→ 浮窗颜色跟随主题
  2. 切换英文 → 浮窗/悬浮球/提示文案全英文
  3. 拖动悬浮球 → 杀进程重启无障碍服务 → 位置保持
  4. 浮窗点币种 chip 切换 USD → 确认入账 → 交易币种为 USD
  5. 金额输入 1e10 / 0 / 0.005 → 保存按钮禁用;12.50 → 可用
  6. 浮窗 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计划要求实色 菜单背景直接使用 colorCardBgfull 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 保留为语义色(扫描红光),不受主题控制。