diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b173ddb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,62 @@ +# Repository Guidelines + +## Project Overview + +DriftLedger (浮记) is an offline-first mobile client for [Beancount](https://beancount.github.io/) double-entry accounting, built with React Native + Expo (CNG), TypeScript, Zustand, and expo-sqlite. The app treats desktop-maintained `.bean` files as the read-only source of truth and appends new entries to `main.bean`. + +## Project Structure & Module Organization + +| Directory | Purpose | +|-----------|---------| +| `src/domain/` | Pure business logic (ledger parser, bill pipeline, dedup, rules, OCR processor). No React/RN imports. | +| `src/app/` | Expo Router file-based routes; `(tabs)/` is bottom navigation. | +| `src/components/` | Reusable React Native UI components and charts. | +| `src/store/` | Zustand stores (ledger, import, settings, metadata, automation). | +| `src/services/` | Platform-facing concerns (sync, backup, security, OCR bridge). | +| `src/storage/` | SQLite read-cache with versioned migrations. | +| `src/theme/` | Token-based theming system. | +| `src/i18n/` | zh/en localization dictionaries. | +| `plugins/` | Expo Config Plugins for native modules (ppocr, accessibility, sms-receiver, etc.). | +| `tests/` | Vitest unit tests (mock-backed, domain-focused). | + +## Build, Test, and Development Commands + +```bash +npm install --legacy-peer-deps # Install dependencies (legacy-peer-deps required) +npm run start # Start Metro dev server +npm run android # Build & run on Android device/emulator +npm test # Run full Vitest suite +npm run typecheck # TypeScript strict-mode check (tsc --noEmit) + +# Run a single test file: +npx vitest run tests/ledger.test.ts +``` + +## Coding Style & Naming Conventions + +- **TypeScript strict mode** is enabled; path alias `@/*` maps to `src/*`. +- **Money math** uses string decimals (`src/domain/decimal.ts`) — never raw JS floats. +- **Domain layer** (`src/domain/`) must remain pure: no React, RN, or Expo imports. Inject external dependencies via interfaces. +- Code comments and `plan.md` are written in **Chinese**; match that convention. +- New domain modules must be re-exported from `src/domain/index.ts`. +- Native code lives exclusively in `plugins//` as Expo Config Plugins — never edit generated `android/` files directly. + +## Testing Guidelines + +- Framework: **Vitest** (no config file; picks up `tests/**/*.test.ts` by default). +- Tests target the domain layer using mock backends (`MemoryBackend`, `MockOcrEngine`). +- Name test files descriptively: `.test.ts` (e.g., `decimal.test.ts`, `billPipeline.test.ts`). +- Run `npm run typecheck` after non-trivial changes to catch type regressions. + +## Commit & Pull Request Guidelines + +- Commit messages follow the pattern: `feat: ` with a detailed multi-line body listing changes by area. +- Example: `feat: 品牌重命名为浮记(DriftLedger),全面升级精度安全与架构` +- PRs should describe what changed and why; link related issues. Include screenshots for UI changes. + +## Key Architectural Invariants + +1. `.bean` files are the source of truth; SQLite is a disposable read-cache. +2. All bill ingestion funnels through `BillPipeline` (`src/domain/billPipeline.ts`) under a serialized mutex. +3. Every Config Plugin function **must `return config`** at the end. +4. IDs/checksums use FNV-1a hashing (`hash()` in `ledger.ts`), not crypto hashes. diff --git a/README.md b/README.md index 95f6659..8bc5971 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,54 @@ -# Bean Mobile +# DriftLedger (浮记) -面向 Beancount 用户的离线移动端原型:桌面账本只读,移动端仅写入 `mobile.bean`;CSV 账单先生成复式分录草稿,再由用户确认。 +面向 [Beancount](https://beancount.github.io/) 用户的离线移动记账客户端。桌面账本只读,移动端仅追加写入 `main.bean`;支持 OCR、无障碍服务、通知监听、短信等多渠道自动记账。 -## 启动 +**技术栈**:React Native + Expo (CNG) · TypeScript (strict) · Zustand · expo-sqlite · PP-OCRv6 (ONNX Runtime) + +## 快速开始 ```bash -npm install -npm run start +npm install --legacy-peer-deps # 安装依赖(必须带 --legacy-peer-deps) +npm run start # 启动 Metro 开发服务器 +npm run android # 构建并运行 Android +npm test # 运行 Vitest 全量测试 +npm run typecheck # TypeScript 类型检查 ``` -使用 Expo CNG 开发构建运行:`npm run android` 或 `npm run ios`。应用配置启用了 `expo-sqlite` 的 SQLCipher 构建选项,实际密钥管理应接入 SecureStore/Keychain 后再发布。 +> 本应用为侧载开源项目,不上架应用商店。Android 构建详见 [docs/android-build-guide.md](docs/android-build-guide.md)。 + +## 模块概览 + +| 目录 | 职责 | +|------|------| +| `src/domain/` | 纯业务逻辑:`.bean` 解析器、账单管道、去重、规则引擎、OCR 处理器 | +| `src/app/` | Expo Router 文件路由(`(tabs)/` 底部导航) | +| `src/components/` | React Native UI 组件与图表 | +| `src/store/` | Zustand 状态管理(ledger / import / settings / metadata / automation) | +| `src/services/` | 平台服务(同步、备份、安全、OCR 桥接) | +| `src/storage/` | SQLite 读缓存 + 版本化迁移 | +| `src/theme/` | Token 主题系统(浅色 / OLED 暗色) | +| `src/i18n/` | 中英文国际化 | +| `plugins/` | Expo Config Plugins 原生模块(OCR、无障碍、通知、短信、截图) | +| `tests/` | Vitest 单元测试(mock 后端,覆盖 domain 层) | + +## 文档导航 + +| 文档 | 内容 | +|------|------| +| [docs/architecture.md](docs/architecture.md) | 系统架构与数据流 | +| [docs/development.md](docs/development.md) | 开发指南(环境、命令、规范、测试) | +| [docs/android-build-guide.md](docs/android-build-guide.md) | Android 打包与体积优化 | +| [docs/design/](docs/design/) | UI 重设计规格与分阶段实施计划 | +| [design-system/](design-system/beancount-mobile/MASTER.md) | 设计系统 Token 定义 | +| [AGENTS.md](AGENTS.md) | AI 编程助手贡献指南 | +| [plan.md](plan.md) | 设计决策与实施路线图(权威文档) | ## 账本约定 -在主账本添加一次: +在桌面主账本中添加一次: ```beancount -include "mobile.bean" +include "main.bean" ``` -应用不改写主账本或其他桌面维护文件;确认的交易只追加到 `mobile.bean`。导出的账本包由用户再同步到 Git、WebDAV 或 iCloud。 - -## 当前边界 - -已实现账务校验、基础 `.bean` 解析、CSV 账单规范化、去重、规则分类、转账候选和 `mobile.bean` 序列化。Tree-sitter 原生模块、真实文件选择/ZIP 导出、正式支付宝/微信/各银行字段映射、SecureStore 密钥和多设备冲突处理仍需在真机集成阶段接入。 +应用不改写主账本或其他桌面维护文件;确认的交易只追加到 `main.bean`。 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..ba75ce8 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,116 @@ +# 系统架构 + +## 核心不变量 + +`.bean` 文件是唯一事实来源;SQLite 仅为可丢弃的读缓存。 + +## 数据流 + +```mermaid +flowchart LR + subgraph DataSource[数据源] + A[".bean 文件"] + B["OCR / 无障碍 / 通知 / 短信"] + C[“手动录入 / CSV 导入"] + end + + subgraph Pipeline[处理层] + D["BillPipeline 串行互斥锁"] + E[“转账识别"] + F[“去重"] + G[“规则分类"] + end + + subgraph Storage[存储层] + H["main.bean 追加写入"] + I["SQLite 缓存"] + J["Zustand Store 内存索引"] + end + + subgraph UI[展示层] + K["Expo Router 页面"] + L[“报表 / 图表"] + end + + A -->|parseLedger| J + B -->|原始事件| D + C -->|TransactionDraft| D + D --> E --> F --> G + G -->|确认写入| H + H -->|reparse| J + J --> K + J --> L + I -.->|搜索查询| J +``` + +## 模块分层 + +```mermaid +graph TB + subgraph UILayer["UI 层: src/app + src/components"] + R[“Expo Router 页面"] + CO[“可复用组件"] + end + + subgraph StateLayer["状态层: src/store"] + S[“Zustand Stores"] + end + + subgraph DomainLayer["领域层: src/domain — 纯 TS 零 RN 依赖"] + LE["ledger.ts 解析器"] + P["billPipeline.ts"] + D2["dedup / transfer / rules"] + O["ocrProcessor.ts"] + end + + subgraph Infra[“基础设施"] + FS["expo-file-system"] + DB["expo-sqlite"] + NAT["plugins/ 原生模块"] + end + + R --> S + CO --> S + S --> LE + S --> P + P --> D2 + P --> O + LE --> FS + D2 --> DB + O --> NAT +``` + +## BillPipeline 处理顺序 + +所有导入渠道(手动、CSV、OCR、无障碍、通知、短信、截图)统一经过 `BillPipeline`,严格按序执行: + +1. **转账识别** — 配对收入+支出 → 单笔转账(必须先于去重) +2. **批内去重** — 时间窗口 + 金额 + 交易对手 +3. **历史去重** — 按日期索引比对已提交交易 +4. **规则分类** — 规则匹配 → 关键词 → `Uncategorized` 兆底 + +## 原生模块 + +原生功能以 Expo Config Plugin 形式封装在 `plugins//`,详见 [plugins/README.md](../plugins/README.md)。 + +原生服务**不直接写入**数据库或文件,而是通过 `NativeEventEmitter` 将原始事件推送到 JS 层管道。 + +## OCR 三层级联 + +```mermaid +flowchart LR + A[“图像输入"] --> B["Layer 1: 正则规则"] + B -->|未命中| C["Layer 2: 本地 OCR PP-OCRv6"] + C -->|未命中| D["Layer 3: AI Vision 付费"] +``` + +## 双轨映射 + +Beancount 无原生“分类/预算”概念,应用在本地 SQLite 维护 UI 元数据,写入时映射回 Beancount 语义: + +| 概念 | 本地表 | 映射到 `.bean` | +|------|--------|----------------| +| 分类 | `categories` | `linkedAccount` → posting 账户 | +| 标签 | `tags` | narration 中的 `#tag` | +| 预算 | `budgets` | 仅本地,不影响余额 | +| 信用卡 | `credit_cards` | 账户在 `.bean`,UI 字段本地 | diff --git a/docs/ui-redesign-design.md b/docs/design/ui-redesign-design.md similarity index 100% rename from docs/ui-redesign-design.md rename to docs/design/ui-redesign-design.md diff --git a/docs/ui-redesign-p1-plan.md b/docs/design/ui-redesign-p1-plan.md similarity index 100% rename from docs/ui-redesign-p1-plan.md rename to docs/design/ui-redesign-p1-plan.md diff --git a/docs/ui-redesign-p2-plan.md b/docs/design/ui-redesign-p2-plan.md similarity index 100% rename from docs/ui-redesign-p2-plan.md rename to docs/design/ui-redesign-p2-plan.md diff --git a/docs/ui-redesign-p3-plan.md b/docs/design/ui-redesign-p3-plan.md similarity index 100% rename from docs/ui-redesign-p3-plan.md rename to docs/design/ui-redesign-p3-plan.md diff --git a/docs/ui-redesign-p4-plan.md b/docs/design/ui-redesign-p4-plan.md similarity index 100% rename from docs/ui-redesign-p4-plan.md rename to docs/design/ui-redesign-p4-plan.md diff --git a/docs/ui-redesign-p5-plan.md b/docs/design/ui-redesign-p5-plan.md similarity index 99% rename from docs/ui-redesign-p5-plan.md rename to docs/design/ui-redesign-p5-plan.md index 37a29ea..26efe1d 100644 --- a/docs/ui-redesign-p5-plan.md +++ b/docs/design/ui-redesign-p5-plan.md @@ -9,6 +9,7 @@ **与 spec 的偏差**:①account 页是「账户浏览器」(类型 Tab + 开户/调余额/关户三个交互),不套 ManagementScreen,只做 ScreenHeader 统一;②`numpadGlobalEntry` 翻默认只影响新安装(已持久化的 false 不覆盖,尊重用户选择)。 **模板适配决策**(已通读 7 页源码): + - budget/rules/remark-template → 直套(T1) - category → `headerContent` 放支出/收入 chips,新增按当前类型(T2) - recurring → 卡片保留显式编辑/删除按钮(用 handlers.openEdit/confirmDelete),不用长按(T2) @@ -19,6 +20,7 @@ ### Task 1: 管理页模板化批次 1(budget / rules / remark-template) **Files:** + - Modify: `src/app/budget/index.tsx`、`src/app/rules/index.tsx`、`src/app/remark-template/index.tsx` - [ ] **Step 1: 三页套 ManagementScreen**(参照试点 `src/app/tag/index.tsx` 与模板 `src/components/ManagementScreen.tsx`) @@ -26,6 +28,7 @@ 通用映射:页面 state 消失(modal/editing 由模板持有);手写 header/addBtn/Alert/FormModal 全删;`footer` 传 `common.clickEditLongDelete` 提示;`deleteConfirmTitle` 传各页 `t('xxx.deleteTitle')`。 **budget/index.tsx**: + - `items={budgets}`、`keyExtractor={b => b.id}`、`addLabel={t('budget.add')}`、`emptyText={t('budget.empty')}` - renderItem:保留现有 Card + 进度条结构(Pressable onPress={openEdit} onLongPress={confirmDelete} 包裹)。`today` 改用 `toDateString(new Date())`(修掉 `toISOString().slice(0,10)` 的 UTC 偏移) - formFields:5 字段(name/amount decimal-pad/period/categoryAccount/startDate),defaultValue 从 editing 取 @@ -33,12 +36,14 @@ - onDelete:`removeBudget(budget.id)`;deleteConfirmText:`t('budget.deleteConfirm', { name: budget.name })` **rules/index.tsx**: + - `items={[...rules].sort((a, b) => b.priority - a.priority)}` - renderItem:保留规则行(narration/P 徽标/条件/目标账户+命中次数);**删除 `fontFamily: theme.typography.caption.fontFamily`**(审计项) - formFields:7 字段(priority numeric/counterpartyContains/memoContains/sourceAccount/categoryAccount/narration/tags) - onSubmit:保留现有 Rule 组装逻辑;addRule `{...rule, id: generateId('rule'), hits: 0}` / updateRule **remark-template/index.tsx**: + - 2 字段(name/template);renderItem 的模板文本 `fontFamily: 'monospace'` 改 `fontVariant: ['tabular-nums']`(审计项) - onSubmit:name 空回退 `t('common.untitled')` @@ -52,6 +57,7 @@ Run: `grep -ln "FormModal\|arrow-back" src/app/budget/index.tsx src/app/rules/in ### Task 2: 管理页模板化批次 2(category / recurring / credit-card) **Files:** + - Modify: `src/app/category/index.tsx`、`src/app/recurring/index.tsx`、`src/app/credit-card/index.tsx` - [ ] **Step 1: category/index.tsx — headerContent 类型 chips** @@ -87,6 +93,7 @@ Run: `grep -ln "FormModal\|arrow-back" src/app/category/index.tsx src/app/recurr ### Task 3: ScreenHeader 统一(account + settings 三页) **Files:** + - Modify: `src/app/account/index.tsx`、`src/app/settings/sync.tsx`、`src/app/settings/ai.tsx`、`src/app/settings/preferences.tsx` - [ ] **Step 1: account/index.tsx** @@ -107,6 +114,7 @@ Run: `grep -rn "arrow-back" src/app/ | grep -v ScreenHeader` → 无输出 ### Task 4: 图表与杂项清理(fontFamily / emoji / 星期头 i18n) **Files:** + - Modify: `src/components/charts/CategoryPie.tsx`、`src/components/charts/TrendLine.tsx`、`src/components/charts/NetWorthChart.tsx`、`src/components/CalendarView.tsx`、`src/components/charts/CalendarHeatmap.tsx`、`src/components/DatePickerField.tsx`、`src/components/NumpadKeyboard.tsx`、`src/components/NumpadSheet.tsx`、`src/ai/monthlySummary.ts`、`src/app/ai/chat.tsx`、`src/app/transaction/[id].tsx`、`src/app/_error.tsx`、`src/app/import/index.tsx`、`src/components/PostingEditor.tsx`、`src/i18n/zh.ts`、`src/i18n/en.ts` - [ ] **Step 1: fontFamily 清零** @@ -142,6 +150,7 @@ Run: `grep -rn "fontFamily" src/ --include="*.tsx" | grep -v "_error.tsx"` → ### Task 5: 收尾(翻默认 + modal 守卫 + MonthlyReport 删除 + budgets 时区修复 + duplicate 增强) **Files:** + - Modify: `src/store/settingsStore.ts`、`src/components/NumpadSheet.tsx`、`src/components/NumpadSheetHost.tsx`、`src/domain/budgets.ts`、`src/app/(tabs)/transactions.tsx`、`tests/budgets.test.ts`(或现有 budget 测试文件) - Delete: `src/components/charts/MonthlyReport.tsx` @@ -207,6 +216,7 @@ Run: `npm run typecheck` → 无错误 Run: `npm run android` 走查清单(浅/暗双主题): + 1. 6 个管理页(分类/预算/规则/周期/信用卡/备注模板):+新增、点按编辑、删除(recurring 为显式按钮,其余长按)、空态文案 2. account 页:类型 Tab、开户、调余额、关户 3. settings/sync、ai、preferences:header 统一有返回箭头 diff --git a/docs/design/ui-redesign-p6-plan.md b/docs/design/ui-redesign-p6-plan.md new file mode 100644 index 0000000..9b939c6 --- /dev/null +++ b/docs/design/ui-redesign-p6-plan.md @@ -0,0 +1,317 @@ +# 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()` → 原生存 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` + +```typescript +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 行为不变)。 +- `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.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`** + +```typescript +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; +``` + +- [ ] **Step 3: bridge 接口扩展** + +`accessibilityBridge.ts`:`NativeAccessibilityBridge` 加 `setFloatingUiConfig(config: FloatingUiConfig): Promise;`,文件头注释方法清单补一行。`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.parseColor` try/catch)。 +- org.json 可用(Android 内置),无需新依赖。 + +- [ ] **Step 2: AccessibilityBridgeModule.setFloatingUiConfig** + +```kotlin +@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,ProgressBar `progressTintList` 设 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: 审计** + +```bash +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),计划要求实色 | 菜单背景直接使用 `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` 保留为语义色(扫描红光),不受主题控制。 diff --git a/docs/design/ui-redesign-p7-plan.md b/docs/design/ui-redesign-p7-plan.md new file mode 100644 index 0000000..8ad7e0a --- /dev/null +++ b/docs/design/ui-redesign-p7-plan.md @@ -0,0 +1,192 @@ +# P7:权限快捷引导 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax. **不执行任何 git 操作**(用户要求,改动留工作区)。不创建额外任务清单。 + +**Goal:** 引导页权限步骤从纯文字改可操作状态清单;automation 页补齐通知监听/短信/存储三项权限的状态检测与快速跳转按钮。不新增原生权限,只加 UI 入口和一座通知监听状态检测 bridge 方法。 + +--- + +## 背景决策 + +- **不新增权限**。所有权限已在 AndroidManifest 中声明(通过 Config Plugin),P7 只加 UI 入口。 +- **通知监听状态检测**需要一个原生 bridge 方法(`isNotificationListenerEnabled`),因为 RN 侧无法直接检查。用 `NotificationManager.getEnabledListenerPackages()`。 +- **短信 + 存储**用 React Native 内置 `PermissionsAndroid` API。 +- **引导页 UI** 沿用现有 Card + Button 组件,与 P1-P5 设计系统一致。 + +--- + +### Task 1: 新增桥接方法 `isNotificationListenerEnabled` + +**Files:** +- Modify: `plugins/accessibility/android/AccessibilityBridgeModule.kt` +- Modify: `src/services/accessibilityBridge.ts` + +- [ ] **Step 1: AccessibilityBridgeModule.kt 加方法** + +```kotlin +@ReactMethod +fun isNotificationListenerEnabled(promise: Promise) { + try { + val pm = reactContext.packageManager + val enabledListeners = android.app.NotificationManager::class.java + .getMethod("getEnabledListenerPackages") + .invoke(reactContext.getSystemService(Context.NOTIFICATION_SERVICE)) as? List + val myPkg = reactContext.packageName + promise.resolve(enabledListeners?.any { it.startsWith(myPkg) } == true) + } catch (e: Exception) { + promise.resolve(false) + } +} +``` + +**注意**:`getEnabledListenerPackages()` 在 Android 11+ 需 `isNotificationListenerEnabled(ComponentName)`;优先用反射避免 API level lint 错误,反射失败返回 false。 + +- [ ] **Step 2: JS 侧 bridge 接口** + +`NativeAccessibilityBridge` 加 `isNotificationListenerEnabled(): Promise;` + +- [ ] **Step 3: 编译验证** + +`cd android && ./gradlew :app:compileDebugKotlin` → BUILD SUCCESSFUL + +--- + +### Task 2: 引导页权限步骤(`_onboarding.tsx` 重写 permissions 步骤) + +**Files:** +- Modify: `src/app/_onboarding.tsx` +- Modify: `src/i18n/zh.ts`、`src/i18n/en.ts`(少量新键) + +- [ ] **Step 1: i18n 新键(zh/en 对等)** + +在 `onboarding` section 加: +``` +permAccessibility 无障碍服务 / Accessibility Service +permNotification 通知监听 / Notification Listener +permOverlay 悬浮窗 / Overlay +permSms 短信读取 / SMS +permStorage 相册/存储 / Photos & Storage +permGranted 已授权 / Granted +permNotGranted 未授权 / Not Granted +permOpenSettings 前往设置 / Open Settings +permRequest 请求权限 / Request +``` + +- [ ] **Step 2: 重写 permissions 步骤** + +替换 `_onboarding.tsx:48` 的 `key: 'permissions'` 步骤内容。在当前 description 下方加权限状态清单(Card 包裹,每行:图标 + 名称 + 状态点 + 操作按钮)。 + +权限列表: +1. **无障碍服务**(`BIND_ACCESSIBILITY_SERVICE`)—— 最核心。检测 `serviceRunning`(从 `getAccessibilityBridge().isServiceRunning()` 异步取)。未授权 → 跳转 `ACCESSIBILITY_SETTINGS` +2. **通知监听**(`BIND_NOTIFICATION_LISTENER_SERVICE`)—— 检测 `bridge.isNotificationListenerEnabled()`。未授权 → 跳转 `ACTION_NOTIFICATION_LISTENER_SETTINGS` +3. **短信**(`RECEIVE_SMS`)—— `PermissionsAndroid.check()`。未授权 → `PermissionsAndroid.request()` +4. **存储**(`READ_MEDIA_IMAGES`,Android 13+ 用 `READ_MEDIA_IMAGES`,否则 `READ_EXTERNAL_STORAGE`)—— 同上 + +**实现细节**: +- `const [permStates, setPermStates] = useState>({})` —— null = 加载中 +- `useEffect(() => { checkAllPermissions(); }, [])` —— 步骤切到 permissions 时触发 +- `checkAllPermissions`:调 `PermissionsAndroid.check()` 检查短信和存储;调 bridge 检查无障碍和通知监听 +- 每行渲染:`Ionicons` 图标 + `permLabel` + 右侧状态文字(绿「已授权」/ 灰「未授权」)+ 未授权时显示操作按钮 +- 「全部已授权」时显示一个大绿色勾 + 提示文字,按钮不显示 + +**权限 API 版本适配**: +- `PermissionsAndroid.PERMISSIONS` 里 Android 13+ 是 `READ_MEDIA_IMAGES`,低版本用 `READ_EXTERNAL_STORAGE` +- 统一用 `Platform.Version >= 33 ? 'android.permission.READ_MEDIA_IMAGES' : 'android.permission.READ_EXTERNAL_STORAGE'` + +- [ ] **Step 3: 验证** + +Run: `npm run typecheck && npm test` → 全绿 +Run: `grep -rn "onboarding.perm" src/i18n/zh.ts src/i18n/en.ts` → 确认键对等 + +--- + +### Task 3: automation 页补齐权限入口 + +**Files:** +- Modify: `src/app/automation/index.tsx` +- Modify: `src/i18n/zh.ts`、`src/i18n/en.ts`(少量新键) + +- [ ] **Step 1: i18n 新键** + +在 `automation` section 加: +``` +notificationTitle 通知监听服务 / Notification Listener +notificationEnabled 已启用 / Enabled +notificationDisabled 未启用 / Disabled +notificationOpenSettings 前往系统设置 / Open Settings +smsPermissionTitle 短信读取权限 / SMS Permission +smsPermissionGranted 已授权 / Granted +smsPermissionRequest 请求权限 / Request +storagePermissionTitle 存储权限 / Storage Permission +storagePermissionGranted 已授权 / Granted +storagePermissionRequest 请求权限 / Request +``` + +- [ ] **Step 2: automation 页加权限卡片** + +在现有「无障碍服务状态与控制」Card **之后**加一个新 Card:``。 + +内容三行: +1. **通知监听**:异步 `bridge.isNotificationListenerEnabled()` → 状态文字 + 未启用时「前往系统设置」按钮(`Linking.sendIntent('android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS')`) +2. **短信**:`PermissionsAndroid.check('android.permission.RECEIVE_SMS')` + `PermissionsAndroid.request()` +3. **存储**:`PermissionsAndroid.check(StoragePermission)` + `PermissionsAndroid.request()` + +**实现模式**: +```tsx +const [notifEnabled, setNotifEnabled] = useState(false); +const [smsGranted, setSmsGranted] = useState(false); +const [storageGranted, setStorageGranted] = useState(false); +useEffect(() => { + // 异步获取各权限状态 + checkNotif(); checkSms(); checkStorage(); +}, []); +``` + +每行渲染:`Ionicons` 图标 + 权限名称 + 状态文字 + 未授权时的按钮。 + +- [ ] **Step 3: 验证** + +Run: `npm run typecheck && npm test` → 全绿 + +--- + +### Task 4: 编译 + 测试 + 终审 + +- [ ] **Step 1: 编译验证** + +`cd android && ./gradlew :app:compileDebugKotlin` → BUILD SUCCESSFUL + +- [ ] **Step 2: 全量测试** + +`npm test` → 全绿;`npm run typecheck` → 0 错误 + +- [ ] **Step 3: 审计** + +```bash +grep -rn "#[0-9A-Fa-f]\{6\}" src/ --include="*.ts" --include="*.tsx" | grep -v "theme/presets.ts\|theme/palette.ts" # 无输出 +grep -rn "fontFamily" src/app/_onboarding.tsx src/app/automation/index.tsx # 无输出 +grep -rnE "📊|💰|💸|📝|🔄|🎉|✨|⚠|📌|🌟" src/i18n/ src/app/_onboarding.tsx src/app/automation/index.tsx # 无输出(预存 i18n fallbackWarn ⚠ 除外) +``` + +- [ ] **Step 4: 手工走查(需设备)** + +1. 清数据 → 启动应用 → 引导页第 5 步(权限)→ 看到 4 个权限状态 +2. 点未授权的无障碍 → 跳系统无障碍设置 → 返回看到状态已变 +3. 点未授权的通知监听 → 跳系统通知设置 +4. 点短信 → 弹出系统权限对话框 +5. 引导页全部完成后 → 进入 automation 页 → 看到新增的权限卡片 + +--- + +## 终审修订(已实施,以实际代码为准) + +**终审日期**:2026-07-22,审计 598 测试全绿、typecheck 干净、Kotlin BUILD SUCCESSFUL。 + +| # | 级别 | 问题 | 处理 | +|---|---|---|---| +| — | — | 无 Critical / Important 发现 | — | + +**偏差说明**: +1. `onboarding.permOverlay`(悬浮窗)未加入引导页——计划 Task 2 决策列为可选项,实际未实现。不影响核心功能(sideload 自动授予 + 无障碍运行时浮窗走 ACCESSIBILITY_OVERLAY 不依赖此权限)。 +2. T3 中 `Platform.Version >= 33` 改为 `Number(Platform.Version) >= 33`——React Native 的 `Platform.Version` 类型为 `string | number`,直接比较会 TS 报错。 +3. T1 的 `isNotificationListenerEnabled` 使用反射而非 `enabledListenerPackages` 属性——minSdk=24 < API 27,反射是兼容方案。 diff --git a/docs/design/ui-redesign-p8-plan.md b/docs/design/ui-redesign-p8-plan.md new file mode 100644 index 0000000..6b26414 --- /dev/null +++ b/docs/design/ui-redesign-p8-plan.md @@ -0,0 +1,338 @@ +# P8:自动记账可配置化 + 模型按需下载 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 操作**。不创建额外任务清单。 + +**Goal:** automation 页统一管控所有自动记账能力(规则/OCR/AI Vision 三层独立开关 + 模型下载管理 + AI 配置整合),`settings/ai.tsx` 删除并重定向;OCR 模型从 APK assets 改为应用内下载(filesystem 路径加载 ONNX)。 + +**背景决策**: +- 三层开关默认值:L1 规则 `true`、L2 OCR `true`、L3 AI Vision `false`(需用户配 API Key 后才可开启) +- 模型下载:首次启用 L2 时检查模型是否存在,无则弹出下载提示 +- `settings/ai.tsx` 删除,原入口重定向到 automation 页 +- 去重/转账识别两个 setting 字段不动(它们不是「层」而是管道的独立步骤),但在 automation 页加开关 +- 模型从 assets 复制到 filesystem:预构建时仍打 APK assets 以兼容旧用户,应用首次启动将模型文件从 assets 拷贝到 documentDirectory,OcrModule 从 filesystem 加载。**这样之后卸载 assets 打包就变成了纯删除**(未来版本切到纯下载只需移除 assets + app.plugin.js 禁拷贝)。 + +--- + +### Task 1: settingsStore 新增字段 + 迁移 + +**Files:** `src/store/settingsStore.ts` + +- [ ] **Step 1: 接口 + DEFAULTS + 持久化 + setter** + +在 `SettingsState` 接口加 5 个新字段: +```typescript +/** 自动记账 L1:正则规则匹配 */ +layer1RuleEnabled: boolean; +/** 自动记账 L2:本地 OCR 识别 */ +layer2OcrEnabled: boolean; +/** 自动记账 L3:云端 AI Vision */ +layer3AiEnabled: boolean; +/** OCR 模型版本(已下载),空字符串表示未安装 */ +ocrModelVersion: string; +/** OCR 模型下载路径 */ +ocrModelDir: string; +``` + +DEFAULTS: +- `layer1RuleEnabled: true` +- `layer2OcrEnabled: true` +- `layer3AiEnabled: false` +- `ocrModelVersion: ''` +- `ocrModelDir: ''` + +`PersistableSettings` 加对应 5 字段;`toPersistable` 加 5 行;新增 5 个 setter 函数。现有 `aiEnabled` 保留作为 LLM 的总开关语义(`layer3AiEnabled` 是在 automation 页独立控制,「启用」需同时满足 `aiEnabled && aiApiKey`)。 + +**注意**:`isNotificationListenerEnabled` 这类 bridge 方法在 `setFloatingBallEnabled` 附近——新字段与它们无关,不加 bridge 方法。 + +- [ ] **Step 2: 验证** + +`npm run typecheck` → 0 错误;`npm test` → 全绿 + +--- + +### Task 2: OcrProcessor 逐级回退逻辑 + +**Files:** `src/domain/ocrProcessor.ts`、`src/services/ocrBridge.ts`、`src/services/automationPipeline.ts` + +- [ ] **Step 1: OcrProcessorConfig 扩展** + +`OcrProcessorConfig` 接口加: +```typescript +layer1Enabled: boolean; // 默认 true +layer2Enabled: boolean; // 默认 true +layer3Enabled: boolean; // 默认 false +``` + +- [ ] **Step 2: doProcess() 逐级跳过** + +在 `OcrProcessor.doProcess()` 中修改流程(约 :103-162): + +``` +// 1. 横屏 / 图片去重 守卫不变 + +// 2. L1+L2 需要 OCR 文本 +if (config.layer1Enabled || config.layer2Enabled) { + if (!ocrEngine) 返回 none(引擎不可用) + ocrText = await ocrEngine.recognizeText(imageBase64) + if (!ocrText) → 如果 L3 启用则 goto L3,否则返回 none +} else { + ocrText = '' +} + +// 3. Layer 1(仅当启用) +if (config.layer1Enabled && ocrText && !isDetailPage) { + result = matchOcrRule(ocrText, packageName) + if (result) return { layer: 'layer1-rule', ... } +} + +// 4. Layer 2(仅当启用) +if (config.layer2Enabled && ocrText) { + result = parseOcrBill(ocrText, packageName) + if (result) return { layer: 'layer2-ocr', ... } +} + +// 5. Layer 3(仅当启用) +if (config.layer3Enabled) { + return tryLayer3(imageBase64, ocrText) +} + +// 6. 全部未命中或全部关闭 +return { event: null, layer: 'none', skipped: 'non-bill' } +``` + +- [ ] **Step 3: automationPipeline.ts 的 getOcrProcessor 改读新字段** + +`getOcrProcessor()` 约 :116-147 行:从 `settingsStore` 读取 `layer1RuleEnabled` / `layer2OcrEnabled` / `layer3AiEnabled` 传入 `OcrProcessorConfig`。 + +- [ ] **Step 4: 验证** + +`npm run typecheck` → 0 错误;`npm test` → 全绿 + +--- + +### Task 3: 模型文件从 assets 拷贝到 filesystem + OcrModule 改路径 + +**Files:** `plugins/ppocr/android/OcrModule.kt`、`plugins/ppocr/app.plugin.js`、`plugins/accessibility/android/AccessibilityBridgeModule.kt`(新 bridge 方法) + +- [ ] **Step 1: AccessibilityBridgeModule 加 copyModelFromAssets 方法** + +```kotlin +@ReactMethod +fun copyOcrModelsFromAssets(promise: Promise) { + try { + val assetManager = reactContext.assets + val destDir = java.io.File(reactContext.filesDir, "ocr_models_v6") + if (!destDir.exists()) destDir.mkdirs() + + val files = listOf("ppocrv6_det.onnx", "ppocrv6_rec.onnx", "ppocrv6_dict.txt") + for (filename in files) { + val destFile = java.io.File(destDir, filename) + if (destFile.exists()) continue // skip existing + val input = assetManager.open(filename) + val output = java.io.FileOutputStream(destFile) + val buffer = ByteArray(8192) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + } + input.close() + output.close() + } + promise.resolve(destDir.absolutePath) + } catch (e: Exception) { + promise.reject("COPY_MODEL_FAIL", e.message) + } +} +``` + +- [ ] **Step 2: OcrModule.kt 改为 filesystem 路径加载** + +读 `OcrModule.kt` 中 `createSession` 调用的位置,改从传入路径加载而非 asset 文件名。当前构造可能接受 asset 路径——改为接收 filesystem 绝对路径。新增一个 `@ReactMethod setModelDir(dir: String)` 存储路径,或直接在 `initialize(assetManager: ...)` 的参数中改为 `initialize(modelDir: String, ...)`。 + +**注意**:ONNX Runtime Android 的 `OrtEnvironment.createSession(filePath)` 接受文件系统路径(String),不需要 asset 特殊处理——只需确认 `createSession` 的参数类支持文件路径,否则改用 `OrtEnvironment.createSession(byte[] modelBytes)` 从内存加载。**最终方案**:保持 `OrtEnvironment.createSession(filePath)` ,将 `modelDir + "/ppocrv6_det.onnx"` 作为绝对路径传入。 + +- [ ] **Step 3: JS 侧 bridge 接口 + modelDownloader 服务** + +`NativeAccessibilityBridge` 加 `copyOcrModelsFromAssets(): Promise` → 返回模型目录绝对路径。 + +新建 `src/services/modelDownloader.ts`: +```typescript +export async function ensureOcrModels(): Promise { + // 1. 如果 settingsStore.ocrModelDir 有效且文件存在 → 直接返回 + // 2. 尝试从 assets 拷贝到 documentDirectory/ocr_models_v6 + // 3. 返回 modelsDir 绝对路径,存储到 settingsStore.ocrModelDir + ocrModelVersion +} +``` + +`ocrModelVersion` 在从 assets 拷贝成功后写 `'v6-assets'`(区分下载版本和打包版本)。 + +- [ ] **Step 4: automationPipeline.ts 的 OcrProcessor 从 filesystem 初始化** + +`getNativeOcrBridge()`或 `getOcrProcessor()` 中,在初始化 OCR 引擎前调用 `ensureOcrModels()` 获取模型路径,传入 `NativeOcrBridge` 构造(或 OcrModule initialize)。 + +- [ ] **Step 5: 验证** + +`cd android && ./gradlew :app:compileDebugKotlin` → BUILD SUCCESSFUL + +--- + +### Task 4: automation 页 UI —— 三层开关 + 模型管理 + AI 配置整合 + +**Files:** `src/app/automation/index.tsx`、`src/i18n/zh.ts`、`src/i18n/en.ts` + +- [ ] **Step 1: i18n 新键(zh/en 对等)** + +在 `automation` section 加: +``` +autoBookkeeping 自动记账层级 / Auto Bookkeeping Layers +layer1Rule 规则匹配 / Rule Matching +layer1RuleDesc 基于正则规则的快速账单识别 / Fast regex-based bill recognition +layer2Ocr OCR 识别 / OCR Recognition +layer2OcrDesc 本地 PP-OCRv6 模型识别 / On-device PP-OCRv6 model +layer3Ai AI 视觉识别 / AI Vision +layer3AiDesc 云端多模态大模型识别 / Cloud multimodal LLM +layer3AiDisabled 请先配置 AI Key / Configure AI Key first +ocrModelTitle OCR 模型 / OCR Model +ocrModelNotInstalled 未安装 / Not Installed +ocrModelInstalled 已安装 ({version}) / Installed ({version}) +ocrModelInstall 安装模型 / Install Model +ocrModelInstalling 安装中... / Installing... +ocrModelCopying 正在复制模型文件... / Copying model files... +dedupSwitch 多通道联合去重 / Cross-channel Dedup +transferSwitch 转账智能识别 / Transfer Recognition +``` + +- [ ] **Step 2: automation 页 UI 重组织** + +在现有截图监控按钮 Card **之后**加以下新 Card 区块: + +**Card A:「自动记账层级」** +- 三行 Switch:L1 规则匹配(always enabled,不依赖外部条件) +- L2 OCR 识别(Switch;开启时检查模型→无模型则弹下载提示 Dialog) +- L3 AI 视觉(Switch;如果 aiEnabled=false 或 aiApiKey 为空则 disabled+提示文字;开启后展开 Provider/Key/Model 输入) + +**Card B:「OCR 模型」** +- 状态行:版本号(未安装显示灰色「未安装」;已安装显示绿色「已安装 (v6-assets)」) +- 操作按钮:安装/重新安装(从 assets 拷贝 → 进度状态 → 完成) + +**Card C:「管道设置」**(复用现有 Card B 和 C 部分) +- `dedupEnabled` Switch +- `transferRecognitionEnabled` Switch + +- [ ] **Step 3: 操作逻辑** + +```typescript +// 模型安装 +const handleInstallModel = async () => { + setModelStatus('installing'); + try { + const bridge = getAccessibilityBridge(); + if (!bridge) throw new Error('Bridge unavailable'); + const modelDir = await bridge.copyOcrModelsFromAssets(); + setOcrModelVersion('v6-assets'); + setOcrModelDir(modelDir); + setModelStatus('done'); + } catch (e) { + setModelStatus('error'); + Alert.alert('安装失败', String(e)); + } +}; + +// L2 开关变化时检查模型 +const handleL2Toggle = (val: boolean) => { + setLayer2OcrEnabled(val); + if (val && !ocrModelVersion) { + Alert.alert(t('automation.ocrModelTitle'), t('automation.ocrModelNotInstalled'), [ + { text: t('common.cancel'), onPress: () => setLayer2OcrEnabled(false) }, + { text: t('automation.ocrModelInstall'), onPress: handleInstallModel }, + ]); + } +}; + +// L3 开关变化时检查 AI Key +const handleL3Toggle = (val: boolean) => { + if (val && (!aiEnabled || !aiApiKey)) { + Alert.alert(t('automation.layer3Ai'), t('automation.layer3AiDisabled')); + return; + } + setLayer3AiEnabled(val); +}; +``` + +- [ ] **Step 4: AI 配置内联** + +L3 启用时展开的子区域: +- `aiProviderId`:三选一 chip(openai/gemini/deepseek) +- `aiApiKey`:输入框(secureTextEntry) +- `aiBaseUrl`:输入框(默认值 placeholder) +- `aiModel`:输入框(默认值 placeholder) +- `aiEnabled`:Switch(总开关,控制 L3 的实际可用性) + +- [ ] **Step 5: 验证** + +`npm run typecheck` → 0 错误;`npm test` → 全绿 + +--- + +### Task 5: AI 设置页清理 + settings 导航更新 + +**Files:** `src/app/settings/ai.tsx`(重写为跳转)、`src/app/(tabs)/settings.tsx`(更新入口文案) + +- [ ] **Step 1: settings/ai.tsx 改为 redirect** + +不删除文件(保留路由),但内容改为:`useEffect(() => { router.replace('/automation'); }, [])` 跳转到 automation 页,期间显示 loading。 + +- [ ] **Step 2: settings.tsx 入口文案** + +`src/app/(tabs)/settings.tsx` 中「智能记账 & AI」入口的文案保持不变,但实际点击跳转已由 expo-router 自动处理(ai.tsx → redirect → automation),不需要改路由注册。 + +- [ ] **Step 3: 验证** + +`npm run typecheck` → 0 错误;`npm test` → 全绿 +`grep -rn "settings/ai" src/app/(tabs)/settings.tsx` → 确认无需改动即可跳转 + +--- + +### Task 6: 编译 + 测试 + 终审 + +- [ ] **Step 1: Kotlin 编译** + +`cd android && ./gradlew :app:compileDebugKotlin` → BUILD SUCCESSFUL + +- [ ] **Step 2: 全量测试** + +`npm test` → 全绿;`npm run typecheck` → 0 错误 + +- [ ] **Step 3: 审计** + +```bash +grep -rn "#[0-9A-Fa-f]\{6\}" src/ --include="*.ts" --include="*.tsx" | grep -v "theme/presets.ts\|theme/palette.ts" # 无输出 +grep -rn "fontFamily" src/app/automation/index.tsx # 无输出 +grep -rn "📊|💰|💸|📝" src/app/automation/ # 无输出 +``` + +- [ ] **Step 4: 手工走查清单** + +1. settings 页点「智能记账」→ 跳转到 automation 页 +2. automation 页看到三个层级开关(L1 默认开 / L2 默认开 / L3 默认关灰) +3. 关闭 L1 → 仍可记账(走 L2 OCR) +4. 关闭 L2 → L1 失败不再调用 OCR +5. 开启 L3 → 提示配 AI Key → 配置后可用 +6. OCR 模型未安装 → 点安装 → 从 assets 拷贝 → 显示绿色已安装 +7. L2 开关关闭再打开 → 已有模型直接可用,不再提示 +8. AI Provider 切换 openai/gemini/deepseek → 对应的 baseUrl 默认值变化 + +--- + +## 终审修订(已实施,以实际代码为准) + +**日期**:2026-07-22,审计 598 测试全绿、typecheck 干净、Kotlin BUILD SUCCESSFUL。 + +| # | 级别 | 问题 | 处理 | +|---|---|---|---| +| — | — | 无 Critical / Important 发现 | — | + +**偏差说明**: +1. 管道设置 Card 复用现有 `settings.dedupLabel`/`settings.transferLabel` i18n 键(已在 settings/ai.tsx 中使用),未新键 `dedupSwitch`/`transferSwitch`——避免重复键。 +2. OcrModule.kt 从 assets 加载路径改为 `modelDir` 字段 + `setModelDir` 方法——保留原 assets 加载作为兜底(modelDir 为空时走原路径),保证向后兼容。 +3. `ensureOcrModels()` 在 `_layout.tsx` ready 前调用一次(异步非阻塞),`getOcrProcessor()` 中也传 `ocrModelDir` 作为安全网。 diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..c4c6db6 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,68 @@ +# 开发指南 + +## 环境要求 + +- Node.js 18+ +- JDK 21 + Android SDK 35 + NDK 27.1.12297006(仅 Android 构建需要) +- 详见 [android-build-guide.md](android-build-guide.md) + +## 常用命令 + +```bash +npm install --legacy-peer-deps # 安装依赖(必须带 --legacy-peer-deps) +npm run start # 启动 Metro 开发服务器 +npm run android # expo run:android(设备/模拟器) +npm test # Vitest 全量测试 +npm run typecheck # tsc --noEmit 类型检查 + +# 单文件测试 +npx vitest run tests/ledger.test.ts + +# 按名称运行测试 +npx vitest run -t "拒绝不平衡交易" + +# 重新生成原生项目 +npx expo prebuild --platform android +``` + +## 编码规范 + +### TypeScript + +- **strict 模式**已开启,路径别名 `@/*` → `src/*` +- 非平凡改动后必须运行 `npm run typecheck` + +### 金额计算 + +- 使用字符串十进制运算(`src/domain/decimal.ts`),**禁止**用 JS 浮点数处理金额 +- 关键 API:`addDecimals`、`subtractDecimals`、`multiplyDecimal`、`divideDecimals`、`negateDecimal` + +### 领域层纯净性 + +- `src/domain/` 禁止导入 React / React Native / Expo 任何模块 +- 外部依赖通过接口注入:`MobileBeanBackend`、`OcrEngine`、`AiVisionProvider` +- 新模块必须在 `src/domain/index.ts` 重导出 + +### 原生代码 + +- 原生 Kotlin/XML 只能放在 `plugins//` 下,不可直接编辑 `android/` +- 每个 Config Plugin 函数**必须 `return config`** +- `expo prebuild` 后检查 `onnxruntime-android:1.20.0` 版本 + +### 注释与文档 + +- 代码注释和 `plan.md` 使用**中文** +- ID/校验和使用 FNV-1a 哈希(`hash()` in `ledger.ts`),不用加密哈希 + +## 测试 + +- 框架:**Vitest**(无配置文件,默认拾取 `tests/**/*.test.ts`) +- 测试对象:主要覆盖 `src/domain/` 层,使用 mock 后端(`MemoryBackend`、`MockOcrEngine`) +- 命名:`<模块或功能>.test.ts`,如 `decimal.test.ts`、`billPipeline.test.ts` +- 新增 domain 逻辑时必须同步添加测试 + +## 提交规范 + +- 提交消息格式:`feat: <中文摘要>`,多行 body 按模块分区列举变更 +- 示例:`feat: 品牌重命名为浮记(DriftLedger),全面升级精度安全与架构` +- PR 需描述变更内容与原因,UI 变更附截图,关联相关 issue diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..4fe20cb --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,31 @@ +# Expo Config Plugins + +本目录包含所有原生模块的 Expo Config Plugin。由于 `android/` 目录由 `expo prebuild` 自动生成且被 Git 忽略,所有原生代码必须以 Config Plugin 形式存在。 + +## 插件结构 + +``` +plugins// + app.plugin.js # prebuild 时执行,注入 Kotlin/XML、修改 gradle、注册服务 + package.json # 插件元数据 + android/*.kt # 原生 Kotlin 代码 + assets/ # 模型文件等静态资源(仅 ppocr) +``` + +## 当前插件 + +| 插件 | 功能 | 备注 | +|------|------|------| +| `ppocr` | PP-OCRv6 检测+识别 | ONNX Runtime,模型在 `assets/` | +| `accessibility` | 无障碍服务账单抓取 + 悬浮窗 + OCR 贴片 | 仅侧载场景 | +| `notification-listener` | 通知栏账单监听 | | +| `sms-receiver` | 短信账单解析 | | +| `screenshot-monitor` | 截图触发 OCR | | +| `size-optimization` | ABI 分包 + NDK 版本统一 | 仅影响 gradle 配置 | + +## 开发须知 + +1. 每个插件函数**必须 `return config`**,否则后续插件会崩溃 +2. 修改插件后需重新运行 `npx expo prebuild --platform android` +3. 原生服务通过 `NativeEventEmitter` 向 JS 层推送事件,**不直接写入**数据库或文件 +4. `ppocr` 依赖 `onnxruntime-android:1.20.0`,prebuild 后请验证版本 diff --git a/plugins/accessibility/android/AccessibilityBridgeModule.kt b/plugins/accessibility/android/AccessibilityBridgeModule.kt index feda700..8efa87b 100644 --- a/plugins/accessibility/android/AccessibilityBridgeModule.kt +++ b/plugins/accessibility/android/AccessibilityBridgeModule.kt @@ -1,5 +1,6 @@ package com.beancount.mobile.accessibility +import android.util.Log import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod @@ -7,6 +8,7 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap /** * 无障碍服务 RN 桥接模块(plan.md「3.6 无障碍服务」JS 接线)。 @@ -20,6 +22,10 @@ import com.facebook.react.bridge.ReadableArray class AccessibilityBridgeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + companion object { + private const val TAG = "AccessibilityBridge" + } + init { ReactContextHolder.context = reactContext } @@ -188,17 +194,18 @@ class AccessibilityBridgeModule(private val reactContext: ReactApplicationContex /** 显示账单浮窗(直接在当前其他应用上方渲染,不返回 App 内) */ @ReactMethod fun showFloatingBill( - amount: String, - merchant: String, - time: String, - packageName: String, + amount: String, + merchant: String, + time: String, + packageName: String, categories: ReadableArray, accounts: ReadableArray, direction: String, + currency: String, draftId: String, promise: Promise ) { - val context = reactContext.currentActivity ?: BillingAccessibilityService.instance + val context = BillingAccessibilityService.instance ?: reactContext.currentActivity if (context == null) { promise.reject("NO_CONTEXT", "无法获取当前前台 Activity 或 AccessibilityService 实例") return @@ -222,7 +229,7 @@ class AccessibilityBridgeModule(private val reactContext: ReactApplicationContex android.os.Handler(android.os.Looper.getMainLooper()).post { try { - val floatingView = FloatingBillView(context, draftId, amount, merchant, time, packageName, categoryList, accountList, direction) + val floatingView = FloatingBillView(context, draftId, amount, merchant, time, packageName, categoryList, accountList, direction, currency) floatingView.show() promise.resolve(true) } catch (e: Exception) { @@ -247,4 +254,71 @@ class AccessibilityBridgeModule(private val reactContext: ReactApplicationContex } promise.resolve(true) } + + /** 从 APK assets 复制 OCR 模型文件到 filesDir/ocr_models_v6。 */ + @ReactMethod + fun copyOcrModelsFromAssets(promise: Promise) { + try { + val destDir = java.io.File(reactContext.filesDir, "ocr_models_v6") + if (!destDir.exists()) destDir.mkdirs() + val assetManager = reactContext.assets + val files = listOf("ppocrv6_det.onnx", "ppocrv6_rec.onnx", "ppocrv6_dict.txt") + for (filename in files) { + val destFile = java.io.File(destDir, filename) + if (destFile.exists() && destFile.length() > 0) continue + assetManager.open(filename).use { input -> + java.io.FileOutputStream(destFile).use { output -> + input.copyTo(output) + } + } + } + promise.resolve(destDir.absolutePath) + } catch (e: Exception) { + promise.reject("COPY_MODEL_FAIL", e.message) + } + } + + /** 检查通知监听服务是否已启用(反射调用 getEnabledListenerPackages,避免 API level lint 错误)。 */ + @ReactMethod + fun isNotificationListenerEnabled(promise: Promise) { + try { + // 通过 Settings.Secure 读取系统设置(公开 API,无需反射,不受 hidden API 限制) + // enabled_notification_listeners 格式: + // "com.pkg1/com.pkg1.Service:com.pkg2/com.pkg2.Service" + val flat = android.provider.Settings.Secure.getString( + reactContext.contentResolver, + "enabled_notification_listeners" + ) ?: "" + val myPkg = reactContext.packageName + promise.resolve(flat.split(":").any { it.startsWith(myPkg) }) + } catch (e: Exception) { + Log.w(TAG, "isNotificationListenerEnabled 检测失败", e) + promise.resolve(false) + } + } + + /** 检查悬浮窗权限(SYSTEM_ALERT_WINDOW)是否已授予。 */ + @ReactMethod + fun canDrawOverlays(promise: Promise) { + try { + promise.resolve(android.provider.Settings.canDrawOverlays(reactContext)) + } catch (e: Exception) { + promise.resolve(false) + } + } + + /** + * 下发浮层 UI 配置(颜色 + 文案)。 + * JS 侧从 theme tokens + i18n 构建 config, + * 原生存 SharedPreferences,三个浮层组件读取。 + */ + @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) + } + } } diff --git a/plugins/accessibility/android/BillingAccessibilityService.kt b/plugins/accessibility/android/BillingAccessibilityService.kt index 938230e..c9ee4ef 100644 --- a/plugins/accessibility/android/BillingAccessibilityService.kt +++ b/plugins/accessibility/android/BillingAccessibilityService.kt @@ -192,7 +192,7 @@ class BillingAccessibilityService : AccessibilityService() { handler.post { val shouldShow = floatingBallEnabled && pkg != null - && !filterPackage(pkg, "") + && !filterPackage(pkg, topActivity ?: "") && !isLandscape() if (shouldShow) { @@ -380,11 +380,12 @@ class BillingAccessibilityService : AccessibilityService() { val pkg = topPackage ?: return val activity = topActivity ?: "" val sig = "$pkg|$activity" + val uiLabels = FloatingUiConfigStore.load(this).labels if (pageSignatures.add(sig)) { savePageSignatures() Log.i(TAG, "已记住页面: $sig") handler.post { - android.widget.Toast.makeText(this, "已记住页面签名:\n$sig", android.widget.Toast.LENGTH_LONG).show() + android.widget.Toast.makeText(this, "${uiLabels.pageRemembered}:\n$sig", android.widget.Toast.LENGTH_LONG).show() } // 抓取并提取屏幕所有无障碍文本 @@ -416,7 +417,7 @@ class BillingAccessibilityService : AccessibilityService() { } } else { handler.post { - android.widget.Toast.makeText(this, "该页面签名已存在:\n$sig", android.widget.Toast.LENGTH_LONG).show() + android.widget.Toast.makeText(this, "${uiLabels.pageSignatureExists}:\n$sig", android.widget.Toast.LENGTH_LONG).show() } } } @@ -507,6 +508,7 @@ class BillingAccessibilityService : AccessibilityService() { val p = pkg.lowercase() // 自身 App:只放行 MainActivity,避免悬浮球容器触发自毁式关闭 if (pkg == packageName) { + if (className.isEmpty()) return false return !className.endsWith(".MainActivity") } // 桌面启动器(悬浮球在桌面无意义) diff --git a/plugins/accessibility/android/FloatingBillView.kt b/plugins/accessibility/android/FloatingBillView.kt index 686b112..c6aa322 100644 --- a/plugins/accessibility/android/FloatingBillView.kt +++ b/plugins/accessibility/android/FloatingBillView.kt @@ -17,9 +17,11 @@ import android.widget.LinearLayout import android.text.InputType import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.modules.core.DeviceEventManagerModule +import java.math.BigDecimal /** * 浮窗账单提示(直接呈现高度优化、支持三方向切换的修改入账面板)。 + * P6 重设计:颜色/文案走 FloatingUiConfigStore,新增币种 chip,金额校验改正则+BigDecimal。 */ class FloatingBillView( private val context: Context, @@ -30,12 +32,27 @@ class FloatingBillView( private val packageName: String, private val categories: List> = emptyList(), private val accounts: List = emptyList(), - private val initialDirection: String = "expense" + private val initialDirection: String = "expense", + private val initialCurrency: String = "CNY" ) { companion object { private const val TAG = "FloatingBillView" + private val DEFAULT_CURRENCY_LIST = listOf("CNY", "USD", "HKD", "JPY", "EUR", "GBP") } + // 从 JS 下发的配置读取颜色与文案(缺失字段回退默认值,保证旧 JS 行为不变) + private val config = FloatingUiConfigStore.load(context) + + // 显示币种列表;若 initialCurrency 不在默认列表中则临时插入首位 + private val currencyList: List = run { + val list = DEFAULT_CURRENCY_LIST.toMutableList() + if (!list.contains(initialCurrency)) { + list.add(0, initialCurrency) + } + list + } + private var currentCurrency: String = initialCurrency + private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager private var view: View? = null private val handler = Handler(Looper.getMainLooper()) @@ -49,6 +66,20 @@ class FloatingBillView( private var accountContainer: LinearLayout? = null private var saveBtn: Button? = null + // 预解析常用颜色(parseColorOr 异常时回退默认值) + private val colorAccent = FloatingUiConfigStore.parseColorOr(config.colors.accent, "#FF5E6AD2") + private val colorAccentFg = FloatingUiConfigStore.parseColorOr(config.colors.accentFg, "#FFFFFFFF") + private val colorCardBg = FloatingUiConfigStore.parseColorOr(config.colors.cardBg, "#F0050506") + private val colorInputBg = FloatingUiConfigStore.parseColorOr(config.colors.inputBg, "#4012131A") + private val colorFgPrimary = FloatingUiConfigStore.parseColorOr(config.colors.fgPrimary, "#FFFFFFFF") + private val colorFgSecondary = FloatingUiConfigStore.parseColorOr(config.colors.fgSecondary, "#FF9CA3AF") + private val colorBorder = FloatingUiConfigStore.parseColorOr(config.colors.border, "#80222433") + private val colorIncome = FloatingUiConfigStore.parseColorOr(config.colors.income, "#FF10B981") + private val colorExpense = FloatingUiConfigStore.parseColorOr(config.colors.expense, "#FFE11D48") + private val colorTransfer = FloatingUiConfigStore.parseColorOr(config.colors.transfer, "#FF10B981") + + // 容器边框直接使用 config border(不再从 accent 派生 alpha) + private fun dp(value: Float): Int { val density = context.resources.displayMetrics.density return (value * density).toInt() @@ -74,29 +105,34 @@ class FloatingBillView( currentDirection = if (initialDirection == "income" || initialDirection == "transfer") initialDirection else "expense" // 1. 设置 Window 布局参数使其可聚焦(用以键盘输入)并贴靠屏幕底部 + val overlayType = if (context is android.accessibilityservice.AccessibilityService) { + WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY + } else { + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } val windowParams = WindowManager.LayoutParams( dp(310f), WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + overlayType, 0, // 0 标志代表可获取焦点 PixelFormat.TRANSLUCENT ).apply { gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL - y = dp(12f) // 尽量靠下以留出上方账单对照区 + y = dp(48f) // 留出底部导航栏空间 + 按钮/键盘安全区 } - // 2. 使用 55% 透明度 OLED 磨砂效果背景,发光靛蓝细边框 + // 2. 实色卡片背景(不再半透明磨砂),圆角 16dp,1dp 描边 val containerBg = GradientDrawable().apply { shape = GradientDrawable.RECTANGLE - cornerRadius = dp(14f).toFloat() - setColor(0x8C050506.toInt()) // 55% 透明度 OLED 黑色 - setStroke(dp(1.2f), 0x995E6AD2.toInt()) // 60% 透明度靛蓝发光边框 + cornerRadius = dp(16f).toFloat() + setColor(colorCardBg) + setStroke(dp(1f), colorBorder) // 使用 config border 颜色 } val container = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL background = containerBg - setPadding(dp(12f), dp(8f), dp(12f), dp(8f)) + setPadding(dp(16f), dp(16f), dp(16f), dp(16f)) layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT @@ -114,9 +150,9 @@ class FloatingBillView( } val titleText = TextView(context).apply { - text = "调整交易草稿" - textSize = 12f - setTextColor(0xFF98A2FF.toInt()) + text = config.labels.billTitle + textSize = 13f + setTextColor(colorFgPrimary) paint.isFakeBoldText = true layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) } @@ -126,12 +162,12 @@ class FloatingBillView( val segmentContainer = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL background = GradientDrawable().apply { - cornerRadius = dp(4f).toFloat() - setColor(0x20FFFFFF.toInt()) // 12% white opacity + cornerRadius = dp(6f).toFloat() + setColor((colorFgPrimary and 0x00FFFFFF) or 0x12000000) // 7% fg overlay } } - val tabTexts = listOf("支出", "收入", "转账") + val tabTexts = listOf(config.labels.dirExpense, config.labels.dirIncome, config.labels.dirTransfer) val tabDirections = listOf("expense", "income", "transfer") val tabViews = mutableListOf() @@ -139,11 +175,11 @@ class FloatingBillView( for (i in tabViews.indices) { val active = tabDirections[i] == currentDirection tabViews[i].apply { - setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + setTextColor(if (active) colorAccentFg else colorFgSecondary) background = if (active) { GradientDrawable().apply { - cornerRadius = dp(4f).toFloat() - setColor(0xFF5E6AD2.toInt()) // Indigo highlight + cornerRadius = dp(6f).toFloat() + setColor(colorAccent) } } else { null @@ -173,40 +209,75 @@ class FloatingBillView( headerLayout.addView(segmentContainer) container.addView(headerLayout) - // 金额编辑 + // 金额行:币种 chip(左)+ 金额输入框(右) val amountLabel = TextView(context).apply { - text = "金额" - textSize = 9f + text = config.labels.amountLabel + textSize = 10f paint.isFakeBoldText = true - setTextColor(0xFF98A2FF.toInt()) - setPadding(0, dp(4f), 0, dp(1f)) + setTextColor(colorFgSecondary) + setPadding(0, dp(8f), 0, dp(1f)) } container.addView(amountLabel) - val amountInput = EditText(context).apply { - textSize = 13f - setTextColor(0xFFFFFFFF.toInt()) - background = GradientDrawable().apply { - setColor(0x4012131A.toInt()) // 25% 半透底色 - cornerRadius = dp(6f).toFloat() - setStroke(dp(1f), 0x80222433.toInt()) - } - setPadding(dp(10f), dp(4f), dp(10f), dp(4f)) - inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + val amountRow = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) } - container.addView(amountInput) - container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(3f)) }) + + // 币种 chip + val currencyChip = TextView(context).apply { + text = currentCurrency + textSize = 11f + setTextColor(colorFgSecondary) + setPadding(dp(8f), dp(4f), dp(8f), dp(4f)) + gravity = Gravity.CENTER + background = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = dp(12f).toFloat() + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) + } + setOnClickListener { + val idx = currencyList.indexOf(currentCurrency) + currentCurrency = currencyList[(idx + 1) % currencyList.size] + text = currentCurrency + } + } + amountRow.addView(currencyChip) + + val amountInput = EditText(context).apply { + textSize = 13f + setTextColor(colorFgPrimary) + setHintTextColor(colorFgSecondary) + background = GradientDrawable().apply { + setColor(colorInputBg) + cornerRadius = dp(10f).toFloat() + setStroke(dp(1f), colorBorder) + } + setPadding(dp(10f), dp(6f), dp(10f), dp(6f)) + inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + layoutParams = LinearLayout.LayoutParams( + 0, + LinearLayout.LayoutParams.WRAP_CONTENT, + 1f + ).apply { + leftMargin = dp(8f) + } + } + amountRow.addView(amountInput) + container.addView(amountRow) + container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(4f)) }) // 商户编辑 val merchantLabel = TextView(context).apply { - text = "交易对手" - textSize = 9f + text = config.labels.payeeLabel + textSize = 10f paint.isFakeBoldText = true - setTextColor(0xFF98A2FF.toInt()) + setTextColor(colorFgSecondary) setPadding(0, 0, 0, dp(1f)) } container.addView(merchantLabel) @@ -214,56 +285,57 @@ class FloatingBillView( val merchantInput = EditText(context).apply { setText(merchant) textSize = 12f - setTextColor(0xFFFFFFFF.toInt()) + setTextColor(colorFgPrimary) + setHintTextColor(colorFgSecondary) background = GradientDrawable().apply { - setColor(0x4012131A.toInt()) - cornerRadius = dp(6f).toFloat() - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + cornerRadius = dp(10f).toFloat() + setStroke(dp(1f), colorBorder) } - setPadding(dp(10f), dp(4f), dp(10f), dp(4f)) + setPadding(dp(10f), dp(6f), dp(10f), dp(6f)) layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) } container.addView(merchantInput) - container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(3f)) }) + container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(4f)) }) // 叙述备注编辑 val narrationLabel = TextView(context).apply { - text = "描述/备注" - textSize = 9f + text = config.labels.narrationLabel + textSize = 10f paint.isFakeBoldText = true - setTextColor(0xFF98A2FF.toInt()) + setTextColor(colorFgSecondary) setPadding(0, 0, 0, dp(1f)) } container.addView(narrationLabel) val narrationInput = EditText(context).apply { - hint = "输入交易叙述" - setHintTextColor(0xFF6B7280.toInt()) + hint = config.labels.narrationHint + setHintTextColor(colorFgSecondary) textSize = 12f - setTextColor(0xFFFFFFFF.toInt()) + setTextColor(colorFgPrimary) background = GradientDrawable().apply { - setColor(0x4012131A.toInt()) - cornerRadius = dp(6f).toFloat() - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + cornerRadius = dp(10f).toFloat() + setStroke(dp(1f), colorBorder) } - setPadding(dp(10f), dp(4f), dp(10f), dp(4f)) + setPadding(dp(10f), dp(6f), dp(10f), dp(6f)) layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) } container.addView(narrationInput) - container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(4f)) }) + container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(6f)) }) // Row 1 分类/转入选择 categoryLabel = TextView(context).apply { - text = "交易分类" - textSize = 9f + text = config.labels.categoryExpense + textSize = 10f paint.isFakeBoldText = true - setTextColor(0xFF98A2FF.toInt()) + setTextColor(colorFgSecondary) setPadding(0, 0, 0, dp(2f)) } container.addView(categoryLabel) @@ -281,14 +353,14 @@ class FloatingBillView( ) } container.addView(categoryScroll) - container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(4f)) }) + container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(6f)) }) // Row 2 资金出入账户选择 accountLabel = TextView(context).apply { - text = "资金来源账户" - textSize = 9f + text = config.labels.accountExpense + textSize = 10f paint.isFakeBoldText = true - setTextColor(0xFF98A2FF.toInt()) + setTextColor(colorFgSecondary) setPadding(0, 0, 0, dp(2f)) } container.addView(accountLabel) @@ -306,7 +378,7 @@ class FloatingBillView( ) } container.addView(accountScroll) - container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(8f)) }) + container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(10f)) }) // 底部操作栏 val btnContainer = LinearLayout(context).apply { @@ -320,16 +392,16 @@ class FloatingBillView( // 1) 打开应用按钮 val openAppBtn = Button(context).apply { - text = "打开应用" - setTextColor(0xFFD1D5DB.toInt()) + text = config.labels.openApp + setTextColor(colorFgPrimary) background = GradientDrawable().apply { shape = GradientDrawable.RECTANGLE - cornerRadius = dp(8f).toFloat() - setColor(0xFF1F2937.toInt()) - setStroke(dp(1f), 0xFF374151.toInt()) + cornerRadius = dp(12f).toFloat() + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } textSize = 11f - layoutParams = LinearLayout.LayoutParams(0, dp(32f), 1f).apply { rightMargin = dp(6f) } + layoutParams = LinearLayout.LayoutParams(0, dp(40f), 1f).apply { rightMargin = dp(6f) } setOnClickListener { val newAmount = amountInput.text.toString().trim() val newPayee = merchantInput.text.toString().trim() @@ -342,18 +414,17 @@ class FloatingBillView( } // 2) 忽略按钮 - val cancelBg = GradientDrawable().apply { - shape = GradientDrawable.RECTANGLE - cornerRadius = dp(8f).toFloat() - setColor(0xFF1F2937.toInt()) - setStroke(dp(1f), 0xFF374151.toInt()) - } val cancelBtn = Button(context).apply { - text = "忽略" - setTextColor(0xFFD1D5DB.toInt()) - background = cancelBg + text = config.labels.dismiss + setTextColor(colorFgPrimary) + background = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = dp(12f).toFloat() + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) + } textSize = 11f - layoutParams = LinearLayout.LayoutParams(0, dp(32f), 1f).apply { rightMargin = dp(6f) } + layoutParams = LinearLayout.LayoutParams(0, dp(40f), 1f).apply { rightMargin = dp(6f) } setOnClickListener { sendCancelEvent() dismiss() @@ -361,18 +432,17 @@ class FloatingBillView( } // 3) 确认入账按钮 - val saveBg = GradientDrawable().apply { - shape = GradientDrawable.RECTANGLE - cornerRadius = dp(8f).toFloat() - setColor(0xFF5E6AD2.toInt()) - } saveBtn = Button(context).apply { - text = "确认入账" - setTextColor(0xFFFFFFFF.toInt()) - background = saveBg + text = config.labels.confirm + setTextColor(colorAccentFg) + background = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = dp(12f).toFloat() + setColor(colorAccent) + } textSize = 11f paint.isFakeBoldText = true - layoutParams = LinearLayout.LayoutParams(0, dp(32f), 1.3f) + layoutParams = LinearLayout.LayoutParams(0, dp(40f), 1.3f) setOnClickListener { val newAmount = amountInput.text.toString().trim() val newPayee = merchantInput.text.toString().trim() @@ -388,21 +458,23 @@ class FloatingBillView( btnContainer.addView(saveBtn) container.addView(btnContainer) - // 构建金额安全校验 + // 金额安全校验(BigDecimal + 正则) val amountWatcher = object : android.text.TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: android.text.Editable?) { try { val text = s?.toString()?.trim() ?: "" - val value = text.toDoubleOrNull() - val isValid = value != null && !value.isNaN() && !value.isInfinite() && value > 0.0 + val pattern = Regex("^\\d+(\\.\\d{1,2})?$") + val value = text.toBigDecimalOrNull() + val isValid = pattern.matches(text) && value != null && value > BigDecimal.ZERO saveBtn?.isEnabled = isValid saveBtn?.background = GradientDrawable().apply { - cornerRadius = dp(8f).toFloat() - setColor(if (isValid) 0xFF5E6AD2.toInt() else 0xFF374151.toInt()) + shape = GradientDrawable.RECTANGLE + cornerRadius = dp(12f).toFloat() + setColor(if (isValid) colorAccent else colorInputBg) } - saveBtn?.setTextColor(if (isValid) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + saveBtn?.setTextColor(if (isValid) colorAccentFg else colorFgSecondary) } catch (e: Exception) { Log.e(TAG, "金额校验异常", e) saveBtn?.isEnabled = false @@ -419,7 +491,7 @@ class FloatingBillView( view = container windowManager.addView(view, windowParams) - Log.i(TAG, "悬浮修改记账面板已显示: ¥$amount") + Log.i(TAG, "悬浮修改记账面板已显示: $currentCurrency $amount") // 用户一旦进行任何交互(触摸面板或获得输入焦点),立刻取消自动消失定时器 val cancelTimerListener = View.OnFocusChangeListener { _, hasFocus -> @@ -438,11 +510,11 @@ class FloatingBillView( false } - // 15 秒无操作自动消失(如果用户没有交互的话) + // 30 秒无操作自动消失(如果用户没有交互的话) handler.postDelayed({ sendCancelEvent() dismiss() - }, 15000L) + }, 30000L) } catch (e: Exception) { Log.e(TAG, "悬浮账单面板显示失败: ${e.message}", e) @@ -459,7 +531,7 @@ class FloatingBillView( // === 1. 绘制第一行 (分类 / 转入) === if (currentDirection == "expense") { - categoryLabel?.text = "交易分类" + categoryLabel?.text = config.labels.categoryExpense val filteredCats = categories.filter { it["type"] == "expense" } for (cat in filteredCats) { val catAccount = cat["account"] ?: "" @@ -470,14 +542,14 @@ class FloatingBillView( for (pair in categoryChips) { val active = pair.first == selected pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() - if (active) setColor(0xFF5E6AD2.toInt()) + cornerRadius = dp(14f).toFloat() + if (active) setColor(colorAccent) else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } @@ -492,18 +564,18 @@ class FloatingBillView( categoryChips.forEach { pair -> val active = pair.first == selectedCategoryAccount pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() - if (active) setColor(0xFF5E6AD2.toInt()) + cornerRadius = dp(14f).toFloat() + if (active) setColor(colorAccent) else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } else if (currentDirection == "income") { - categoryLabel?.text = "收入分类" + categoryLabel?.text = config.labels.categoryIncome val filteredCats = categories.filter { it["type"] == "income" } for (cat in filteredCats) { val catAccount = cat["account"] ?: "" @@ -514,14 +586,14 @@ class FloatingBillView( for (pair in categoryChips) { val active = pair.first == selected pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() - if (active) setColor(0xFF5E6AD2.toInt()) + cornerRadius = dp(14f).toFloat() + if (active) setColor(colorAccent) else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } @@ -536,19 +608,19 @@ class FloatingBillView( categoryChips.forEach { pair -> val active = pair.first == selectedCategoryAccount pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() - if (active) setColor(0xFF5E6AD2.toInt()) + cornerRadius = dp(14f).toFloat() + if (active) setColor(colorAccent) else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } else { - // transfer - categoryLabel?.text = "转入账户" + // transfer — 第一行 = 转入账户,选中色 = transfer + categoryLabel?.text = config.labels.transferTarget for (acct in accounts) { val shortName = acct.split(":").lastOrNull() ?: acct val chip = createChipView(shortName) @@ -557,14 +629,14 @@ class FloatingBillView( for (pair in categoryChips) { val active = pair.first == selected pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() - if (active) setColor(0xFF10B981.toInt()) + cornerRadius = dp(14f).toFloat() + if (active) setColor(colorTransfer) else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } @@ -579,26 +651,29 @@ class FloatingBillView( categoryChips.forEach { pair -> val active = pair.first == selectedCategoryAccount pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() - if (active) setColor(0xFF10B981.toInt()) + cornerRadius = dp(14f).toFloat() + if (active) setColor(colorTransfer) else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } // === 2. 绘制第二行 (资金账户) === if (currentDirection == "expense") { - accountLabel?.text = "资金来源" + accountLabel?.text = config.labels.accountExpense } else if (currentDirection == "income") { - accountLabel?.text = "存入账户" + accountLabel?.text = config.labels.accountIncome } else { - accountLabel?.text = "转出账户" + accountLabel?.text = config.labels.accountTransfer } + // 账户行选中色按方向:expense→expense色,income→income色,transfer→expense色 + val accountSelectedColor = if (currentDirection == "income") colorIncome else colorExpense + for (acct in accounts) { val shortName = acct.split(":").lastOrNull() ?: acct val chip = createChipView(shortName) @@ -607,15 +682,15 @@ class FloatingBillView( for (pair in accountChips) { val active = pair.first == selected pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() + cornerRadius = dp(14f).toFloat() if (active) { - setColor(if (currentDirection == "income") 0xFF10B981.toInt() else 0xFFE11D48.toInt()) + setColor(accountSelectedColor) } else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } @@ -631,14 +706,14 @@ class FloatingBillView( for (p in categoryChips) { val act = p.first == selectedCategoryAccount p.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() - if (act) setColor(0xFF10B981.toInt()) + cornerRadius = dp(14f).toFloat() + if (act) setColor(colorTransfer) else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - p.second.setTextColor(if (act) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + p.second.setTextColor(if (act) colorAccentFg else colorFgSecondary) } } } @@ -651,15 +726,15 @@ class FloatingBillView( accountChips.forEach { pair -> val active = pair.first == selectedSourceAccount pair.second.background = GradientDrawable().apply { - cornerRadius = dp(6f).toFloat() + cornerRadius = dp(14f).toFloat() if (active) { - setColor(if (currentDirection == "income") 0xFF10B981.toInt() else 0xFFE11D48.toInt()) + setColor(accountSelectedColor) } else { - setColor(0x4012131A.toInt()) - setStroke(dp(1f), 0x80222433.toInt()) + setColor(colorInputBg) + setStroke(dp(1f), colorBorder) } } - pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + pair.second.setTextColor(if (active) colorAccentFg else colorFgSecondary) } } @@ -677,6 +752,7 @@ class FloatingBillView( putString("time", time) putString("direction", currentDirection) putString("packageName", packageName) + putString("currency", currentCurrency) putBoolean("confirmed", true) putBoolean("editRequested", false) putBoolean("isManualEdit", true) @@ -703,6 +779,7 @@ class FloatingBillView( putString("time", time) putString("direction", currentDirection) putString("packageName", packageName) + putString("currency", currentCurrency) } reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) @@ -736,4 +813,13 @@ class FloatingBillView( } catch (_: Exception) {} view = null } + + /** Kotlin 中缺失的 toBigDecimalOrNull 扩展。 */ + private fun String.toBigDecimalOrNull(): BigDecimal? { + return try { + BigDecimal(this) + } catch (_: Exception) { + null + } + } } diff --git a/plugins/accessibility/android/FloatingHelper.kt b/plugins/accessibility/android/FloatingHelper.kt index 06b0ea8..e68d749 100644 --- a/plugins/accessibility/android/FloatingHelper.kt +++ b/plugins/accessibility/android/FloatingHelper.kt @@ -28,6 +28,9 @@ class FloatingHelper( ) { companion object { private const val TAG = "FloatingHelper" + private const val PREFS_NAME = "billing_accessibility_prefs" + private const val KEY_X = "floating_ball_x" + private const val KEY_Y = "floating_ball_y" private var lastX = 0 private var lastY = 400 } @@ -41,7 +44,7 @@ class FloatingHelper( private val params = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, PixelFormat.TRANSLUCENT ).apply { @@ -57,6 +60,25 @@ class FloatingHelper( try { val context = service + // 加载浮层 UI 配置(颜色与文案均从 JS 下发的 config 读取,未下发时回退默认值) + val config = FloatingUiConfigStore.load(service) + + // 预解析颜色 + val colorAccent = FloatingUiConfigStore.parseColorOr(config.colors.accent, "#FF5E6AD2") + val colorAccentFg = FloatingUiConfigStore.parseColorOr(config.colors.accentFg, "#FFFFFFFF") + val colorCardBg = FloatingUiConfigStore.parseColorOr(config.colors.cardBg, "#F0050506") + val colorFgPrimary = FloatingUiConfigStore.parseColorOr(config.colors.fgPrimary, "#FFFFFFFF") + val colorFgSecondary = FloatingUiConfigStore.parseColorOr(config.colors.fgSecondary, "#FF9CA3AF") + val colorBorder = FloatingUiConfigStore.parseColorOr(config.colors.border, "#80222433") + val colorInputBg = FloatingUiConfigStore.parseColorOr(config.colors.inputBg, "#4012131A") + + // 位置持久化:从 SharedPreferences 读取,回退到 companion 缓存 + val prefs = service.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + params.x = prefs.getInt(KEY_X, lastX) + params.y = prefs.getInt(KEY_Y, lastY) + lastX = params.x + lastY = params.y + val density = service.resources.displayMetrics.density fun dp(value: Float) = (value * density).toInt() @@ -79,25 +101,31 @@ class FloatingHelper( layoutParams = LinearLayout.LayoutParams(dp(24f), dp(60f)) } + // 边缘指示竖线:accent 色取 RGB + 固定 0xB0 alpha(保持 70% 透明) + val indicatorColor = (colorAccent and 0x00FFFFFF) or 0xB0000000.toInt() val indicatorView = View(context).apply { background = GradientDrawable().apply { shape = GradientDrawable.RECTANGLE - cornerRadius = dp(1.5f).toFloat() // 高度圆润 - setColor(0xB05E6AD2.toInt()) // 70% 高透靛蓝色,无边框 + cornerRadius = dp(1.5f).toFloat() + setColor(indicatorColor) } layoutParams = FrameLayout.LayoutParams(dp(3f), dp(44f)).apply { - gravity = Gravity.CENTER_VERTICAL or Gravity.START // 贴左边缘 + gravity = Gravity.CENTER_VERTICAL or Gravity.START } } bubbleLayout.addView(indicatorView) bubbleView = bubbleLayout // 3. 创建展开菜单 (垂直布局,高紧凑度设计) + // 菜单背景:实色 cardBg(per plan §T4) + val menuBgColor = colorCardBg + // 菜单描边:border 色 + 半透明 alpha + val menuBorderColor = (colorBorder and 0x00FFFFFF) or 0x22000000.toInt() val menuBg = GradientDrawable().apply { shape = GradientDrawable.RECTANGLE cornerRadius = dp(10f).toFloat() - setColor(0xA611131E.toInt()) // 65% OLED high-transparency black - setStroke(dp(0.8f), 0x22FFFFFF.toInt()) // subtle 13% white border + setColor(menuBgColor) + setStroke(dp(0.8f), menuBorderColor) } menuView = LinearLayout(context).apply { @@ -107,30 +135,37 @@ class FloatingHelper( setPadding(dp(4f), dp(4f), dp(4f), dp(4f)) visibility = View.GONE layoutParams = LinearLayout.LayoutParams( - dp(96f), // ultra-compact width: 96dp + dp(96f), LinearLayout.LayoutParams.WRAP_CONTENT ).apply { - leftMargin = dp(4f) // spacing with indicator line + leftMargin = dp(4f) } } // Vector icons val sizePx = dp(14f) val strokePx = dp(1.4f).toFloat() - val ocrIcon = ScanIconDrawable(0xFF818CF8.toInt(), strokePx, 0xFFF87171.toInt(), sizePx) - val pinIcon = PinIconDrawable(0xFF34D399.toInt(), strokePx, sizePx) + // ScanIconDrawable:accentFg 色(在 accent 底按钮上可见),laserColor 保留红色语义 + val ocrIcon = ScanIconDrawable(colorAccentFg, strokePx, 0xFFF87171.toInt(), sizePx) + // PinIconDrawable:fgSecondary 色 + val pinIcon = PinIconDrawable(colorFgSecondary, strokePx, sizePx) + + // Button 1: 识别账单(主操作:accent 底 accentFg 字) + // 背景常时:accent 色取 RGB + 固定 0x1F alpha(约 12%) + val btnOcrBgNormal = (colorAccent and 0x00FFFFFF) or 0x1F000000.toInt() + // 按下加深:accent 色取 RGB + 固定 0x40 alpha(约 25%) + val btnOcrBgPressed = (colorAccent and 0x00FFFFFF) or 0x40000000.toInt() - // Button 1: "识别账单" val btnOcr = TextView(context).apply { - text = "识别账单" - setTextColor(0xFFFFFFFF.toInt()) + text = config.labels.ballOcr + setTextColor(colorAccentFg) textSize = 11f paint.isFakeBoldText = true gravity = Gravity.CENTER_VERTICAL setPadding(dp(6f), 0, dp(6f), 0) background = GradientDrawable().apply { cornerRadius = dp(7f).toFloat() - setColor(0x1F5E6AD2.toInt()) // translucent indigo background + setColor(btnOcrBgNormal) } layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -145,13 +180,13 @@ class FloatingHelper( MotionEvent.ACTION_DOWN -> { view.background = GradientDrawable().apply { cornerRadius = dp(7f).toFloat() - setColor(0x405E6AD2.toInt()) + setColor(btnOcrBgPressed) } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { view.background = GradientDrawable().apply { cornerRadius = dp(7f).toFloat() - setColor(0x1F5E6AD2.toInt()) + setColor(btnOcrBgNormal) } if (event.action == MotionEvent.ACTION_UP) { view.performClick() @@ -165,17 +200,22 @@ class FloatingHelper( } } - // Button 2: "记住此页" + // Button 2: 记住此页(次要操作:inputBg 底 fgPrimary 字,per plan §T4) + // 背景常时:inputBg 色取 RGB + 固定 0x15 alpha + val btnRememberBgNormal = (colorInputBg and 0x00FFFFFF) or 0x15000000.toInt() + // 按下加深:inputBg 色取 RGB + 固定 0x30 alpha + val btnRememberBgPressed = (colorInputBg and 0x00FFFFFF) or 0x30000000.toInt() + val btnRemember = TextView(context).apply { - text = "记住此页" - setTextColor(0xFFE5E7EB.toInt()) + text = config.labels.ballRemember + setTextColor(colorFgPrimary) textSize = 11f paint.isFakeBoldText = true gravity = Gravity.CENTER_VERTICAL setPadding(dp(6f), 0, dp(6f), 0) background = GradientDrawable().apply { cornerRadius = dp(7f).toFloat() - setColor(0x15FFFFFF.toInt()) // subtle translucent gray + setColor(btnRememberBgNormal) } layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -188,13 +228,13 @@ class FloatingHelper( MotionEvent.ACTION_DOWN -> { view.background = GradientDrawable().apply { cornerRadius = dp(7f).toFloat() - setColor(0x30FFFFFF.toInt()) + setColor(btnRememberBgPressed) } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { view.background = GradientDrawable().apply { cornerRadius = dp(7f).toFloat() - setColor(0x15FFFFFF.toInt()) + setColor(btnRememberBgNormal) } if (event.action == MotionEvent.ACTION_UP) { view.performClick() @@ -206,9 +246,9 @@ class FloatingHelper( setOnClickListener { try { service.rememberCurrentPage() - FloatingTip(service, "📌 已将当前页面加入识别白名单!", FloatingTip.TipPosition.TOP, 2500L).show() + FloatingTip(service, config.labels.rememberSuccess, FloatingTip.TipPosition.TOP, 2500L).show() } catch (e: Exception) { - FloatingTip(service, "记录失败: ${e.message}", FloatingTip.TipPosition.TOP, 2500L).show() + FloatingTip(service, "${config.labels.rememberFail}: ${e.message}", FloatingTip.TipPosition.TOP, 2500L).show() } collapse() } @@ -246,7 +286,7 @@ class FloatingHelper( params.x = initialX + dx params.y = initialY + dy containerView?.let { windowManager.updateViewLayout(it, params) } - + lastX = params.x lastY = params.y true @@ -257,14 +297,20 @@ class FloatingHelper( } else { // 拖动抬起时自动吸附到屏幕边缘(指示器中心对齐边缘) if (params.x < service.resources.displayMetrics.widthPixels / 2) { - params.x = -dp(1.5f) // 指示器 3dp 宽,中心对齐边缘 + params.x = -dp(1.5f) } else { params.x = service.resources.displayMetrics.widthPixels - dp(24f) + dp(1.5f) } containerView?.let { windowManager.updateViewLayout(it, params) } - + lastX = params.x lastY = params.y + + // 位置持久化:吸附后写入 SharedPreferences + prefs.edit() + .putInt(KEY_X, params.x) + .putInt(KEY_Y, params.y) + .apply() } true } diff --git a/plugins/accessibility/android/FloatingTip.kt b/plugins/accessibility/android/FloatingTip.kt index 9640820..204b837 100644 --- a/plugins/accessibility/android/FloatingTip.kt +++ b/plugins/accessibility/android/FloatingTip.kt @@ -6,14 +6,10 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.view.Gravity -import android.view.LayoutInflater import android.view.View import android.view.WindowManager import android.widget.LinearLayout -import android.widget.ProgressBar import android.widget.TextView -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit /** * 浮窗提示(plan.md「3.8 浮窗账单提示」)。 @@ -47,6 +43,10 @@ class FloatingTip( /** 显示浮窗提示。 */ fun show() { try { + val config = FloatingUiConfigStore.load(context) + val bgColor = FloatingUiConfigStore.parseColorOr(config.colors.accent, "#FF000000") or 0xFF000000.toInt() // 强制不透明化 + val textColor = FloatingUiConfigStore.parseColorOr(config.colors.accentFg, "#FFFFFFFF") + val layoutParams = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, @@ -65,42 +65,20 @@ class FloatingTip( val container = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL - setBackgroundColor(0xF0333333.toInt()) + setBackgroundColor(bgColor) setPadding(32, 16, 32, 16) } val text = TextView(context).apply { text = message textSize = 13f - setTextColor(0xFFFFFFFF.toInt()) - } - // 倒计时进度条 - val progress = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal) - progress.max = 100 - progress.progress = 100 - progress.layoutParams = LinearLayout.LayoutParams(200, 8).apply { - setMargins(16, 0, 0, 0) + setTextColor(textColor) } container.addView(text) - container.addView(progress) view = container windowManager.addView(view, layoutParams) Log.d(TAG, "FloatingTip 显示: $message") - // 倒计时动画(300ms 内 progress 从 100 到 0) - val steps = 30 - val stepMs = durationMs / steps - var currentStep = 0 - handler.post(object : Runnable { - override fun run() { - currentStep++ - progress.progress = 100 - (currentStep * 100 / steps) - if (currentStep < steps) { - handler.postDelayed(this, stepMs) - } - } - }) - handler.postDelayed({ dismiss() }, durationMs) } catch (e: Exception) { Log.e(TAG, "FloatingTip 显示失败: ${e.message}") @@ -121,7 +99,7 @@ class FloatingTip( */ class RepeatToast(private val context: Context, private val message: String) { fun show() { - val tip = FloatingTip(context, "⚠ $message", FloatingTip.TipPosition.TOP, 2000L) + val tip = FloatingTip(context, message, FloatingTip.TipPosition.TOP, 2000L) tip.show() Log.d("RepeatToast", "重复提示: $message") } diff --git a/plugins/accessibility/android/FloatingUiConfigStore.kt b/plugins/accessibility/android/FloatingUiConfigStore.kt new file mode 100644 index 0000000..41e7689 --- /dev/null +++ b/plugins/accessibility/android/FloatingUiConfigStore.kt @@ -0,0 +1,211 @@ +package com.beancount.mobile.accessibility + +import android.content.Context +import android.graphics.Color +import android.util.Log +import com.facebook.react.bridge.ReadableMap +import org.json.JSONObject + +/** + * 浮层 UI 颜色契约(docs/ui-redesign-p6-plan.md「FloatingUiConfig 契约」)。 + * 字段名(camelCase)与 src/services/floatingUiConfig.ts 的 FloatingUiConfig.colors 一一对应。 + * 默认值 = 现状硬编码颜色(JS 未推送时行为不变),注释标明出处。 + */ +data class FloatingUiConfigColors( + /** 按钮/选中态背景。 */ val accent: String = "#FF5E6AD2", // 原 FloatingBillView saveBg / 分段选中态 + /** accent 上的文字色。 */ val accentFg: String = "#FFFFFFFF", // 原 FloatingBillView 按钮文字色 + /** 卡片底色(原 0x8C050506 半透明,默认实色化到 F0 保证可读)。 */ + val cardBg: String = "#F0050506", // 原 FloatingBillView containerBg 0x8C050506 + /** 输入框/未选中 chip 底色。 */ val inputBg: String = "#4012131A", // 原 FloatingBillView 输入框底 0x4012131A + val fgPrimary: String = "#FFFFFFFF", // 原 FloatingBillView 输入框文字色 + val fgSecondary: String = "#FF9CA3AF", // 原 FloatingBillView 未选中 tab/chip 文字色 + val border: String = "#80222433", // 原 FloatingBillView 输入框/chip 描边 0x80222433 + /** financial.income。 */ val income: String = "#FF10B981", // 原 FloatingBillView 收入/转账 chip 选中色 + /** financial.expense。 */ val expense: String = "#FFE11D48", // 原 FloatingBillView 支出账户 chip 选中色 + /** financial.transfer。 */ val transfer: String = "#FF10B981", // 原 FloatingBillView 转账转入 chip 选中色 +) + +/** + * 浮层 UI 文案契约。 + * 字段名(camelCase)与 src/services/floatingUiConfig.ts 的 FloatingUiConfig.labels 一一对应。 + * 默认值 = 现状中文硬编码文案(去 emoji)。 + */ +data class FloatingUiConfigLabels( + /** 浮窗标题。 */ val billTitle: String = "调整交易草稿", + val dirExpense: String = "支出", + val dirIncome: String = "收入", + val dirTransfer: String = "转账", + val amountLabel: String = "金额", + val payeeLabel: String = "交易对手", + val narrationLabel: String = "描述/备注", + val narrationHint: String = "输入交易叙述", + /** 支出方向分类行标签。 */ val categoryExpense: String = "交易分类", + /** 收入方向分类行标签。 */ val categoryIncome: String = "收入分类", + /** 转账方向第一行标签。 */ val transferTarget: String = "转入账户", + /** 支出方向账户行标签。 */ val accountExpense: String = "资金来源", + /** 收入方向账户行标签。 */ val accountIncome: String = "存入账户", + /** 转账方向账户行标签。 */ val accountTransfer: String = "转出账户", + val openApp: String = "打开应用", + val dismiss: String = "忽略", + val confirm: String = "确认入账", + /** 悬浮球「识别账单」。 */ val ballOcr: String = "识别账单", + /** 悬浮球「记住此页」。 */ val ballRemember: String = "记住此页", + /** 记住页面成功提示(不含 emoji)。 */ val rememberSuccess: String = "已将当前页面加入识别白名单", + /** 失败提示前缀(原生拼接 ': ' + e.message)。 */ val rememberFail: String = "记录失败", + /** BillingAccessibilityService Toast「已记住页面签名」前缀。 */ val pageRemembered: String = "已记住页面签名", + /** 「该页面签名已存在」前缀。 */ val pageSignatureExists: String = "该页面签名已存在", +) + +/** 原生浮层 UI 配置(JS 经 AccessibilityBridge.setFloatingUiConfig 下发)。 */ +data class FloatingUiConfig( + val colors: FloatingUiConfigColors = FloatingUiConfigColors(), + val labels: FloatingUiConfigLabels = FloatingUiConfigLabels(), +) + +/** + * FloatingUiConfig 的 SharedPreferences 持久化(plan「原生持久化」节)。 + * + * - 文件名 floating_ui_config,单键 config_json 存整个 JSON。 + * - save 采用合并策略:先 load 出现存 JSON,再覆盖传入键,避免部分更新丢字段。 + * - load 逐字段 optString 缺省回退 data class 默认值;任何异常返回全默认。 + */ +object FloatingUiConfigStore { + private const val TAG = "FloatingUiConfigStore" + private const val PREFS = "floating_ui_config" + private const val KEY = "config_json" + + /** 契约内已知 colors 键(save 时只写这些键)。 */ + private val COLOR_KEYS = listOf( + "accent", "accentFg", "cardBg", "inputBg", "fgPrimary", + "fgSecondary", "border", "income", "expense", "transfer", + ) + + /** 契约内已知 labels 键(save 时只写这些键)。 */ + private val LABEL_KEYS = listOf( + "billTitle", "dirExpense", "dirIncome", "dirTransfer", "amountLabel", + "payeeLabel", "narrationLabel", "narrationHint", "categoryExpense", + "categoryIncome", "transferTarget", "accountExpense", "accountIncome", + "accountTransfer", "openApp", "dismiss", "confirm", "ballOcr", + "ballRemember", "rememberSuccess", "rememberFail", "pageRemembered", + "pageSignatureExists", + ) + + /** 保存(合并):先读出现存 JSON,再覆盖传入的已知键。 */ + fun save(context: Context, map: ReadableMap) { + val root = readRawJson(context) + + if (map.hasKey("colors")) { + val colorsMap = map.getMap("colors") + val colorsJson = root.optJSONObject("colors") ?: JSONObject().also { root.put("colors", it) } + if (colorsMap != null) { + for (key in COLOR_KEYS) { + if (colorsMap.hasKey(key) && !colorsMap.isNull(key)) { + colorsJson.put(key, colorsMap.getString(key)) + } + } + } + } + + if (map.hasKey("labels")) { + val labelsMap = map.getMap("labels") + val labelsJson = root.optJSONObject("labels") ?: JSONObject().also { root.put("labels", it) } + if (labelsMap != null) { + for (key in LABEL_KEYS) { + if (labelsMap.hasKey(key) && !labelsMap.isNull(key)) { + labelsJson.put(key, labelsMap.getString(key)) + } + } + } + } + + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit() + .putString(KEY, root.toString()) + .apply() + } + + /** 读取配置;缺失字段回退默认值,任何异常返回全默认。 */ + fun load(context: Context): FloatingUiConfig { + return try { + val root = readRawJson(context) + val defaultColors = FloatingUiConfigColors() + val defaultLabels = FloatingUiConfigLabels() + val colorsJson = root.optJSONObject("colors") + val labelsJson = root.optJSONObject("labels") + + val colors = FloatingUiConfigColors( + accent = colorsJson.optStringOr("accent", defaultColors.accent), + accentFg = colorsJson.optStringOr("accentFg", defaultColors.accentFg), + cardBg = colorsJson.optStringOr("cardBg", defaultColors.cardBg), + inputBg = colorsJson.optStringOr("inputBg", defaultColors.inputBg), + fgPrimary = colorsJson.optStringOr("fgPrimary", defaultColors.fgPrimary), + fgSecondary = colorsJson.optStringOr("fgSecondary", defaultColors.fgSecondary), + border = colorsJson.optStringOr("border", defaultColors.border), + income = colorsJson.optStringOr("income", defaultColors.income), + expense = colorsJson.optStringOr("expense", defaultColors.expense), + transfer = colorsJson.optStringOr("transfer", defaultColors.transfer), + ) + val labels = FloatingUiConfigLabels( + billTitle = labelsJson.optStringOr("billTitle", defaultLabels.billTitle), + dirExpense = labelsJson.optStringOr("dirExpense", defaultLabels.dirExpense), + dirIncome = labelsJson.optStringOr("dirIncome", defaultLabels.dirIncome), + dirTransfer = labelsJson.optStringOr("dirTransfer", defaultLabels.dirTransfer), + amountLabel = labelsJson.optStringOr("amountLabel", defaultLabels.amountLabel), + payeeLabel = labelsJson.optStringOr("payeeLabel", defaultLabels.payeeLabel), + narrationLabel = labelsJson.optStringOr("narrationLabel", defaultLabels.narrationLabel), + narrationHint = labelsJson.optStringOr("narrationHint", defaultLabels.narrationHint), + categoryExpense = labelsJson.optStringOr("categoryExpense", defaultLabels.categoryExpense), + categoryIncome = labelsJson.optStringOr("categoryIncome", defaultLabels.categoryIncome), + transferTarget = labelsJson.optStringOr("transferTarget", defaultLabels.transferTarget), + accountExpense = labelsJson.optStringOr("accountExpense", defaultLabels.accountExpense), + accountIncome = labelsJson.optStringOr("accountIncome", defaultLabels.accountIncome), + accountTransfer = labelsJson.optStringOr("accountTransfer", defaultLabels.accountTransfer), + openApp = labelsJson.optStringOr("openApp", defaultLabels.openApp), + dismiss = labelsJson.optStringOr("dismiss", defaultLabels.dismiss), + confirm = labelsJson.optStringOr("confirm", defaultLabels.confirm), + ballOcr = labelsJson.optStringOr("ballOcr", defaultLabels.ballOcr), + ballRemember = labelsJson.optStringOr("ballRemember", defaultLabels.ballRemember), + rememberSuccess = labelsJson.optStringOr("rememberSuccess", defaultLabels.rememberSuccess), + rememberFail = labelsJson.optStringOr("rememberFail", defaultLabels.rememberFail), + pageRemembered = labelsJson.optStringOr("pageRemembered", defaultLabels.pageRemembered), + pageSignatureExists = labelsJson.optStringOr("pageSignatureExists", defaultLabels.pageSignatureExists), + ) + FloatingUiConfig(colors, labels) + } catch (e: Exception) { + Log.e(TAG, "读取浮层 UI 配置失败,回退默认值: ${e.message}") + FloatingUiConfig() + } + } + + /** 解析颜色字符串;失败/为空时解析 fallbackHex(fallback 也不合法则返回 Color.BLACK)。 */ + fun parseColorOr(value: String, fallbackHex: String): Int { + if (value.isNotBlank()) { + try { + return Color.parseColor(value) + } catch (_: Exception) {} + } + return try { + Color.parseColor(fallbackHex) + } catch (_: Exception) { + Color.BLACK + } + } + + /** 读出现存 JSON;无数据或解析失败返回空 JSONObject。 */ + private fun readRawJson(context: Context): JSONObject { + return try { + val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getString(KEY, null) + if (raw.isNullOrBlank()) JSONObject() else JSONObject(raw) + } catch (_: Exception) { + JSONObject() + } + } + + /** optString 包装:JSON 为 null / 键缺失 / 空串时回退 default。 */ + private fun JSONObject?.optStringOr(key: String, default: String): String { + if (this == null) return default + val v = optString(key, default) + return if (v.isBlank()) default else v + } +} diff --git a/plugins/accessibility/app.plugin.js b/plugins/accessibility/app.plugin.js index 78f412c..7f96025 100644 --- a/plugins/accessibility/app.plugin.js +++ b/plugins/accessibility/app.plugin.js @@ -121,12 +121,11 @@ function withAccessibilityService(config) { let content = modConfig.modResults.contents; // 2a. 注入 import(AccessibilityBridgePackage) - if (!content.includes(`import ${PACKAGE}.AccessibilityBridgePackage`)) { - content = content.replace( - /^(package\s+[\w.]+;?\s*)$/m, - `$1\nimport ${PACKAGE}.AccessibilityBridgePackage`, - ); - } + content = content.replace(/^import\s+[\w.]+\.AccessibilityBridgePackage\s*$/gm, ''); + content = content.replace( + /^(package\s+[\w.]+;?\s*)$/m, + `$1\nimport ${PACKAGE}.AccessibilityBridgePackage`, + ); // 2c. 在 getPackages() 的 .apply {} 块里注入 add(AccessibilityBridgePackage()) if (!content.includes('add(AccessibilityBridgePackage())')) { @@ -166,7 +165,7 @@ function withAccessibilityService(config) { $: { 'android:name': `${PACKAGE}.BillingAccessibilityService`, 'android:permission': 'android.permission.BIND_ACCESSIBILITY_SERVICE', - 'android:label': '账单识别', + 'android:label': '浮记-账单识别', 'android:exported': 'false', }, 'intent-filter': [{ diff --git a/plugins/ppocr/README.md b/plugins/ppocr/README.md index a8c969e..12b14fd 100644 --- a/plugins/ppocr/README.md +++ b/plugins/ppocr/README.md @@ -8,11 +8,11 @@ **PP-OCRv6 small**(det 2.5M 参数 + rec 5.3M 参数,精度显著优于 v5 mobile) -| 指标 | PP-OCRv5 mobile | PP-OCRv6 small | -|------|----------------|----------------| -| det Hmean | 75.2% | 84.1% | -| rec W-Avg | 73.7% | 81.3% | -| rec ONNX 大小 | ~17 MB | ~21 MB | +| 指标 | PP-OCRv5 mobile | PP-OCRv6 small | +| ------------- | --------------- | -------------- | +| det Hmean | 75.2% | 84.1% | +| rec W-Avg | 73.7% | 81.3% | +| rec ONNX 大小 | ~17 MB | ~21 MB | ## 文件结构 @@ -55,14 +55,14 @@ curl -L -o ppocrv6_dict.txt \ ## 性能配置(参考 AutoAccounting OcrProcessor.kt) -| 优化项 | 配置 | -|--------|------| -| 引擎 | ONNX Runtime Android | -| 执行器 | CPU(兼容性最稳,部分设备 GPU 会崩溃) | -| 线程 | intraOp=2 / interOp=2 | -| det 图像 | 最大边 960px,短边压缩 720px | -| rec 图像 | 固定高度 48px | -| ABI | arm64-v8a(主流设备) | +| 优化项 | 配置 | +| -------- | -------------------------------------- | +| 引擎 | ONNX Runtime Android | +| 执行器 | CPU(兼容性最稳,部分设备 GPU 会崩溃) | +| 线程 | intraOp=2 / interOp=2 | +| det 图像 | 最大边 960px,短边压缩 720px | +| rec 图像 | 固定高度 48px | +| ABI | arm64-v8a(主流设备) | ## 使用 diff --git a/plugins/ppocr/android/OcrModule.kt b/plugins/ppocr/android/OcrModule.kt index 3c58a80..b5fa418 100644 --- a/plugins/ppocr/android/OcrModule.kt +++ b/plugins/ppocr/android/OcrModule.kt @@ -62,6 +62,9 @@ class OcrModule(private val context: ReactApplicationContext) : @Volatile private var initialized = false @Volatile private var initFailed = false + /** 模型文件目录(filesystem 绝对路径)。非空时从该目录加载模型,否则回退到 assets。 */ + @Volatile private var modelDir: String? = null + override fun getName(): String = OCR_MODULE_NAME override fun initialize() { @@ -70,7 +73,7 @@ class OcrModule(private val context: ReactApplicationContext) : scope.launch { initEngine() } } - /** 从 assets 加载 det/rec ONNX 模型与字典。 */ + /** 从 assets 或 filesystem 加载 det/rec ONNX 模型与字典。 */ private fun initEngine() { lock.lock() try { @@ -83,11 +86,29 @@ class OcrModule(private val context: ReactApplicationContext) : // 移动端关闭内存优化里的图优化级别过高(部分模型会崩) setOptimizationLevel(OrtSession.SessionOptions.OptLevel.BASIC_OPT) } - val detBytes = context.assets.open(ASSET_DET_MODEL).use { it.readBytes() } - val recBytes = context.assets.open(ASSET_REC_MODEL).use { it.readBytes() } - val det = env.createSession(detBytes, opts) - val rec = env.createSession(recBytes, opts) - val dict = loadDictionary() + val dir = modelDir + val (det, rec, dict) = if (dir != null) { + // 从 filesystem 加载(P8:模型按需下载到本地目录) + val detPath = dir + java.io.File.separator + ASSET_DET_MODEL + val recPath = dir + java.io.File.separator + ASSET_REC_MODEL + val dictPath = dir + java.io.File.separator + ASSET_DICT + Log.i(OCR_MODULE_NAME, "从 filesystem 加载模型: det=$detPath, rec=$recPath") + Triple( + env.createSession(detPath, opts), + env.createSession(recPath, opts), + loadDictionaryFromFile(dictPath) + ) + } else { + // 回退:从 assets 加载(兼容未迁移的老用户) + val detBytes = context.assets.open(ASSET_DET_MODEL).use { it.readBytes() } + val recBytes = context.assets.open(ASSET_REC_MODEL).use { it.readBytes() } + Log.i(OCR_MODULE_NAME, "从 assets 加载模型(兼容模式)") + Triple( + env.createSession(detBytes, opts), + env.createSession(recBytes, opts), + loadDictionaryFromAssets() + ) + } detSession = det recSession = rec @@ -98,7 +119,12 @@ class OcrModule(private val context: ReactApplicationContext) : } catch (e: Exception) { initFailed = true Log.e(OCR_MODULE_NAME, "OCR 初始化失败: ${e.message}", e) - Log.e(OCR_MODULE_NAME, "请确认 assets 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT") + val dir = modelDir + if (dir != null) { + Log.e(OCR_MODULE_NAME, "请确认 $dir 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT") + } else { + Log.e(OCR_MODULE_NAME, "请确认 assets 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT") + } } finally { lock.unlock() } @@ -108,13 +134,13 @@ class OcrModule(private val context: ReactApplicationContext) : private val recEnv: OrtEnvironment? get() = ortEnv /** - * 加载 ppocrv6_dict.txt 字典。 + * 从 assets 加载 ppocrv6_dict.txt 字典。 * * PaddleOCR CTC 约定:模型输出 logits 的 index 0 是 blank,字符从 index 1 开始; * 字典条目 dictionary[i] 对应模型输出 index i+1。解码时 dictIdx = argmaxIdx - 1。 * 字典本身不含 blank,运行时固定用 index 0 作 blank(见 ctcGreedyDecode)。 */ - private fun loadDictionary(): List { + private fun loadDictionaryFromAssets(): List { val words = mutableListOf() context.assets.open(ASSET_DICT).use { stream -> BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).useLines { lines -> @@ -127,6 +153,17 @@ class OcrModule(private val context: ReactApplicationContext) : return words } + /** 从 filesystem 路径加载字典文件。 */ + private fun loadDictionaryFromFile(path: String): List { + val words = mutableListOf() + java.io.File(path).bufferedReader(Charsets.UTF_8).useLines { lines -> + lines.forEach { line -> + words.add(line.trimEnd('\r', '\n')) + } + } + return words + } + /** * 识别图片文本。 * @param imageBase64 base64 编码的图片(JPEG/PNG) @@ -209,6 +246,18 @@ class OcrModule(private val context: ReactApplicationContext) : promise.resolve(initialized) } + /** 设置模型文件目录(绝对路径)。若引擎已初始化则释放并重新加载。 */ + @ReactMethod + fun setModelDir(dir: String, promise: Promise) { + modelDir = dir + if (initialized) { + release() + initFailed = false + scope.launch { initEngine() } + } + promise.resolve(true) + } + // ============== 推理流水线 ============== private fun ensureReady() { diff --git a/plugins/ppocr/app.plugin.js b/plugins/ppocr/app.plugin.js index 957d447..b2837c1 100644 --- a/plugins/ppocr/app.plugin.js +++ b/plugins/ppocr/app.plugin.js @@ -96,12 +96,11 @@ function withPpOcr(config) { let content = modConfig.modResults.contents; // 3a. 注入 import(在 package 声明行后插入) - if (!content.includes(`import ${PACKAGE}.OcrPackage`)) { - content = content.replace( - /^(package\s+[\w.]+;?\s*)$/m, - `$1\nimport ${PACKAGE}.OcrPackage`, - ); - } + content = content.replace(/^import\s+[\w.]+\.OcrPackage\s*$/gm, ''); + content = content.replace( + /^(package\s+[\w.]+;?\s*)$/m, + `$1\nimport ${PACKAGE}.OcrPackage`, + ); // 3b. 在 getPackages() 的 .apply {} 块里注入 add(OcrPackage()) if (!content.includes('add(OcrPackage())')) { diff --git a/plugins/ppocr/assets/ppocrv6_det.onnx b/plugins/ppocr/assets/ppocrv6_det.onnx deleted file mode 100644 index 15b8ac4..0000000 Binary files a/plugins/ppocr/assets/ppocrv6_det.onnx and /dev/null differ diff --git a/plugins/ppocr/assets/ppocrv6_dict.txt b/plugins/ppocr/assets/ppocrv6_dict.txt deleted file mode 100644 index 270082a..0000000 --- a/plugins/ppocr/assets/ppocrv6_dict.txt +++ /dev/null @@ -1,18708 +0,0 @@ -! -" -# -$ -% -& -' -( -) -* -+ -, -- -. -/ -: -; -< -= -> -? -@ -[ -\ -] -^ -_ -` -{ -| -} -~ -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F -G -H -I -J -K -L -M -N -O -P -Q -R -S -T -U -V -W -X -Y -Z -a -b -c -d -e -f -g -h -i -j -k -l -m -n -o -p -q -r -s -t -u -v -w -x -y -z -¢ -£ -¤ -¥ -¦ -§ -¨ -© -ª -« -¬ -® -¯ -° -± -² -³ -´ -µ -¶ -· -¸ -¹ -º -» -¼ -½ -¾ -¿ -À -Á - -à -Ä -Å -Æ -Ç -È -É -Ê -Ë -Ì -Í -Î -Ï -Ð -Ñ -Ò -Ó -Ô -Õ -Ö -× -Ø -Ù -Ú -Û -Ü -Ý -Þ -ß -à -á -â -ã -ä -å -æ -ç -è -é -ê -ë -ì -í -î -ï -ð -ñ -ò -ó -ô -õ -ö -÷ -ø -ù -ú -û -ü -ý -þ -ÿ -Ā -ā -Ă -ă -Ą -ą -Ć -ć -Ĉ -ĉ -Ċ -ċ -Č -č -Ď -ď -Đ -đ -Ē -ē -Ĕ -ĕ -Ė -ė -Ę -ę -Ě -ě -Ĝ -ĝ -Ğ -ğ -Ġ -ġ -Ģ -ģ -Ĥ -ĥ -Ħ -ħ -Ĩ -ĩ -Ī -ī -Ĭ -ĭ -Į -į -İ -ı -IJ -ij -Ĵ -ĵ -Ķ -ķ -ĸ -Ĺ -ĺ -Ļ -ļ -Ľ -ľ -Ŀ -ŀ -Ł -ł -Ń -ń -Ņ -ņ -Ň -ň -ʼn -Ŋ -ŋ -Ō -ō -Ŏ -ŏ -Ő -ő -Œ -œ -Ŕ -ŕ -Ŗ -ŗ -Ř -ř -Ś -ś -Ŝ -ŝ -Ş -ş -Š -š -Ţ -ţ -Ť -ť -Ŧ -ŧ -Ũ -ũ -Ū -ū -Ŭ -ŭ -Ů -ů -Ű -ű -Ų -ų -Ŵ -ŵ -Ŷ -ŷ -Ÿ -Ź -ź -Ż -ż -Ž -ž -ſ -ƀ -Ɓ -Ƃ -ƃ -Ƅ -ƅ -Ɔ -Ƈ -ƈ -Ɖ -Ɗ -Ƌ -ƌ -ƍ -Ǝ -Ə -Ɛ -Ƒ -ƒ -Ɠ -Ɣ -ƕ -Ɩ -Ɨ -Ƙ -ƙ -ƚ -ƛ -Ɯ -Ɲ -ƞ -Ɵ -Ơ -ơ -Ƣ -ƣ -Ƥ -ƥ -Ʀ -Ƨ -ƨ -Ʃ -ƪ -ƫ -Ƭ -ƭ -Ʈ -Ư -ư -Ʊ -Ʋ -Ƴ -ƴ -Ƶ -ƶ -Ʒ -Ƹ -ƹ -ƺ -ƻ -Ƽ -ƽ -ƾ -ƿ -ǀ -ǁ -ǂ -ǃ -DŽ -Dž -dž -LJ -Lj -lj -NJ -Nj -nj -Ǎ -ǎ -Ǐ -ǐ -Ǒ -ǒ -Ǔ -ǔ -Ǖ -ǖ -Ǘ -ǘ -Ǚ -ǚ -Ǜ -ǜ -ǝ -Ǟ -ǟ -Ǡ -ǡ -Ǣ -ǣ -Ǥ -ǥ -Ǧ -ǧ -Ǩ -ǩ -Ǫ -ǫ -Ǭ -ǭ -Ǯ -ǯ -ǰ -DZ -Dz -dz -Ǵ -ǵ -Ƕ -Ƿ -Ǹ -ǹ -Ǻ -ǻ -Ǽ -ǽ -Ǿ -ǿ -Ȁ -ȁ -Ȃ -ȃ -Ȅ -ȅ -Ȇ -ȇ -Ȉ -ȉ -Ȋ -ȋ -Ȍ -ȍ -Ȏ -ȏ -Ȑ -ȑ -Ȓ -ȓ -Ȕ -ȕ -Ȗ -ȗ -Ș -ș -Ț -ț -Ȝ -ȝ -Ȟ -ȟ -Ƞ -ȡ -Ȣ -ȣ -Ȥ -ȥ -Ȧ -ȧ -Ȩ -ȩ -Ȫ -ȫ -Ȭ -ȭ -Ȯ -ȯ -Ȱ -ȱ -Ȳ -ȳ -ȴ -ȵ -ȶ -ȷ -ȸ -ȹ -Ⱥ -Ȼ -ȼ -Ƚ -Ⱦ -ȿ -ɀ -Ɂ -ɂ -Ƀ -Ʉ -Ʌ -Ɇ -ɇ -Ɉ -ɉ -Ɋ -ɋ -Ɍ -ɍ -Ɏ -ɏ -ɐ -ɑ -ɒ -ɓ -ɔ -ɕ -ɖ -ɗ -ɘ -ə -ɚ -ɛ -ɜ -ɝ -ɞ -ɟ -ɠ -ɡ -ɢ -ɣ -ɤ -ɥ -ɦ -ɧ -ɨ -ɩ -ɪ -ɫ -ɬ -ɭ -ɮ -ɯ -ɰ -ɱ -ɲ -ɳ -ɴ -ɵ -ɶ -ɷ -ɸ -ɹ -ɺ -ɻ -ɼ -ɽ -ɾ -ɿ -ʀ -ʁ -ʂ -ʃ -ʄ -ʅ -ʆ -ʇ -ʈ -ʉ -ʊ -ʋ -ʌ -ʍ -ʎ -ʏ -ʐ -ʑ -ʒ -ʓ -ʔ -ʕ -ʖ -ʗ -ʘ -ʙ -ʚ -ʛ -ʜ -ʝ -ʞ -ʟ -ʠ -ʡ -ʢ -ʣ -ʤ -ʥ -ʦ -ʧ -ʨ -ʩ -ʪ -ʫ -ʬ -ʭ -ʮ -ʯ -Α -Β -Γ -Δ -Ε -Ζ -Η -Θ -Ι -Κ -Λ -Μ -Ν -Ξ -Ο -Π -Ρ -Σ -Τ -Υ -Φ -Χ -Ψ -Ω -α -β -γ -δ -ε -ζ -η -θ -ι -κ -λ -μ -ν -ξ -ο -π -ρ -σ -τ -υ -φ -χ -ψ -ω -ϐ -ϑ -ϒ -ϕ -ϖ -ϝ -Ϟ -ϟ -Ϡ -ϡ -Ϣ -ϣ -Ϥ -ϥ -Ϧ -ϧ -Ϩ -ϩ -Ϫ -ϫ -Ϭ -ϭ -Ϯ -ϯ -ϰ -ϱ -ϴ -ϵ -Ạ -ồ -– -— -― -‖ -‘ -’ -“ -” -† -‡ -• -‥ -… -‰ -′ -″ -※ -⁎ -⁰ -⁴ -⁵ -⁶ -⁷ -⁸ -⁹ -⁺ -⁻ -⁼ -⁽ -⁾ -ⁿ -₀ -₁ -₂ -₃ -₄ -₅ -₆ -₇ -₈ -₉ -₊ -₋ -₌ -₍ -₎ -ₒ -₠ -₡ -₢ -₣ -₤ -₥ -₦ -₧ -₨ -₩ -₪ -₫ -€ -₭ -₮ -₯ -₰ -₱ -₲ -₳ -₴ -₵ -₶ -₷ -₸ -₹ -₺ -₻ -₼ -₽ -₾ -₿ -℃ -℉ -ℎ -ℏ -ℑ -℘ -ℜ -™ -℧ -Å -ℵ -ℶ -ℷ -ℸ -ℹ -⅀ -Ⅰ -Ⅱ -Ⅲ -Ⅳ -Ⅴ -Ⅵ -Ⅶ -Ⅷ -Ⅸ -Ⅹ -Ⅺ -Ⅻ -ⅰ -ⅱ -ⅲ -ⅳ -ⅴ -ⅵ -ⅶ -ⅷ -ⅸ -ⅹ -ⅺ -ⅻ -ⅼ -ↀ -ↁ -ↂ -← -↑ -→ -↓ -↔ -↕ -↖ -↗ -↘ -↙ -↚ -↛ -↜ -↝ -↞ -↟ -↠ -↡ -↢ -↣ -↤ -↥ -↦ -↧ -↨ -↩ -↪ -↫ -↬ -↭ -↮ -↯ -↰ -↱ -↲ -↳ -↴ -↵ -↶ -↷ -↸ -↹ -↺ -↻ -↼ -↽ -↾ -↿ -⇀ -⇁ -⇂ -⇃ -⇄ -⇅ -⇆ -⇇ -⇈ -⇉ -⇊ -⇋ -⇌ -⇍ -⇎ -⇏ -⇐ -⇑ -⇒ -⇓ -⇔ -⇕ -⇖ -⇗ -⇘ -⇙ -⇚ -⇛ -⇜ -⇝ -⇞ -⇟ -⇠ -⇡ -⇢ -⇣ -⇤ -⇥ -⇦ -⇧ -⇨ -⇩ -⇪ -⇫ -⇬ -⇭ -⇮ -⇯ -⇰ -⇱ -⇲ -⇳ -⇴ -⇵ -⇶ -⇷ -⇸ -⇹ -⇺ -⇻ -⇼ -⇽ -⇾ -⇿ -∀ -∁ -∂ -∃ -∄ -∅ -∆ -∇ -∈ -∉ -∋ -∏ -∑ -− -∓ -∕ -∖ -∙ -√ -∛ -∜ -∝ -∞ -∟ -∠ -∡ -∢ -∥ -∧ -∨ -∩ -∪ -∫ -∬ -∭ -∮ -∯ -∰ -∱ -∲ -∳ -∴ -∵ -∶ -∷ -∸ -∹ -∺ -∻ -∼ -∽ -∾ -∿ -≀ -≁ -≂ -≃ -≄ -≅ -≆ -≇ -≈ -≉ -≊ -≋ -≌ -≍ -≎ -≏ -≐ -≑ -≒ -≓ -≔ -≕ -≖ -≗ -≘ -≙ -≚ -≛ -≜ -≝ -≞ -≟ -≠ -≡ -≢ -≣ -≤ -≥ -≦ -≧ -≨ -≩ -≪ -≫ -≬ -≭ -≮ -≯ -≰ -≱ -≲ -≳ -≴ -≵ -≶ -≷ -≸ -≹ -≺ -≻ -≼ -≽ -≾ -≿ -⊀ -⊁ -⊂ -⊃ -⊄ -⊅ -⊆ -⊇ -⊈ -⊉ -⊊ -⊋ -⊌ -⊍ -⊎ -⊏ -⊐ -⊑ -⊒ -⊓ -⊔ -⊕ -⊖ -⊗ -⊘ -⊙ -⊚ -⊛ -⊜ -⊝ -⊞ -⊟ -⊠ -⊡ -⊢ -⊣ -⊤ -⊥ -⊦ -⊧ -⊨ -⊩ -⊪ -⊫ -⊬ -⊭ -⊮ -⊯ -⊰ -⊱ -⊲ -⊳ -⊴ -⊵ -⊶ -⊷ -⊸ -⊹ -⊺ -⊻ -⊼ -⊽ -⊾ -⊿ -⋅ -⌀ -⌃ -⌘ -⌚ -⌛ -⌤ -⌥ -⌦ -⌧ -⌨ -〉 -⌫ -⌬ -⌭ -⌮ -⌯ -⍵ -⍺ -⎆ -⎇ -⎈ -⎉ -⎊ -⎋ -⎌ -⎍ -⎎ -⎏ -⎐ -⎑ -⎒ -⎓ -⎔ -⎕ -⎖ -⎗ -⎘ -⎙ -⎚ -⎛ -⎜ -⎝ -⎞ -⎟ -⎠ -⎡ -⎢ -⎣ -⎤ -⎥ -⎦ -⎧ -⎨ -⎩ -⎪ -⎫ -⎬ -⎭ -⎮ -⎯ -⎰ -⎱ -⎲ -⎳ -⎴ -⎵ -⎶ -⎷ -⎸ -⎹ -⎺ -⎻ -⎼ -⎽ -⎾ -⎿ -⏀ -⏁ -⏂ -⏃ -⏄ -⏅ -⏆ -⏇ -⏈ -⏉ -⏊ -⏋ -⏌ -⏍ -⏎ -⏏ -⏐ -⏑ -⏒ -⏓ -⏔ -⏕ -⏖ -⏗ -⏘ -⏙ -⏚ -⏛ -⏜ -⏝ -⏞ -⏟ -⏠ -⏡ -⏢ -⏣ -⏤ -⏥ -⏦ -⏧ -⏨ -⏩ -⏪ -⏫ -⏬ -⏭ -⏮ -⏯ -⏰ -⏱ -⏲ -⏳ -⏴ -⏵ -⏶ -⏷ -⏸ -⏹ -⏺ -⏻ -⏼ -⏽ -⏾ -⏿ -① -② -③ -④ -⑤ -⑥ -⑦ -⑧ -⑨ -⑩ -⑪ -⑫ -⑬ -⑭ -⑮ -⑯ -⑰ -⑱ -⑲ -⑳ -Ⓜ -━ -╆ -╳ -▁ -█ -■ -□ -▪ -▫ -▬ -▲ -△ -▶ -▷ -▼ -▽ -◀ -◆ -◇ -◉ -◊ -○ -◎ -● -◐ -◥ -◻ -◼ -◽ -◾ -☀ -☁ -☂ -☃ -☄ -★ -☆ -☇ -☈ -☉ -☊ -☋ -☌ -☍ -☎ -☏ -☐ -☑ -☒ -☓ -☔ -☕ -☖ -☗ -☘ -☙ -☚ -☛ -☜ -☝ -☞ -☟ -☠ -☡ -☢ -☣ -☤ -☥ -☦ -☧ -☨ -☩ -☪ -☫ -☬ -☭ -☮ -☯ -☰ -☱ -☲ -☳ -☴ -☵ -☶ -☷ -☸ -☹ -☺ -☻ -☼ -☽ -☾ -☿ -♀ -♁ -♂ -♃ -♄ -♅ -♆ -♇ -♈ -♉ -♊ -♋ -♌ -♍ -♎ -♏ -♐ -♑ -♒ -♓ -♔ -♕ -♖ -♗ -♘ -♙ -♚ -♛ -♜ -♝ -♞ -♟ -♠ -♡ -♢ -♣ -♤ -♥ -♦ -♧ -♨ -♩ -♪ -♫ -♬ -♭ -♮ -♯ -♰ -♱ -♲ -♳ -♴ -♵ -♶ -♷ -♸ -♹ -♺ -♻ -♼ -♽ -♾ -♿ -⚀ -⚁ -⚂ -⚃ -⚄ -⚅ -⚆ -⚇ -⚈ -⚉ -⚊ -⚋ -⚌ -⚍ -⚎ -⚏ -⚐ -⚑ -⚒ -⚓ -⚔ -⚕ -⚖ -⚗ -⚘ -⚙ -⚚ -⚛ -⚜ -⚝ -⚞ -⚟ -⚠ -⚡ -⚢ -⚣ -⚤ -⚥ -⚦ -⚧ -⚨ -⚩ -⚪ -⚫ -⚬ -⚭ -⚮ -⚯ -⚰ -⚱ -⚲ -⚳ -⚴ -⚵ -⚶ -⚷ -⚸ -⚹ -⚺ -⚻ -⚼ -⚽ -⚾ -⚿ -⛀ -⛁ -⛂ -⛃ -⛄ -⛅ -⛆ -⛇ -⛈ -⛉ -⛊ -⛋ -⛌ -⛍ -⛎ -⛏ -⛐ -⛑ -⛒ -⛓ -⛔ -⛕ -⛖ -⛗ -⛘ -⛙ -⛚ -⛛ -⛜ -⛝ -⛞ -⛠ -⛡ -⛢ -⛣ -⛤ -⛥ -⛦ -⛧ -⛨ -⛩ -⛪ -⛫ -⛬ -⛭ -⛮ -⛯ -⛲ -⛳ -⛵ -⛶ -⛺ -⛽ -⛾ -⛿ -✂ -✅ -✆ -✇ -✈ -✉ -✊ -✋ -✌ -✍ -✎ -✏ -✐ -✑ -✒ -✓ -✔ -✕ -✖ -✘ -✙ -✚ -✛ -✜ -✝ -✞ -✟ -✠ -✡ -✢ -✣ -✤ -✥ -✦ -✧ -✨ -✩ -✪ -✫ -✬ -✭ -✮ -✯ -✰ -✱ -✲ -✳ -✴ -✵ -✶ -✷ -✸ -✹ -✺ -✻ -✼ -✽ -✾ -✿ -❀ -❁ -❂ -❃ -❄ -❅ -❆ -❇ -❈ -❉ -❊ -❋ -❌ -❍ -❎ -❏ -❐ -❑ -❒ -❓ -❔ -❕ -❖ -❗ -❘ -❙ -❚ -❛ -❜ -❝ -❞ -❡ -❢ -❣ -❤ -❥ -❦ -❧ -❨ -❩ -❪ -❫ -❬ -❭ -❮ -❯ -❰ -❱ -❲ -❳ -❴ -❵ -❶ -❷ -❸ -❹ -❺ -❻ -❼ -❽ -❾ -❿ -➀ -➁ -➂ -➃ -➄ -➅ -➆ -➇ -➈ -➉ -➊ -➋ -➌ -➍ -➎ -➏ -➐ -➑ -➒ -➓ -➔ -➕ -➖ -➗ -➘ -➙ -➚ -➛ -➜ -➝ -➞ -➟ -➠ -➡ -➢ -➣ -➤ -➥ -➦ -➧ -➨ -➩ -➪ -➫ -➬ -➭ -➮ -➯ -➰ -➱ -➲ -➳ -➴ -➵ -➶ -➷ -➸ -➹ -➺ -➻ -➼ -➽ -➾ -➿ -⟶ -⟹ -⤴ -⤵ -⬅ -⬆ -⬇ -⬛ -⬜ -⭐ -⭕ -  -、 -。 -々 -〆 -〇 -〈 -〉 -《 -》 -「 -」 -『 -』 -【 -】 -〒 -〔 -〕 -〖 -〗 -〜 -〰 -〵 -〽 -ぁ -あ -ぃ -い -ぅ -う -ぇ -え -ぉ -お -か -が -き -ぎ -く -ぐ -け -げ -こ -ご -さ -ざ -し -じ -す -ず -せ -ぜ -そ -ぞ -た -だ -ち -ぢ -っ -つ -づ -て -で -と -ど -な -に -ぬ -ね -の -は -ば -ぱ -ひ -び -ぴ -ふ -ぶ -ぷ -へ -べ -ぺ -ほ -ぼ -ぽ -ま -み -む -め -も -ゃ -や -ゅ -ゆ -ょ -よ -ら -り -る -れ -ろ -ゎ -わ -ゑ -を -ん -ゕ -ゖ -ゝ -ゞ -ァ -ア -ィ -イ -ゥ -ウ -ェ -エ -ォ -オ -カ -ガ -キ -ギ -ク -グ -ケ -ゲ -コ -ゴ -サ -ザ -シ -ジ -ス -ズ -セ -ゼ -ソ -ゾ -タ -ダ -チ -ヂ -ッ -ツ -ヅ -テ -デ -ト -ド -ナ -ニ -ヌ -ネ -ノ -ハ -バ -パ -ヒ -ビ -ピ -フ -ブ -プ -ヘ -ベ -ペ -ホ -ボ -ポ -マ -ミ -ム -メ -モ -ャ -ヤ -ュ -ユ -ョ -ヨ -ラ -リ -ル -レ -ロ -ヮ -ワ -ヰ -ヲ -ン -ヴ -ヵ -ヶ -ヷ -ヸ -ヹ -ヺ -・ -ー -ヽ -ヾ -ヿ -㈱ -㊗ -㊙ -㐂 -㐱 -㑇 -㑊 -㑳 -㑺 -㒺 -㕁 -㕑 -㕥 -㕮 -㘎 -㘭 -㙍 -㙘 -㙟 -㙦 -㚷 -㛃 -㛚 -㛹 -㝛 -㝠 -㝡 -㟁 -㟃 -㠇 -㠓 -㠣 -㠯 -㠶 -㡌 -㢘 -㤘 -㤙 -㥄 -㥫 -㥮 -㧐 -㧑 -㧟 -㧱 -㨗 -㨪 -㩗 -㩦 -㩳 -㪚 -㪟 -㫰 -㬉 -㬊 -㬎 -㬚 -㬪 -㭎 -㭕 -㮣 -㮾 -㰀 -㳄 -㳇 -㳒 -㳘 -㳚 -㴔 -㴪 -㴱 -㵐 -㶲 -㸃 -㸆 -㸌 -㺄 -㻬 -㼝 -㽏 -㽞 -㿠 -䁖 -䂮 -䃅 -䃎 -䃮 -䅟 -䈰 -䊀 -䌹 -䎃 -䎖 -䏝 -䏡 -䏲 -䐃 -䓖 -䓛 -䓣 -䓨 -䓫 -䓬 -䖝 -䗖 -䗛 -䗪 -䗬 -䗴 -䘏 -䘑 -䘚 -䜣 -䝉 -䝔 -䝙 -䠀 -䠶 -䢺 -䢼 -䣘 -䥽 -䦃 -䮄 -䰟 -䰾 -䲁 -䲟 -䲠 -䲢 -䴉 -䴓 -䴔 -䴕 -䴖 -䴗 -䴘 -䴙 -䶊 -䶮 -一 -丁 -七 -丄 -万 -丈 -三 -上 -下 -丌 -不 -与 -丏 -丐 -丑 -专 -且 -丕 -世 -丘 -丙 -业 -丛 -东 -丝 -丞 -丟 -両 -丢 -两 -严 -並 -丧 -个 -丫 -中 -丮 -丰 -丱 -串 -临 -丸 -丹 -为 -主 -丼 -丽 -举 -丿 -乂 -乃 -久 -么 -义 -之 -乌 -乍 -乎 -乏 -乐 -乒 -乓 -乔 -乖 -乗 -乘 -乙 -乜 -九 -乞 -也 -习 -乡 -书 -乩 -乭 -买 -乱 -乳 -乸 -乹 -乾 -亀 -亂 -了 -予 -争 -亊 -事 -二 -亍 -于 -亏 -云 -互 -亓 -五 -井 -亘 -亙 -亚 -些 -亜 -亞 -亟 -亡 -亢 -交 -亥 -亦 -产 -亨 -亩 -享 -京 -亭 -亮 -亯 -亰 -亱 -亲 -亳 -亵 -亶 -亷 -亸 -亹 -人 -亾 -亿 -什 -仁 -仂 -仃 -仄 -仅 -仆 -仇 -仉 -今 -介 -仍 -从 -仏 -仑 -仓 -仔 -仕 -他 -仗 -付 -仙 -仛 -仜 -仝 -仞 -仟 -仡 -代 -令 -以 -仨 -仩 -仪 -仫 -们 -仮 -仰 -仱 -仲 -仳 -仵 -件 -价 -仺 -任 -仼 -份 -仿 -伀 -企 -伃 -伈 -伉 -伊 -伋 -伍 -伎 -伏 -伐 -休 -伔 -伕 -众 -优 -伙 -会 -伛 -伝 -伞 -伟 -传 -伢 -伣 -伤 -伥 -伦 -伧 -伪 -伫 -伬 -伭 -伯 -估 -伱 -伲 -伴 -伶 -伷 -伸 -伺 -伻 -似 -伽 -伾 -佀 -佁 -佃 -但 -佇 -佈 -佉 -佋 -佌 -位 -低 -住 -佐 -佑 -佒 -体 -佔 -何 -佖 -佗 -佘 -余 -佚 -佛 -作 -佝 -佞 -佟 -你 -佢 -佣 -佤 -佥 -佧 -佩 -佪 -佬 -佯 -佰 -佳 -佴 -併 -佶 -佷 -佸 -佹 -佺 -佻 -佼 -佽 -佾 -使 -侀 -侁 -侂 -侃 -侄 -侅 -來 -侇 -侈 -侉 -侊 -例 -侍 -侏 -侐 -侑 -侔 -侖 -侗 -侘 -侚 -供 -侜 -依 -侞 -侠 -価 -侣 -侥 -侦 -侧 -侨 -侩 -侪 -侬 -侮 -侯 -侲 -侳 -侴 -侵 -侶 -侷 -侹 -侻 -便 -俁 -係 -促 -俄 -俅 -俇 -俉 -俊 -俋 -俌 -俍 -俎 -俏 -俐 -俑 -俓 -俔 -俗 -俘 -俙 -俚 -俛 -俜 -保 -俞 -俟 -俠 -信 -俣 -俤 -俦 -俨 -俩 -俪 -俫 -俬 -俭 -修 -俯 -俱 -俳 -俴 -俵 -俶 -俷 -俸 -俺 -俽 -俾 -倀 -倃 -倅 -倆 -倇 -倈 -倉 -個 -倌 -倍 -倏 -倐 -們 -倒 -倓 -倔 -倕 -倖 -倗 -倘 -候 -倚 -倛 -倜 -倞 -借 -倠 -倡 -倢 -倣 -値 -倥 -倦 -倧 -倨 -倩 -倪 -倫 -倬 -倭 -倮 -倰 -倳 -倴 -倶 -倷 -倸 -倹 -债 -倻 -值 -倾 -偁 -偃 -偅 -假 -偈 -偉 -偊 -偋 -偌 -偍 -偎 -偏 -偐 -偓 -偕 -偘 -做 -偛 -停 -偝 -偞 -偟 -偠 -偡 -偢 -健 -偨 -偩 -偪 -偫 -偬 -偭 -偯 -偰 -偲 -側 -偵 -偶 -偷 -偸 -偺 -偻 -偽 -偾 -偿 -傀 -傂 -傃 -傅 -傈 -傉 -傋 -傌 -傍 -傎 -傑 -傒 -傔 -傕 -傖 -傘 -備 -傚 -傛 -傜 -傝 -傞 -傢 -傣 -傥 -傧 -储 -傩 -催 -傭 -傮 -傯 -傰 -傱 -傲 -傳 -傴 -債 -傷 -傺 -傻 -傽 -傾 -傿 -僁 -僂 -僄 -僅 -僆 -僇 -僈 -僉 -僊 -僋 -働 -僎 -像 -僑 -僓 -僔 -僕 -僖 -僗 -僙 -僚 -僛 -僜 -僝 -僞 -僡 -僤 -僥 -僦 -僧 -僨 -僩 -僪 -僬 -僭 -僮 -僰 -僱 -僳 -僴 -僵 -僶 -僸 -價 -僻 -僽 -僾 -僿 -儀 -儁 -儂 -儃 -億 -儆 -儇 -儈 -儉 -儋 -儌 -儐 -儑 -儒 -儓 -儔 -儕 -儗 -儘 -儚 -儛 -儜 -儞 -償 -儠 -儡 -儢 -儤 -儥 -儦 -優 -儫 -儰 -儱 -儲 -儳 -儴 -儵 -儷 -儸 -儹 -儺 -儻 -儼 -儽 -儿 -兀 -允 -元 -兄 -充 -兆 -兇 -先 -光 -克 -兌 -免 -兎 -児 -兑 -兒 -兔 -兕 -兖 -兗 -兙 -党 -兛 -兜 -兝 -兞 -兠 -兡 -兢 -兣 -入 -內 -全 -兩 -兪 -八 -公 -六 -兮 -兰 -共 -关 -兴 -兵 -其 -具 -典 -兹 -养 -兼 -兽 -兿 -冀 -冁 -冂 -冄 -内 -円 -冇 -冈 -冉 -冊 -册 -再 -冏 -冐 -冑 -冒 -冓 -冔 -冕 -冗 -冘 -写 -冚 -军 -农 -冞 -冠 -冢 -冣 -冤 -冥 -冧 -冨 -冪 -冬 -冮 -冯 -冰 -冱 -冲 -决 -冴 -况 -冶 -冷 -冺 -冻 -冼 -冽 -净 -凃 -凄 -凅 -准 -凇 -凈 -凉 -凊 -凋 -凌 -凍 -减 -凑 -凔 -凖 -凘 -凛 -凜 -凝 -凞 -几 -凡 -凢 -凤 -処 -凪 -凫 -凬 -凭 -凯 -凰 -凱 -凳 -凵 -凶 -凸 -凹 -出 -击 -凼 -函 -凿 -刀 -刁 -刂 -刃 -刄 -分 -切 -刈 -刉 -刊 -刌 -刍 -刎 -刑 -划 -刓 -刖 -列 -刘 -则 -刚 -创 -刜 -初 -刞 -删 -判 -別 -刦 -刧 -刨 -利 -刪 -别 -刬 -刭 -刮 -到 -刱 -刲 -刳 -刵 -制 -刷 -券 -刹 -刺 -刻 -刼 -刽 -刿 -剀 -剁 -剂 -剃 -剄 -剅 -剆 -則 -剉 -削 -剋 -剌 -前 -剎 -剏 -剐 -剑 -剒 -剔 -剕 -剖 -剗 -剚 -剛 -剜 -剝 -剞 -剟 -剡 -剣 -剤 -剥 -剧 -剩 -剪 -剫 -剬 -剭 -剮 -副 -剰 -割 -剳 -剴 -創 -剷 -剸 -剹 -剺 -剻 -剼 -剽 -剿 -劀 -劁 -劂 -劃 -劄 -劇 -劈 -劉 -劊 -劋 -劌 -劍 -劐 -劑 -劓 -劔 -劖 -劗 -劘 -劙 -力 -劝 -办 -功 -加 -务 -劢 -劣 -劦 -动 -助 -努 -劫 -劬 -劭 -励 -劲 -劳 -労 -劵 -効 -劼 -劾 -势 -勁 -勃 -勅 -勇 -勉 -勋 -勌 -勍 -勐 -勑 -勒 -勔 -動 -勖 -勗 -勘 -務 -勚 -勛 -勝 -勞 -募 -勢 -勣 -勤 -勦 -勧 -勩 -勰 -勱 -勲 -勳 -勴 -勵 -勷 -勸 -勺 -勻 -勾 -勿 -匀 -匁 -匂 -匃 -匄 -包 -匆 -匈 -匊 -匋 -匍 -匏 -匐 -匕 -化 -北 -匙 -匜 -匝 -匟 -匠 -匡 -匢 -匣 -匦 -匪 -匭 -匮 -匯 -匰 -匱 -匲 -匴 -匷 -匸 -匹 -区 -医 -匼 -匽 -匾 -匿 -區 -十 -千 -卅 -升 -午 -卉 -半 -卋 -卌 -卍 -华 -协 -卐 -卑 -卒 -卓 -協 -单 -卖 -南 -単 -博 -卜 -卞 -卟 -占 -卡 -卢 -卣 -卤 -卦 -卧 -卫 -卬 -卮 -卯 -印 -危 -卲 -即 -却 -卵 -卷 -卸 -卹 -卺 -卻 -卼 -卽 -卿 -厀 -厂 -厄 -厅 -历 -厉 -压 -厌 -厍 -厎 -厒 -厓 -厔 -厕 -厖 -厗 -厘 -厙 -厚 -厜 -厝 -厞 -原 -厠 -厢 -厣 -厤 -厥 -厦 -厨 -厩 -厬 -厭 -厮 -厰 -厲 -厳 -厴 -厹 -去 -厾 -县 -叁 -参 -參 -叄 -叆 -叇 -又 -叉 -及 -友 -双 -反 -収 -发 -叔 -叕 -取 -受 -变 -叙 -叚 -叛 -叟 -叠 -叡 -叢 -口 -古 -句 -另 -叨 -叩 -只 -叫 -召 -叭 -叮 -可 -台 -叱 -史 -右 -叵 -叶 -号 -司 -叹 -叻 -叼 -叽 -吁 -吃 -各 -吆 -合 -吉 -吊 -吋 -同 -名 -后 -吏 -吐 -向 -吒 -吓 -吔 -吕 -吖 -吗 -吚 -君 -吝 -吞 -吟 -吠 -吡 -吣 -吥 -否 -吧 -吨 -吩 -吪 -含 -听 -吭 -吮 -启 -吰 -吱 -吲 -吳 -吴 -吵 -吶 -吷 -吸 -吹 -吻 -吼 -吽 -吾 -呀 -呂 -呃 -呆 -呇 -呈 -呉 -告 -呋 -呌 -呎 -呐 -呑 -呒 -呓 -呔 -呕 -呖 -呗 -员 -呙 -呛 -呜 -呢 -呣 -呤 -呥 -呦 -周 -呪 -呫 -呬 -呰 -呱 -呲 -味 -呴 -呵 -呶 -呷 -呸 -呺 -呻 -呼 -命 -呾 -呿 -咀 -咁 -咂 -咄 -咅 -咆 -咇 -咈 -咉 -咊 -咋 -和 -咍 -咎 -咏 -咐 -咑 -咒 -咔 -咕 -咖 -咗 -咘 -咙 -咚 -咛 -咝 -咟 -咠 -咡 -咢 -咣 -咤 -咥 -咦 -咧 -咨 -咩 -咪 -咫 -咬 -咭 -咮 -咯 -咱 -咲 -咳 -咴 -咶 -咷 -咸 -咺 -咻 -咼 -咽 -咾 -咿 -哀 -品 -哂 -哃 -哄 -哅 -哆 -哇 -哈 -哉 -哌 -响 -哎 -哏 -哐 -哑 -哒 -哓 -哔 -哕 -哖 -哗 -哙 -哚 -哝 -哞 -哟 -員 -哢 -哤 -哥 -哦 -哧 -哨 -哩 -哪 -哫 -哭 -哮 -哱 -哲 -哳 -哶 -哺 -哼 -哽 -哿 -唁 -唃 -唄 -唅 -唆 -唇 -唈 -唉 -唌 -唏 -唐 -唑 -唒 -唔 -唕 -唖 -唗 -唘 -唚 -唛 -唝 -唠 -唢 -唣 -唤 -唦 -唧 -唪 -唫 -唬 -唭 -售 -唯 -唰 -唱 -唲 -唳 -唴 -唵 -唶 -唷 -唸 -唻 -唼 -唾 -唿 -啁 -啃 -啄 -啅 -商 -啉 -啊 -啍 -啎 -問 -啐 -啑 -啒 -啓 -啕 -啖 -啗 -啚 -啜 -啞 -啟 -啡 -啢 -啣 -啤 -啥 -啦 -啧 -啪 -啫 -啬 -啭 -啮 -啯 -啰 -啱 -啲 -啴 -啵 -啶 -啷 -啸 -啻 -啼 -啽 -啾 -喀 -喁 -喂 -喃 -善 -喆 -喇 -喈 -喉 -喊 -喋 -喌 -喍 -喏 -喑 -喒 -喓 -喔 -喘 -喙 -喚 -喜 -喝 -喟 -喢 -喣 -喤 -喥 -喦 -喧 -喨 -喩 -喪 -喫 -喬 -喭 -單 -喰 -喱 -喲 -喳 -喵 -営 -喷 -喹 -喻 -喼 -喽 -喾 -喿 -嗀 -嗁 -嗃 -嗄 -嗅 -嗆 -嗇 -嗈 -嗉 -嗊 -嗋 -嗌 -嗍 -嗎 -嗏 -嗐 -嗑 -嗒 -嗓 -嗔 -嗕 -嗖 -嗚 -嗛 -嗜 -嗝 -嗞 -嗟 -嗡 -嗢 -嗣 -嗤 -嗥 -嗦 -嗧 -嗨 -嗩 -嗪 -嗫 -嗬 -嗮 -嗯 -嗲 -嗳 -嗵 -嗶 -嗷 -嗹 -嗺 -嗼 -嗽 -嗾 -嗿 -嘀 -嘁 -嘂 -嘄 -嘅 -嘆 -嘈 -嘉 -嘌 -嘍 -嘎 -嘏 -嘐 -嘑 -嘒 -嘓 -嘔 -嘕 -嘖 -嘗 -嘘 -嘚 -嘛 -嘜 -嘝 -嘞 -嘟 -嘠 -嘡 -嘢 -嘣 -嘤 -嘥 -嘧 -嘩 -嘬 -嘭 -嘮 -嘯 -嘰 -嘱 -嘲 -嘳 -嘴 -嘵 -嘶 -嘷 -嘸 -嘹 -嘻 -嘽 -嘾 -嘿 -噀 -噁 -噂 -噅 -噆 -噇 -噈 -噉 -噊 -噌 -噍 -噎 -噏 -噓 -噔 -噗 -噘 -噙 -噚 -噛 -噜 -噝 -噞 -噠 -噢 -噣 -噤 -噥 -噦 -器 -噩 -噪 -噫 -噬 -噭 -噮 -噯 -噰 -噱 -噲 -噳 -噴 -噶 -噸 -噹 -噺 -噻 -噼 -嚀 -嚃 -嚄 -嚅 -嚆 -嚇 -嚈 -嚌 -嚍 -嚎 -嚏 -嚐 -嚒 -嚓 -嚕 -嚗 -嚘 -嚙 -嚚 -嚜 -嚞 -嚟 -嚢 -嚣 -嚤 -嚥 -嚦 -嚧 -嚨 -嚩 -嚪 -嚫 -嚬 -嚭 -嚮 -嚯 -嚲 -嚳 -嚴 -嚵 -嚶 -嚷 -嚼 -嚽 -嚾 -嚿 -囀 -囁 -囂 -囃 -囅 -囆 -囈 -囉 -囊 -囋 -囌 -囍 -囑 -囒 -囓 -囔 -囗 -囙 -囚 -四 -囝 -回 -囟 -因 -囡 -团 -団 -囤 -囧 -囪 -囫 -园 -囮 -囯 -困 -囱 -囲 -図 -围 -囵 -囶 -囷 -囹 -固 -国 -图 -囿 -圀 -圁 -圂 -圃 -圄 -圅 -圆 -圇 -圈 -圉 -圊 -國 -圌 -圍 -圏 -圐 -園 -圓 -圖 -圗 -團 -圙 -圜 -圞 -土 -圠 -圢 -圣 -圧 -在 -圩 -圪 -圫 -圬 -圭 -圮 -圯 -地 -圲 -圳 -圹 -场 -圻 -圾 -址 -坁 -坂 -坅 -均 -坉 -坊 -坋 -坌 -坍 -坎 -坏 -坐 -坑 -坒 -坖 -块 -坚 -坛 -坜 -坝 -坞 -坟 -坠 -坡 -坣 -坤 -坥 -坦 -坨 -坩 -坪 -坫 -坬 -坭 -坮 -坯 -坰 -坱 -坲 -坳 -坴 -坵 -坶 -坷 -坻 -坼 -坽 -坿 -垀 -垂 -垃 -垄 -垆 -垈 -型 -垌 -垍 -垎 -垏 -垒 -垓 -垔 -垕 -垗 -垘 -垙 -垚 -垛 -垜 -垝 -垞 -垟 -垠 -垡 -垢 -垣 -垤 -垦 -垧 -垩 -垫 -垭 -垮 -垯 -垰 -垱 -垲 -垴 -垵 -垸 -垺 -垻 -垼 -垽 -垾 -垿 -埂 -埃 -埅 -埆 -埇 -埈 -埋 -埌 -城 -埏 -埒 -埔 -埕 -埗 -埘 -埙 -埚 -埜 -埝 -域 -埠 -埡 -埢 -埤 -埧 -埨 -埪 -埫 -埭 -埮 -埯 -埳 -埴 -埵 -埶 -執 -埸 -培 -基 -埻 -埼 -埽 -堀 -堂 -堃 -堅 -堆 -堇 -堈 -堉 -堊 -堋 -堌 -堍 -堎 -堐 -堑 -堔 -堕 -堖 -堘 -堙 -堝 -堞 -堠 -堡 -堣 -堤 -堥 -堦 -堧 -堨 -堩 -堪 -堮 -堯 -堰 -報 -堲 -場 -堵 -堷 -堺 -堼 -堽 -堾 -堿 -塀 -塁 -塄 -塅 -塆 -塈 -塉 -塊 -塋 -塌 -塍 -塏 -塑 -塒 -塓 -塔 -塕 -塗 -塘 -塙 -塚 -塜 -塝 -塞 -塟 -塡 -塢 -塤 -塥 -塨 -塩 -填 -塬 -塭 -塯 -塰 -塱 -塲 -塴 -塵 -塸 -塹 -塺 -塼 -塽 -塾 -塿 -墀 -墁 -境 -墅 -墆 -墈 -墉 -墊 -墋 -墎 -墐 -墒 -墓 -墕 -墖 -増 -墘 -墙 -墚 -墜 -墝 -增 -墟 -墠 -墡 -墣 -墦 -墨 -墩 -墪 -墫 -墬 -墮 -墯 -墱 -墳 -墶 -墺 -墻 -墼 -墽 -墾 -壁 -壄 -壅 -壆 -壇 -壈 -壊 -壋 -壌 -壎 -壐 -壑 -壓 -壔 -壕 -壖 -壘 -壙 -壚 -壛 -壝 -壞 -壟 -壠 -壢 -壤 -壩 -壪 -士 -壬 -壮 -壯 -声 -壱 -売 -壳 -壴 -壶 -壷 -壸 -壹 -壺 -壻 -壼 -壽 -壾 -壿 -夀 -夃 -处 -夆 -备 -変 -夊 -夋 -夌 -复 -夎 -夏 -夐 -夒 -夔 -夕 -外 -夗 -夘 -夙 -多 -夜 -够 -夠 -夢 -夤 -夥 -大 -天 -太 -夫 -夬 -夭 -央 -夯 -失 -头 -夷 -夸 -夹 -夺 -夼 -夾 -奀 -奁 -奂 -奄 -奅 -奇 -奈 -奉 -奊 -奋 -奎 -奏 -奐 -契 -奓 -奔 -奕 -奖 -套 -奘 -奚 -奠 -奡 -奢 -奣 -奥 -奧 -奨 -奩 -奪 -奫 -奬 -奭 -奮 -奰 -奲 -女 -奴 -奶 -奸 -她 -奼 -好 -妀 -妁 -如 -妃 -妄 -妆 -妇 -妈 -妊 -妍 -妎 -妏 -妐 -妑 -妒 -妓 -妖 -妗 -妘 -妙 -妝 -妞 -妠 -妢 -妣 -妤 -妥 -妦 -妧 -妨 -妩 -妪 -妫 -妬 -妭 -妮 -妯 -妲 -妳 -妵 -妷 -妸 -妹 -妺 -妻 -妾 -姀 -姁 -姃 -姆 -姈 -姉 -姊 -始 -姌 -姍 -姎 -姏 -姐 -姑 -姒 -姓 -委 -姗 -姘 -姙 -姚 -姜 -姝 -姞 -姡 -姣 -姤 -姥 -姦 -姨 -姪 -姫 -姬 -姮 -姱 -姵 -姶 -姸 -姹 -姺 -姻 -姼 -姽 -姿 -娀 -威 -娃 -娄 -娅 -娆 -娇 -娈 -娉 -娋 -娌 -娍 -娎 -娑 -娓 -娖 -娘 -娙 -娛 -娜 -娟 -娠 -娣 -娥 -娩 -娫 -娭 -娯 -娱 -娲 -娳 -娴 -娵 -娶 -娷 -娸 -娼 -娽 -娿 -婀 -婁 -婆 -婉 -婊 -婌 -婍 -婐 -婑 -婓 -婕 -婗 -婘 -婚 -婞 -婟 -婠 -婢 -婣 -婤 -婥 -婦 -婧 -婪 -婫 -婬 -婭 -婯 -婳 -婴 -婵 -婶 -婷 -婸 -婺 -婻 -婼 -婿 -媂 -媃 -媄 -媆 -媊 -媐 -媒 -媓 -媔 -媕 -媖 -媗 -媚 -媛 -媜 -媞 -媟 -媢 -媥 -媦 -媧 -媪 -媬 -媭 -媮 -媯 -媰 -媱 -媲 -媳 -媵 -媸 -媺 -媻 -媼 -媽 -媾 -媿 -嫁 -嫂 -嫄 -嫇 -嫈 -嫉 -嫋 -嫌 -嫐 -嫒 -嫔 -嫕 -嫖 -嫗 -嫘 -嫚 -嫛 -嫜 -嫠 -嫡 -嫢 -嫣 -嫥 -嫦 -嫩 -嫪 -嫫 -嫭 -嫮 -嫰 -嫱 -嫲 -嫳 -嫴 -嫵 -嫶 -嫷 -嫺 -嫻 -嫽 -嫿 -嬀 -嬃 -嬅 -嬈 -嬉 -嬋 -嬌 -嬎 -嬐 -嬓 -嬖 -嬗 -嬙 -嬛 -嬝 -嬡 -嬢 -嬤 -嬥 -嬨 -嬪 -嬬 -嬭 -嬰 -嬲 -嬴 -嬷 -嬸 -嬽 -嬾 -嬿 -孀 -孁 -孃 -孅 -孆 -孈 -孋 -孌 -子 -孑 -孓 -孔 -孕 -孖 -字 -存 -孙 -孚 -孛 -孜 -孝 -孟 -孢 -季 -孤 -孥 -学 -孩 -孪 -孫 -孬 -孮 -孰 -孱 -孲 -孳 -孵 -孷 -學 -孺 -孻 -孼 -孽 -孿 -宁 -宂 -它 -宄 -宅 -宇 -守 -安 -宋 -完 -宍 -宎 -宏 -宓 -宕 -宗 -官 -宙 -定 -宛 -宜 -宝 -实 -実 -宠 -审 -客 -宣 -室 -宥 -宦 -宧 -宨 -宪 -宫 -宬 -宮 -宰 -害 -宴 -宵 -家 -宸 -容 -宽 -宾 -宿 -寀 -寁 -寂 -寃 -寄 -寅 -密 -寇 -寈 -寊 -寋 -富 -寍 -寐 -寑 -寒 -寓 -寔 -寕 -寖 -寗 -寘 -寙 -寛 -寜 -寝 -寞 -察 -寠 -寡 -寢 -寤 -寥 -實 -寧 -寨 -審 -寪 -寫 -寬 -寮 -寯 -寰 -寱 -寳 -寵 -寶 -寸 -对 -寺 -寻 -导 -対 -寿 -封 -専 -尃 -射 -尅 -将 -將 -專 -尉 -尊 -尋 -尌 -對 -導 -小 -尐 -少 -尒 -尓 -尔 -尕 -尖 -尗 -尘 -尙 -尚 -尜 -尝 -尟 -尢 -尤 -尥 -尧 -尨 -尪 -尬 -尭 -尰 -就 -尳 -尴 -尷 -尸 -尹 -尺 -尻 -尼 -尽 -尾 -尿 -局 -屁 -层 -屃 -屄 -居 -屆 -屇 -屈 -屉 -届 -屋 -屌 -屍 -屎 -屏 -屐 -屑 -屓 -屔 -展 -屖 -屘 -屙 -屚 -屜 -屝 -属 -屠 -屡 -屢 -屣 -層 -履 -屦 -屧 -屨 -屩 -屬 -屭 -屮 -屯 -山 -屴 -屹 -屺 -屻 -屼 -屾 -屿 -岀 -岁 -岂 -岅 -岈 -岉 -岊 -岋 -岌 -岍 -岏 -岐 -岑 -岒 -岔 -岖 -岗 -岘 -岙 -岚 -岛 -岜 -岝 -岞 -岟 -岠 -岡 -岢 -岣 -岤 -岧 -岨 -岩 -岪 -岫 -岬 -岭 -岮 -岯 -岰 -岱 -岳 -岵 -岷 -岸 -岻 -岽 -岿 -峁 -峂 -峃 -峄 -峅 -峇 -峉 -峊 -峋 -峌 -峍 -峐 -峒 -峗 -峘 -峙 -峚 -峛 -峝 -峞 -峠 -峡 -峣 -峤 -峥 -峦 -峧 -峨 -峩 -峪 -峬 -峭 -峮 -峯 -峰 -峱 -峴 -島 -峷 -峻 -峼 -峽 -峿 -崀 -崁 -崂 -崃 -崄 -崆 -崇 -崈 -崋 -崌 -崍 -崎 -崏 -崐 -崑 -崒 -崔 -崖 -崗 -崘 -崙 -崚 -崛 -崝 -崞 -崟 -崠 -崡 -崢 -崣 -崤 -崥 -崦 -崧 -崨 -崩 -崬 -崭 -崮 -崰 -崱 -崳 -崴 -崵 -崶 -崷 -崺 -崽 -崾 -崿 -嵁 -嵂 -嵃 -嵅 -嵇 -嵊 -嵋 -嵌 -嵎 -嵐 -嵑 -嵒 -嵕 -嵖 -嵗 -嵘 -嵙 -嵚 -嵛 -嵜 -嵝 -嵞 -嵣 -嵥 -嵨 -嵩 -嵫 -嵬 -嵮 -嵯 -嵰 -嵱 -嵲 -嵴 -嵷 -嵺 -嵻 -嵽 -嵾 -嵿 -嶀 -嶁 -嶂 -嶃 -嶄 -嶅 -嶆 -嶇 -嶈 -嶉 -嶊 -嶋 -嶌 -嶍 -嶒 -嶓 -嶔 -嶕 -嶗 -嶙 -嶚 -嶜 -嶝 -嶞 -嶟 -嶠 -嶡 -嶢 -嶦 -嶧 -嶨 -嶩 -嶪 -嶬 -嶭 -嶮 -嶰 -嶱 -嶲 -嶴 -嶵 -嶷 -嶸 -嶺 -嶼 -嶽 -巀 -巂 -巃 -巄 -巅 -巆 -巇 -巉 -巋 -巌 -巍 -巎 -巏 -巑 -巒 -巔 -巕 -巖 -巗 -巘 -巛 -川 -州 -巟 -巡 -巢 -巣 -工 -左 -巧 -巨 -巩 -巫 -差 -巯 -巰 -己 -已 -巳 -巴 -巵 -巶 -巷 -巹 -巻 -巽 -巾 -巿 -帀 -币 -市 -布 -帅 -帆 -师 -帊 -希 -帏 -帐 -帑 -帔 -帕 -帖 -帗 -帘 -帙 -帚 -帛 -帜 -帝 -帟 -帠 -帡 -帢 -帣 -帤 -帥 -带 -帧 -帨 -帩 -師 -帬 -席 -帮 -帯 -帰 -帱 -帳 -帴 -帶 -帷 -常 -帻 -帼 -帽 -帾 -幀 -幂 -幃 -幄 -幅 -幇 -幌 -幎 -幏 -幑 -幓 -幔 -幕 -幖 -幗 -幘 -幙 -幚 -幛 -幝 -幞 -幟 -幠 -幡 -幢 -幣 -幦 -幧 -幨 -幩 -幪 -幫 -幬 -幭 -幰 -干 -平 -年 -幵 -并 -幷 -幸 -幹 -幺 -幻 -幼 -幽 -幾 -广 -庀 -庁 -庂 -広 -庄 -庆 -庇 -庈 -庉 -床 -庋 -庌 -序 -庐 -庑 -库 -应 -底 -庖 -店 -庙 -庚 -庛 -府 -庞 -废 -庠 -庢 -庣 -庤 -庥 -度 -座 -庨 -庪 -庫 -庬 -庭 -庮 -庰 -庱 -庲 -庳 -庴 -庵 -庶 -康 -庸 -庹 -庻 -庼 -庽 -庾 -廁 -廂 -廃 -廄 -廅 -廆 -廇 -廈 -廉 -廊 -廋 -廌 -廍 -廎 -廐 -廑 -廒 -廓 -廔 -廕 -廖 -廗 -廘 -廙 -廚 -廛 -廜 -廝 -廞 -廟 -廠 -廡 -廢 -廣 -廥 -廦 -廧 -廨 -廩 -廪 -廬 -廮 -廯 -廰 -廱 -廳 -廵 -延 -廷 -廸 -廹 -建 -廻 -廼 -廾 -廿 -开 -弁 -异 -弃 -弄 -弅 -弆 -弇 -弈 -弉 -弊 -弋 -弌 -弍 -式 -弐 -弑 -弒 -弓 -弔 -引 -弖 -弗 -弘 -弚 -弛 -弝 -弟 -张 -弢 -弣 -弤 -弥 -弦 -弧 -弨 -弩 -弭 -弮 -弯 -弱 -弳 -張 -弶 -強 -弸 -弹 -强 -弼 -弾 -彀 -彃 -彄 -彅 -彆 -彈 -彉 -彊 -彋 -彌 -彎 -彏 -彐 -归 -当 -彔 -录 -彖 -彗 -彘 -彙 -彜 -彝 -彞 -彟 -彠 -彡 -形 -彣 -彤 -彥 -彦 -彧 -彩 -彪 -彫 -彬 -彭 -彯 -彰 -影 -彳 -彴 -彷 -彸 -役 -彻 -彼 -彾 -彿 -往 -征 -徂 -徃 -径 -待 -徇 -很 -徉 -徊 -律 -後 -徐 -徑 -徒 -従 -徕 -得 -徘 -徙 -徛 -徜 -從 -徠 -御 -徥 -徦 -徧 -徨 -復 -循 -徫 -徬 -徭 -微 -徯 -徲 -徳 -徴 -徵 -德 -徸 -徹 -徼 -徽 -徾 -徿 -忀 -忁 -心 -必 -忆 -忉 -忌 -忍 -忏 -忐 -忑 -忒 -忔 -忕 -忖 -志 -忘 -忙 -応 -忝 -忞 -忠 -忡 -忣 -忤 -忥 -忧 -忨 -忪 -快 -忬 -忭 -忮 -忯 -忱 -忳 -念 -忷 -忸 -忺 -忻 -忼 -忽 -忾 -忿 -怀 -态 -怂 -怃 -怄 -怅 -怆 -怊 -怋 -怍 -怎 -怏 -怐 -怒 -怓 -怔 -怕 -怖 -怗 -怙 -怚 -怛 -怜 -思 -怞 -怠 -怡 -怢 -急 -怦 -性 -怨 -怩 -怪 -怫 -怬 -怭 -怮 -怯 -怱 -怲 -怳 -怴 -怵 -怷 -怹 -总 -怼 -怿 -恀 -恁 -恂 -恃 -恅 -恆 -恇 -恉 -恊 -恋 -恌 -恍 -恐 -恒 -恓 -恔 -恕 -恙 -恚 -恛 -恝 -恞 -恟 -恠 -恡 -恢 -恣 -恤 -恥 -恧 -恨 -恩 -恪 -恫 -恬 -恭 -息 -恰 -恲 -恳 -恵 -恶 -恸 -恹 -恺 -恻 -恼 -恽 -恿 -悀 -悁 -悃 -悄 -悅 -悆 -悇 -悈 -悉 -悊 -悌 -悍 -悐 -悒 -悔 -悕 -悖 -悗 -悚 -悛 -悝 -悞 -悟 -悠 -悢 -患 -悤 -悦 -悧 -您 -悩 -悪 -悫 -悬 -悭 -悯 -悰 -悱 -悲 -悳 -悴 -悵 -悶 -悷 -悸 -悹 -悺 -悻 -悼 -悽 -悾 -惄 -情 -惆 -惇 -惈 -惉 -惊 -惋 -惌 -惎 -惏 -惑 -惓 -惔 -惕 -惘 -惙 -惚 -惛 -惜 -惝 -惟 -惠 -惡 -惢 -惣 -惤 -惥 -惦 -惧 -惨 -惩 -惪 -惫 -惬 -惭 -惮 -惯 -惰 -惱 -惲 -想 -惴 -惵 -惶 -惷 -惸 -惹 -惺 -惻 -惼 -惾 -愀 -愁 -愃 -愅 -愆 -愈 -愉 -愊 -愍 -愎 -意 -愐 -愒 -愓 -愔 -愕 -愖 -愚 -愛 -愜 -感 -愠 -愣 -愤 -愦 -愧 -愨 -愫 -愬 -愭 -愮 -愯 -愲 -愴 -愶 -愷 -愼 -愽 -愾 -愿 -慁 -慂 -慄 -慅 -慆 -慇 -慈 -慉 -慊 -態 -慌 -慍 -慎 -慑 -慒 -慓 -慔 -慕 -慘 -慚 -慛 -慜 -慝 -慞 -慟 -慢 -慣 -慤 -慥 -慧 -慨 -慪 -慫 -慬 -慭 -慮 -慰 -慱 -慲 -慳 -慴 -慵 -慶 -慷 -慹 -慺 -慼 -慽 -慾 -憀 -憂 -憃 -憇 -憉 -憊 -憋 -憍 -憎 -憐 -憑 -憒 -憓 -憔 -憕 -憖 -憙 -憚 -憛 -憝 -憟 -憡 -憢 -憤 -憧 -憨 -憩 -憪 -憫 -憬 -憭 -憮 -憯 -憰 -憱 -憲 -憴 -憵 -憶 -憷 -憸 -憺 -憻 -憼 -憾 -憿 -懁 -懂 -懃 -懅 -懆 -懇 -懈 -應 -懊 -懋 -懌 -懍 -懐 -懑 -懒 -懔 -懘 -懞 -懟 -懠 -懣 -懤 -懥 -懦 -懧 -懨 -懪 -懫 -懭 -懮 -懰 -懱 -懲 -懵 -懶 -懷 -懸 -懺 -懻 -懼 -懽 -懾 -懿 -戀 -戁 -戃 -戄 -戆 -戇 -戈 -戉 -戊 -戋 -戌 -戍 -戎 -戏 -成 -我 -戒 -戔 -戕 -或 -戗 -战 -戙 -戚 -戛 -戞 -戟 -戠 -戡 -戢 -戣 -戤 -戥 -戦 -戧 -戩 -截 -戬 -戭 -戮 -戯 -戰 -戱 -戲 -戳 -戴 -戶 -户 -戸 -戹 -戺 -戻 -戼 -戽 -戾 -房 -所 -扁 -扂 -扃 -扅 -扆 -扇 -扈 -扉 -扊 -手 -才 -扎 -扐 -扑 -扒 -打 -扔 -払 -托 -扙 -扛 -扜 -扞 -扡 -扢 -扣 -扤 -扥 -扦 -执 -扩 -扪 -扫 -扬 -扭 -扮 -扯 -扰 -扱 -扳 -扶 -批 -扺 -扻 -扼 -扽 -找 -承 -技 -抃 -抄 -抆 -抇 -抉 -把 -抌 -抎 -抏 -抑 -抒 -抓 -抔 -投 -抖 -抗 -折 -抚 -抛 -抜 -抝 -択 -抟 -抠 -抡 -抢 -护 -报 -抦 -抨 -抪 -披 -抬 -抭 -抮 -抱 -抴 -抵 -抶 -抹 -抻 -押 -抽 -抾 -抿 -拂 -拃 -拄 -担 -拆 -拇 -拈 -拉 -拊 -拋 -拌 -拍 -拎 -拏 -拐 -拑 -拒 -拓 -拔 -拕 -拖 -拗 -拘 -拙 -拚 -招 -拜 -拝 -拟 -拠 -拡 -拢 -拣 -拤 -拥 -拦 -拧 -拨 -择 -拫 -括 -拭 -拮 -拯 -拱 -拲 -拳 -拴 -拵 -拶 -拷 -拸 -拹 -拺 -拼 -拽 -拾 -拿 -挀 -持 -挂 -挃 -指 -挈 -按 -挋 -挌 -挍 -挎 -挏 -挐 -挑 -挓 -挖 -挙 -挚 -挛 -挝 -挞 -挟 -挠 -挡 -挣 -挤 -挥 -挦 -挨 -挩 -挪 -挫 -挬 -振 -挱 -挲 -挴 -挵 -挶 -挹 -挺 -挻 -挼 -挽 -挾 -挿 -捁 -捂 -捃 -捄 -捅 -捆 -捉 -捊 -捋 -捌 -捍 -捎 -捏 -捐 -捒 -捔 -捕 -捖 -捗 -捘 -捜 -捞 -损 -捡 -换 -捣 -捥 -捦 -捧 -捨 -捩 -捫 -捭 -据 -捯 -捱 -捲 -捵 -捶 -捷 -捺 -捻 -捼 -捽 -掀 -掂 -掃 -掄 -掇 -授 -掉 -掊 -掌 -掎 -掏 -掐 -排 -掔 -掖 -掗 -掘 -掙 -掛 -掜 -掝 -掞 -掟 -掠 -採 -探 -掣 -掤 -接 -控 -推 -掩 -措 -掫 -掬 -掭 -掮 -掯 -掰 -掱 -掲 -掳 -掴 -掷 -掸 -掺 -掻 -掼 -掽 -掾 -揀 -揃 -揄 -揅 -揆 -揉 -揊 -揍 -揎 -描 -提 -揑 -插 -揔 -揕 -揖 -揘 -揚 -換 -揜 -揝 -揟 -揠 -握 -揣 -揤 -揥 -揨 -揩 -揪 -揫 -揭 -揮 -揯 -揱 -揲 -揳 -援 -揵 -揶 -揷 -揸 -揹 -揺 -揽 -揿 -搀 -搁 -搂 -搅 -搆 -搉 -搊 -搋 -搌 -損 -搏 -搐 -搒 -搓 -搔 -搕 -搖 -搗 -搘 -搚 -搛 -搜 -搞 -搟 -搠 -搡 -搢 -搣 -搤 -搥 -搦 -搧 -搨 -搪 -搫 -搬 -搭 -搯 -搰 -搳 -搴 -搵 -搶 -搷 -搹 -携 -搽 -搾 -摀 -摁 -摂 -摃 -摄 -摅 -摆 -摇 -摈 -摊 -摋 -摍 -摎 -摏 -摐 -摑 -摒 -摓 -摔 -摘 -摛 -摜 -摝 -摞 -摟 -摠 -摡 -摦 -摧 -摩 -摭 -摮 -摯 -摰 -摲 -摳 -摴 -摵 -摶 -摸 -摹 -摺 -摻 -摽 -摿 -撂 -撃 -撄 -撅 -撇 -撈 -撊 -撋 -撌 -撏 -撐 -撑 -撒 -撓 -撕 -撖 -撗 -撙 -撚 -撜 -撝 -撞 -撟 -撠 -撢 -撣 -撤 -撥 -撦 -撩 -撫 -撬 -播 -撮 -撰 -撲 -撳 -撵 -撷 -撸 -撹 -撺 -撻 -撼 -撽 -撾 -撿 -擀 -擁 -擂 -擄 -擅 -擇 -擉 -擊 -擋 -操 -擎 -擏 -擐 -擒 -擓 -擔 -擕 -擖 -擗 -擘 -據 -擞 -擠 -擡 -擢 -擣 -擤 -擥 -擦 -擧 -擨 -擩 -擫 -擬 -擭 -擯 -擰 -擱 -擲 -擴 -擷 -擸 -擺 -擻 -擼 -擽 -擾 -擿 -攀 -攄 -攆 -攇 -攉 -攌 -攍 -攏 -攐 -攒 -攓 -攔 -攕 -攖 -攗 -攘 -攙 -攛 -攜 -攝 -攞 -攠 -攡 -攢 -攣 -攤 -攥 -攦 -攩 -攪 -攫 -攬 -攭 -攮 -支 -攲 -攳 -攴 -收 -攷 -攸 -改 -攻 -攽 -放 -政 -敁 -敂 -故 -敆 -效 -敉 -敊 -敌 -敍 -敎 -敏 -救 -敓 -敔 -敕 -敖 -敗 -敘 -教 -敛 -敜 -敝 -敞 -敟 -敢 -散 -敤 -敦 -敧 -敩 -敪 -敫 -敬 -敭 -敯 -数 -敲 -敳 -整 -敵 -敶 -敷 -數 -敹 -敺 -敻 -敾 -敿 -斀 -斁 -斂 -斃 -斄 -文 -斉 -斋 -斌 -斎 -斐 -斑 -斒 -斓 -斔 -斕 -斖 -斗 -料 -斛 -斜 -斝 -斞 -斟 -斠 -斡 -斤 -斥 -斧 -斨 -斩 -斪 -斫 -斬 -断 -斮 -斯 -新 -斲 -斵 -斶 -斷 -方 -斻 -於 -施 -斿 -旁 -旂 -旃 -旄 -旅 -旆 -旉 -旋 -旌 -旍 -旎 -族 -旐 -旒 -旓 -旖 -旗 -旙 -旚 -旛 -旝 -旞 -旟 -无 -旡 -既 -日 -旦 -旧 -旨 -早 -旬 -旭 -旮 -旯 -旰 -旱 -旲 -旳 -旴 -旵 -时 -旷 -旸 -旺 -旻 -旼 -旽 -旾 -旿 -昀 -昂 -昃 -昄 -昆 -昇 -昈 -昉 -昊 -昌 -昍 -明 -昏 -昐 -昒 -易 -昔 -昕 -昙 -昚 -昛 -昜 -昝 -昞 -星 -映 -昡 -昢 -昣 -昤 -春 -昦 -昧 -昨 -昪 -昫 -昬 -昭 -是 -昰 -昱 -昲 -昳 -昴 -昵 -昶 -昺 -昼 -昽 -显 -晁 -時 -晃 -晅 -晇 -晈 -晉 -晊 -晋 -晌 -晏 -晐 -晒 -晓 -晔 -晕 -晖 -晗 -晙 -晚 -晛 -晜 -晝 -晞 -晟 -晡 -晢 -晤 -晥 -晦 -晧 -晨 -晩 -晪 -晫 -晬 -晭 -普 -景 -晰 -晱 -晳 -晴 -晶 -晷 -晸 -智 -晻 -晼 -晾 -暀 -暁 -暂 -暃 -暄 -暅 -暆 -暇 -暈 -暉 -暊 -暋 -暌 -暍 -暎 -暏 -暐 -暑 -暕 -暖 -暗 -暘 -暝 -暟 -暠 -暡 -暢 -暦 -暧 -暨 -暩 -暫 -暮 -暰 -暱 -暲 -暴 -暵 -暶 -暸 -暹 -暻 -暾 -暿 -曀 -曄 -曅 -曆 -曇 -曈 -曉 -曊 -曋 -曌 -曏 -曒 -曔 -曖 -曙 -曚 -曛 -曜 -曝 -曠 -曡 -曣 -曦 -曧 -曨 -曩 -曬 -曭 -曮 -曰 -曲 -曳 -更 -曶 -曷 -書 -曹 -曺 -曼 -曽 -曾 -替 -最 -朁 -會 -朄 -朅 -月 -有 -朊 -朋 -服 -朏 -朐 -朒 -朓 -朔 -朕 -朖 -朗 -朘 -望 -朝 -朞 -期 -朡 -朢 -朣 -朦 -朧 -木 -未 -末 -本 -札 -朮 -术 -朱 -朳 -朴 -朵 -朶 -朸 -朹 -机 -朻 -朼 -朽 -朾 -朿 -杀 -杁 -杂 -权 -杄 -杅 -杆 -杇 -杈 -杉 -杋 -杌 -李 -杏 -材 -村 -杓 -杕 -杖 -杗 -杙 -杜 -杝 -杞 -束 -杠 -条 -杢 -杣 -杤 -来 -杧 -杨 -杩 -杪 -杬 -杭 -杮 -杯 -杰 -東 -杲 -杳 -杴 -杵 -杶 -杷 -杻 -杼 -松 -板 -极 -构 -枅 -枆 -枇 -枉 -枋 -枌 -枍 -枎 -枏 -析 -枒 -枓 -枕 -枖 -林 -枘 -枚 -枛 -果 -枝 -枞 -枠 -枡 -枢 -枣 -枥 -枧 -枨 -枪 -枫 -枭 -枯 -枰 -枱 -枲 -枳 -枴 -枵 -架 -枷 -枸 -枹 -枺 -枻 -枼 -柀 -柁 -柂 -柃 -柄 -柅 -柈 -柉 -柊 -柍 -柎 -柏 -某 -柑 -柒 -染 -柔 -柖 -柘 -柙 -柚 -柜 -柝 -柞 -柟 -柠 -柢 -柣 -柤 -查 -柧 -柩 -柫 -柬 -柮 -柯 -柰 -柱 -柲 -柳 -柴 -柵 -柶 -柷 -柸 -柹 -柺 -査 -柽 -柾 -柿 -栀 -栁 -栂 -栃 -栄 -栅 -标 -栈 -栉 -栊 -栋 -栌 -栎 -栏 -栐 -树 -栒 -栓 -栔 -栖 -栗 -栘 -栚 -栜 -栝 -栞 -栟 -校 -栢 -栨 -栩 -株 -栫 -栭 -栯 -栰 -栱 -栲 -栳 -栴 -栵 -样 -核 -根 -栻 -格 -栽 -栾 -桀 -桁 -桂 -桃 -桄 -桅 -框 -案 -桉 -桊 -桋 -桌 -桎 -桏 -桐 -桑 -桒 -桓 -桔 -桕 -桖 -桙 -桜 -桝 -桟 -桠 -桡 -桢 -档 -桤 -桥 -桦 -桧 -桨 -桩 -桫 -桭 -桮 -桯 -桱 -桲 -桴 -桵 -桶 -桷 -桹 -桺 -桼 -桾 -桿 -梀 -梁 -梂 -梃 -梅 -梆 -梇 -梉 -梌 -梏 -梐 -梒 -梓 -梔 -梗 -梘 -梛 -梜 -條 -梟 -梠 -梡 -梢 -梣 -梦 -梧 -梨 -梩 -梫 -梬 -梭 -梮 -梯 -械 -梱 -梲 -梳 -梴 -梵 -梶 -梼 -梽 -梾 -梿 -检 -棁 -棂 -棃 -棄 -棆 -棉 -棊 -棋 -棌 -棍 -棐 -棒 -棓 -棕 -棖 -棗 -棘 -棚 -棛 -棜 -棟 -棠 -棡 -棣 -棤 -棧 -棨 -棩 -棪 -棫 -棬 -森 -棯 -棰 -棱 -棲 -棳 -棵 -棶 -棷 -棸 -棹 -棺 -棻 -棼 -棽 -椀 -椁 -椄 -椅 -椆 -椇 -椈 -椉 -椋 -椌 -植 -椎 -椏 -椐 -椑 -椒 -椓 -椔 -椗 -椙 -検 -椟 -椠 -椤 -椥 -椪 -椭 -椰 -椲 -椳 -椴 -椵 -椶 -椸 -椹 -椽 -椿 -楀 -楂 -楄 -楅 -楈 -楉 -楊 -楎 -楒 -楓 -楔 -楕 -楖 -楗 -楘 -楙 -楚 -楛 -楝 -楞 -楟 -楠 -楡 -楢 -楣 -楤 -楥 -楦 -楧 -楨 -楩 -楪 -楫 -楬 -業 -楮 -楯 -楰 -楱 -楳 -極 -楶 -楷 -楸 -楹 -楺 -楼 -楽 -楿 -概 -榃 -榄 -榅 -榆 -榇 -榈 -榉 -榊 -榍 -榎 -榑 -榔 -榕 -榖 -榗 -榘 -榙 -榛 -榜 -榞 -榠 -榢 -榣 -榤 -榦 -榧 -榨 -榩 -榪 -榫 -榬 -榭 -榮 -榯 -榰 -榱 -榲 -榴 -榶 -榷 -榹 -榻 -榼 -榽 -榾 -榿 -槀 -槁 -槃 -槄 -槇 -槉 -槊 -構 -槌 -槍 -槎 -槐 -槑 -槓 -槔 -槕 -槗 -様 -槙 -槚 -槛 -槜 -槟 -槠 -槢 -槤 -槥 -槧 -槨 -槩 -槬 -槭 -槮 -槰 -槱 -槲 -槳 -槷 -槸 -槺 -槻 -槼 -槽 -槾 -槿 -樀 -樁 -樂 -樅 -樆 -樊 -樋 -樑 -樓 -樔 -樕 -樗 -樘 -標 -樛 -樝 -樞 -樟 -樠 -模 -樣 -樧 -樨 -権 -横 -樫 -樯 -樱 -樲 -樴 -樵 -樸 -樹 -樺 -樻 -樽 -樾 -樿 -橀 -橁 -橄 -橇 -橈 -橉 -橋 -橐 -橑 -橒 -橓 -橘 -橙 -橚 -橛 -橜 -橝 -橞 -機 -橡 -橢 -橤 -橥 -橦 -橧 -橨 -橪 -橫 -橭 -橱 -橶 -橹 -橼 -橾 -橿 -檀 -檁 -檃 -檄 -檇 -檉 -檊 -檍 -檎 -檐 -檑 -檓 -檔 -檕 -檖 -檗 -檛 -檜 -檝 -檞 -檟 -檠 -檡 -檢 -檣 -檥 -檦 -檨 -檩 -檫 -檬 -檮 -檯 -檳 -檵 -檸 -檹 -檻 -檽 -櫂 -櫃 -櫅 -櫆 -櫈 -櫋 -櫌 -櫐 -櫑 -櫓 -櫙 -櫚 -櫛 -櫜 -櫝 -櫞 -櫟 -櫠 -櫡 -櫥 -櫧 -櫨 -櫪 -櫫 -櫬 -櫰 -櫱 -櫳 -櫸 -櫹 -櫺 -櫻 -櫼 -櫾 -櫿 -欂 -欃 -欄 -欅 -欈 -欉 -權 -欋 -欏 -欐 -欑 -欒 -欖 -欘 -欞 -欠 -次 -欢 -欣 -欤 -欥 -欧 -欨 -欬 -欭 -欯 -欱 -欲 -欳 -欴 -欵 -欶 -欷 -欸 -欹 -欺 -欻 -欽 -款 -欿 -歂 -歃 -歅 -歆 -歇 -歈 -歉 -歊 -歋 -歌 -歍 -歎 -歐 -歓 -歔 -歕 -歙 -歛 -歜 -歟 -歠 -歡 -止 -正 -此 -步 -武 -歧 -歩 -歪 -歭 -歯 -歲 -歳 -歴 -歶 -歷 -歸 -歹 -死 -歼 -歾 -歿 -殀 -殁 -殂 -殃 -殄 -殆 -殇 -殈 -殉 -殊 -残 -殍 -殏 -殑 -殒 -殓 -殔 -殖 -殗 -殘 -殙 -殚 -殛 -殞 -殟 -殠 -殡 -殢 -殣 -殤 -殥 -殦 -殧 -殪 -殫 -殭 -殮 -殯 -殰 -殲 -殳 -殴 -段 -殶 -殷 -殺 -殻 -殼 -殽 -殿 -毀 -毁 -毂 -毃 -毄 -毅 -毆 -毇 -毈 -毉 -毊 -毋 -毌 -母 -毎 -每 -毐 -毒 -毓 -比 -毕 -毖 -毗 -毘 -毙 -毚 -毛 -毞 -毡 -毣 -毤 -毦 -毨 -毪 -毫 -毬 -毯 -毰 -毲 -毳 -毴 -毵 -毸 -毹 -毻 -毼 -毽 -毾 -毿 -氀 -氁 -氂 -氄 -氅 -氆 -氇 -氈 -氊 -氌 -氍 -氏 -氐 -民 -氓 -气 -氕 -氖 -気 -氘 -氙 -氚 -氛 -氟 -氠 -氡 -氢 -氣 -氤 -氦 -氧 -氨 -氩 -氪 -氫 -氬 -氮 -氯 -氰 -氲 -氳 -水 -氶 -氷 -永 -氹 -氻 -氽 -氾 -氿 -汀 -汁 -求 -汃 -汆 -汇 -汈 -汉 -汊 -汋 -汍 -汎 -汏 -汐 -汒 -汔 -汕 -汗 -汙 -汚 -汛 -汜 -汝 -汞 -江 -池 -污 -汤 -汦 -汧 -汨 -汩 -汪 -汫 -汭 -汯 -汰 -汱 -汲 -汳 -汴 -汶 -汸 -汹 -決 -汽 -汾 -沁 -沂 -沃 -沄 -沅 -沆 -沇 -沈 -沉 -沋 -沌 -沍 -沏 -沐 -沒 -沓 -沔 -沕 -沖 -沘 -沙 -沚 -沛 -沜 -沟 -没 -沢 -沣 -沤 -沥 -沦 -沧 -沨 -沩 -沪 -沫 -沬 -沭 -沮 -沯 -沱 -河 -沴 -沶 -沷 -沸 -油 -沺 -治 -沼 -沽 -沾 -沿 -況 -泂 -泃 -泄 -泅 -泆 -泇 -泉 -泊 -泌 -泐 -泒 -泓 -泔 -法 -泖 -泗 -泙 -泚 -泛 -泜 -泝 -泞 -泠 -泡 -波 -泣 -泥 -泧 -注 -泩 -泪 -泫 -泬 -泭 -泮 -泯 -泰 -泱 -泲 -泳 -泵 -泷 -泸 -泺 -泻 -泼 -泽 -泾 -洀 -洁 -洄 -洇 -洈 -洊 -洋 -洌 -洍 -洎 -洑 -洒 -洓 -洗 -洘 -洙 -洚 -洛 -洞 -洟 -洢 -洣 -洤 -津 -洧 -洨 -洩 -洪 -洫 -洭 -洮 -洱 -洲 -洳 -洴 -洵 -洶 -洸 -洹 -洺 -活 -洼 -洽 -派 -洿 -流 -浃 -浄 -浅 -浆 -浇 -浈 -浉 -浊 -测 -浍 -济 -浏 -浐 -浑 -浒 -浓 -浔 -浕 -浘 -浙 -浚 -浛 -浜 -浞 -浟 -浠 -浡 -浢 -浣 -浤 -浥 -浦 -浧 -浩 -浪 -浬 -浭 -浮 -浯 -浰 -浲 -浴 -浵 -浶 -海 -浸 -浹 -浺 -浼 -浿 -涂 -涄 -涅 -涆 -涇 -消 -涉 -涊 -涌 -涍 -涎 -涐 -涑 -涒 -涓 -涔 -涕 -涖 -涗 -涘 -涙 -涛 -涝 -涞 -涟 -涠 -涡 -涢 -涣 -涤 -润 -涧 -涨 -涩 -涪 -涫 -涬 -涮 -涯 -液 -涳 -涴 -涵 -涷 -涸 -涼 -涽 -涾 -涿 -淀 -淄 -淅 -淆 -淇 -淈 -淊 -淋 -淌 -淍 -淎 -淏 -淑 -淒 -淓 -淔 -淖 -淘 -淙 -淚 -淛 -淜 -淝 -淞 -淟 -淠 -淡 -淢 -淤 -淥 -淦 -淨 -淩 -淪 -淫 -淬 -淭 -淮 -淯 -淰 -深 -淳 -淴 -淵 -淶 -混 -淸 -淹 -淺 -添 -淼 -淽 -渀 -渃 -清 -渇 -済 -渉 -渊 -渋 -渌 -渍 -渎 -渐 -渑 -渓 -渔 -渕 -渗 -渙 -渚 -減 -渜 -渝 -渟 -渠 -渡 -渢 -渣 -渤 -渥 -渦 -渨 -温 -渫 -測 -渭 -渮 -港 -渰 -渱 -渲 -渳 -渴 -游 -渹 -渺 -渻 -渼 -渽 -渾 -渿 -湀 -湁 -湃 -湄 -湅 -湆 -湇 -湉 -湊 -湋 -湍 -湎 -湑 -湓 -湔 -湖 -湘 -湛 -湜 -湝 -湞 -湟 -湠 -湡 -湢 -湣 -湥 -湧 -湨 -湩 -湫 -湮 -湯 -湱 -湲 -湳 -湴 -湻 -湼 -湾 -湿 -満 -溁 -溃 -溅 -溆 -溇 -溈 -溉 -溋 -溍 -溎 -溏 -源 -溓 -溔 -準 -溘 -溙 -溚 -溛 -溜 -溝 -溞 -溟 -溠 -溢 -溥 -溦 -溧 -溪 -溫 -溮 -溯 -溰 -溱 -溲 -溳 -溴 -溵 -溶 -溷 -溹 -溺 -溻 -溼 -溽 -溾 -滀 -滁 -滂 -滃 -滄 -滅 -滆 -滇 -滈 -滉 -滋 -滌 -滍 -滎 -滏 -滑 -滒 -滓 -滔 -滕 -滖 -滗 -滘 -滙 -滚 -滛 -滜 -滝 -滞 -滟 -滠 -满 -滢 -滤 -滥 -滦 -滧 -滨 -滩 -滪 -滫 -滬 -滭 -滮 -滯 -滱 -滲 -滴 -滵 -滷 -滸 -滹 -滻 -滽 -滾 -滿 -漁 -漂 -漃 -漅 -漆 -漇 -漈 -漉 -漊 -漋 -漎 -漏 -漑 -漒 -漓 -演 -漕 -漖 -漘 -漙 -漚 -漠 -漢 -漣 -漤 -漥 -漦 -漧 -漩 -漪 -漫 -漬 -漭 -漮 -漯 -漰 -漱 -漲 -漳 -漴 -漵 -漶 -漷 -漸 -漹 -漻 -漼 -漾 -漿 -潀 -潁 -潃 -潄 -潅 -潆 -潇 -潋 -潍 -潎 -潏 -潐 -潑 -潒 -潔 -潕 -潖 -潗 -潘 -潙 -潚 -潛 -潜 -潝 -潞 -潟 -潠 -潢 -潤 -潦 -潩 -潬 -潭 -潮 -潯 -潰 -潲 -潳 -潴 -潵 -潶 -潷 -潸 -潺 -潼 -潽 -潾 -潿 -澀 -澁 -澂 -澄 -澅 -澆 -澇 -澈 -澉 -澋 -澌 -澍 -澎 -澐 -澒 -澓 -澔 -澖 -澗 -澛 -澜 -澞 -澠 -澡 -澣 -澤 -澥 -澦 -澧 -澨 -澩 -澪 -澫 -澭 -澮 -澯 -澱 -澳 -澴 -澶 -澹 -澻 -澼 -澽 -激 -濁 -濂 -濃 -濄 -濆 -濇 -濈 -濉 -濊 -濋 -濌 -濎 -濑 -濒 -濔 -濕 -濘 -濙 -濛 -濜 -濞 -濟 -濠 -濡 -濣 -濤 -濦 -濧 -濩 -濫 -濬 -濭 -濮 -濯 -濰 -濱 -濲 -濴 -濶 -濺 -濻 -濼 -濾 -濿 -瀀 -瀁 -瀄 -瀅 -瀆 -瀇 -瀉 -瀋 -瀌 -瀍 -瀎 -瀏 -瀑 -瀔 -瀕 -瀖 -瀘 -瀙 -瀚 -瀛 -瀝 -瀞 -瀟 -瀠 -瀡 -瀢 -瀣 -瀦 -瀧 -瀨 -瀩 -瀫 -瀬 -瀯 -瀰 -瀱 -瀲 -瀴 -瀵 -瀶 -瀷 -瀸 -瀹 -瀺 -瀼 -瀾 -瀿 -灂 -灃 -灄 -灈 -灉 -灊 -灋 -灌 -灏 -灑 -灕 -灖 -灗 -灘 -灛 -灝 -灞 -灟 -灡 -灣 -灤 -灧 -灨 -灩 -灪 -火 -灭 -灯 -灰 -灴 -灵 -灶 -灸 -灼 -災 -灾 -灿 -炀 -炁 -炅 -炆 -炉 -炊 -炌 -炎 -炒 -炔 -炕 -炖 -炘 -炙 -炜 -炝 -炟 -炣 -炤 -炫 -炬 -炭 -炮 -炯 -炰 -炱 -炲 -炳 -炷 -炸 -点 -為 -炻 -炼 -炽 -炾 -烀 -烁 -烂 -烃 -烈 -烉 -烊 -烋 -烏 -烑 -烒 -烓 -烔 -烖 -烘 -烙 -烛 -烜 -烝 -烟 -烠 -烤 -烦 -烧 -烨 -烩 -烫 -烬 -热 -烯 -烰 -烱 -烴 -烶 -烷 -烹 -烺 -烻 -烽 -焃 -焄 -焆 -焉 -焊 -焌 -焍 -焐 -焓 -焔 -焕 -焖 -焗 -焘 -焙 -焚 -焜 -焞 -焠 -無 -焣 -焦 -焮 -焯 -焰 -焱 -焴 -然 -焻 -焼 -焿 -煁 -煃 -煅 -煆 -煇 -煉 -煊 -煋 -煌 -煎 -煐 -煑 -煒 -煓 -煔 -煕 -煖 -煗 -煙 -煚 -煜 -煝 -煞 -煟 -煠 -煢 -煣 -煤 -煥 -煦 -照 -煨 -煩 -煬 -煮 -煲 -煳 -煴 -煵 -煶 -煸 -煺 -煽 -熀 -熂 -熄 -熅 -熇 -熈 -熉 -熊 -熏 -熐 -熒 -熔 -熖 -熗 -熘 -熙 -熚 -熛 -熜 -熟 -熠 -熤 -熥 -熨 -熬 -熯 -熰 -熱 -熲 -熳 -熵 -熸 -熹 -熺 -熻 -熼 -熾 -熿 -燀 -燁 -燂 -燃 -燄 -燅 -燈 -燉 -燊 -燋 -燎 -燏 -燐 -燒 -燔 -燕 -燖 -燗 -燘 -燙 -燚 -燜 -燝 -營 -燠 -燡 -燥 -燦 -燧 -燫 -燬 -燭 -燮 -燴 -燹 -燻 -燼 -燾 -燿 -爀 -爁 -爂 -爆 -爇 -爊 -爌 -爍 -爐 -爓 -爔 -爕 -爚 -爛 -爝 -爞 -爟 -爢 -爣 -爨 -爪 -爬 -爭 -爯 -爰 -爱 -爲 -爵 -父 -爷 -爸 -爹 -爺 -爻 -爼 -爽 -爾 -爿 -牀 -牁 -牂 -牄 -牆 -片 -版 -牉 -牋 -牌 -牍 -牎 -牏 -牒 -牓 -牕 -牖 -牘 -牙 -牚 -牛 -牝 -牟 -牠 -牡 -牢 -牣 -牤 -牥 -牦 -牧 -物 -牬 -牮 -牯 -牰 -牲 -牴 -牵 -牷 -牸 -特 -牺 -牻 -牼 -牽 -牾 -牿 -犀 -犁 -犂 -犄 -犅 -犆 -犇 -犈 -犉 -犊 -犋 -犌 -犍 -犎 -犏 -犐 -犑 -犒 -犓 -犕 -犖 -犗 -犘 -犚 -犛 -犝 -犞 -犟 -犠 -犢 -犣 -犤 -犥 -犦 -犧 -犨 -犩 -犪 -犬 -犮 -犯 -犰 -犴 -犵 -状 -犷 -犸 -犹 -犽 -犿 -狀 -狁 -狂 -狃 -狄 -狆 -狈 -狉 -狊 -狋 -狌 -狍 -狎 -狐 -狑 -狒 -狓 -狔 -狖 -狗 -狘 -狙 -狚 -狛 -狜 -狝 -狞 -狟 -狠 -狡 -狣 -狤 -狥 -狦 -狨 -狩 -狪 -狫 -独 -狭 -狮 -狯 -狰 -狱 -狲 -狳 -狴 -狶 -狷 -狸 -狹 -狺 -狻 -狼 -狽 -狾 -狿 -猀 -猁 -猂 -猃 -猄 -猇 -猈 -猊 -猋 -猎 -猏 -猑 -猒 -猓 -猕 -猖 -猗 -猘 -猙 -猛 -猜 -猝 -猞 -猟 -猡 -猢 -猣 -猥 -猦 -猧 -猨 -猩 -猪 -猫 -猬 -猭 -献 -猯 -猰 -猱 -猲 -猳 -猴 -猵 -猶 -猷 -猹 -猺 -猻 -猼 -猾 -猿 -獀 -獁 -獂 -獃 -獄 -獅 -獇 -獊 -獋 -獌 -獍 -獎 -獏 -獐 -獑 -獒 -獗 -獘 -獛 -獝 -獞 -獟 -獠 -獡 -獢 -獣 -獥 -獦 -獧 -獨 -獩 -獪 -獫 -獬 -獭 -獮 -獯 -獰 -獲 -獳 -獴 -獵 -獶 -獷 -獸 -獺 -獻 -獼 -獽 -獾 -獿 -玀 -玁 -玂 -玃 -玄 -玅 -玆 -率 -玈 -玉 -玊 -王 -玎 -玏 -玑 -玒 -玓 -玔 -玕 -玖 -玗 -玘 -玙 -玚 -玛 -玞 -玟 -玠 -玡 -玢 -玤 -玥 -玦 -玧 -玨 -玩 -玫 -玭 -玮 -环 -现 -玱 -玲 -玳 -玶 -玷 -玹 -玺 -玻 -玼 -玾 -玿 -珀 -珂 -珅 -珇 -珈 -珉 -珊 -珋 -珌 -珍 -珎 -珏 -珐 -珑 -珒 -珓 -珕 -珖 -珙 -珛 -珝 -珞 -珠 -珡 -珢 -珣 -珤 -珥 -珦 -珧 -珩 -珪 -珫 -班 -珮 -珰 -珲 -珵 -珶 -珷 -珸 -珹 -珺 -珽 -現 -琀 -琁 -球 -琄 -琅 -理 -琇 -琈 -琉 -琊 -琍 -琎 -琏 -琐 -琔 -琖 -琚 -琛 -琟 -琡 -琢 -琤 -琥 -琦 -琨 -琪 -琫 -琬 -琭 -琮 -琯 -琰 -琱 -琲 -琳 -琴 -琵 -琶 -琹 -琺 -琼 -琿 -瑀 -瑁 -瑂 -瑃 -瑄 -瑅 -瑆 -瑇 -瑈 -瑊 -瑋 -瑑 -瑒 -瑓 -瑔 -瑕 -瑖 -瑗 -瑙 -瑚 -瑛 -瑜 -瑝 -瑞 -瑟 -瑠 -瑢 -瑣 -瑤 -瑥 -瑧 -瑨 -瑩 -瑪 -瑬 -瑭 -瑯 -瑰 -瑱 -瑲 -瑳 -瑴 -瑵 -瑶 -瑷 -瑺 -瑽 -瑾 -瑿 -璀 -璁 -璃 -璄 -璅 -璆 -璇 -璈 -璉 -璊 -璋 -璌 -璎 -璐 -璒 -璕 -璗 -璘 -璙 -璚 -璜 -璞 -璟 -璠 -璡 -璢 -璣 -璥 -璦 -璧 -璨 -璩 -璪 -璫 -璬 -璮 -環 -璱 -璲 -璵 -璸 -璹 -璺 -璽 -璿 -瓀 -瓁 -瓅 -瓈 -瓊 -瓋 -瓌 -瓏 -瓑 -瓒 -瓔 -瓖 -瓘 -瓚 -瓛 -瓜 -瓝 -瓞 -瓟 -瓠 -瓡 -瓢 -瓣 -瓤 -瓥 -瓦 -瓨 -瓩 -瓬 -瓮 -瓯 -瓴 -瓵 -瓶 -瓷 -瓻 -瓽 -瓾 -瓿 -甀 -甂 -甃 -甄 -甈 -甋 -甌 -甍 -甏 -甐 -甑 -甒 -甓 -甔 -甕 -甖 -甗 -甘 -甙 -甚 -甜 -甝 -甞 -生 -甡 -產 -産 -甥 -甦 -用 -甩 -甪 -甫 -甬 -甭 -甮 -甯 -田 -由 -甲 -申 -电 -男 -甸 -甹 -町 -画 -甽 -甾 -甿 -畀 -畅 -畇 -畈 -畊 -畋 -界 -畎 -畏 -畐 -畑 -畔 -畖 -留 -畚 -畛 -畜 -畝 -畟 -畠 -畢 -畣 -畤 -略 -畦 -畧 -番 -畫 -畬 -畯 -異 -畱 -畲 -畳 -畴 -畵 -當 -畷 -畸 -畹 -畺 -畽 -畿 -疀 -疁 -疃 -疆 -疇 -疊 -疋 -疌 -疍 -疎 -疏 -疐 -疑 -疒 -疔 -疕 -疖 -疗 -疘 -疙 -疚 -疝 -疟 -疠 -疡 -疢 -疣 -疤 -疥 -疧 -疫 -疬 -疭 -疮 -疯 -疰 -疱 -疲 -疳 -疴 -疵 -疸 -疹 -疻 -疼 -疽 -疾 -疿 -痀 -痁 -痂 -痃 -痄 -病 -症 -痈 -痉 -痊 -痋 -痌 -痍 -痎 -痏 -痐 -痑 -痒 -痓 -痔 -痕 -痗 -痘 -痙 -痚 -痛 -痝 -痞 -痟 -痠 -痡 -痢 -痣 -痤 -痦 -痧 -痨 -痩 -痪 -痫 -痯 -痰 -痱 -痲 -痳 -痴 -痷 -痸 -痹 -痺 -痻 -痼 -痾 -痿 -瘀 -瘁 -瘃 -瘅 -瘆 -瘈 -瘉 -瘊 -瘋 -瘌 -瘍 -瘏 -瘐 -瘑 -瘓 -瘕 -瘖 -瘗 -瘘 -瘙 -瘛 -瘜 -瘝 -瘞 -瘟 -瘠 -瘡 -瘢 -瘣 -瘤 -瘥 -瘦 -瘧 -瘩 -瘪 -瘫 -瘭 -瘮 -瘯 -瘰 -瘱 -瘲 -瘳 -瘴 -瘵 -瘸 -瘺 -瘻 -瘼 -瘽 -瘾 -瘿 -癀 -療 -癃 -癄 -癅 -癆 -癇 -癈 -癉 -癌 -癍 -癐 -癒 -癓 -癔 -癖 -癗 -癘 -癙 -癜 -癞 -癟 -癠 -癡 -癢 -癣 -癤 -癥 -癧 -癩 -癪 -癫 -癬 -癭 -癮 -癯 -癰 -癱 -癲 -癵 -癸 -癹 -発 -登 -發 -白 -百 -癿 -皁 -皂 -的 -皆 -皇 -皈 -皊 -皋 -皎 -皏 -皐 -皑 -皓 -皕 -皖 -皙 -皚 -皛 -皜 -皝 -皞 -皤 -皦 -皫 -皭 -皮 -皯 -皰 -皱 -皲 -皴 -皵 -皷 -皸 -皺 -皻 -皽 -皾 -皿 -盂 -盃 -盄 -盅 -盆 -盇 -盈 -盉 -益 -盋 -盌 -盍 -盎 -盏 -盐 -监 -盒 -盓 -盔 -盖 -盗 -盘 -盛 -盜 -盝 -盞 -盟 -盡 -監 -盤 -盥 -盦 -盧 -盨 -盩 -盪 -盫 -盬 -盭 -目 -盯 -盱 -盲 -盳 -直 -盷 -相 -盹 -盺 -盻 -盼 -盾 -眀 -省 -眃 -眄 -眅 -眇 -眈 -眉 -眊 -看 -県 -眍 -眐 -眑 -眒 -眓 -眕 -眙 -眚 -眛 -眜 -眝 -眞 -真 -眠 -眢 -眣 -眥 -眦 -眨 -眩 -眬 -眭 -眯 -眱 -眲 -眳 -眴 -眵 -眶 -眷 -眸 -眹 -眺 -眼 -眽 -眾 -着 -睁 -睃 -睄 -睅 -睆 -睇 -睊 -睋 -睌 -睍 -睎 -睏 -睐 -睑 -睒 -睔 -睕 -睖 -睚 -睛 -睜 -睞 -睟 -睠 -睡 -睢 -督 -睥 -睦 -睧 -睨 -睩 -睪 -睫 -睬 -睭 -睮 -睯 -睹 -睺 -睼 -睽 -睾 -睿 -瞀 -瞁 -瞂 -瞄 -瞅 -瞇 -瞉 -瞋 -瞌 -瞍 -瞎 -瞏 -瞑 -瞒 -瞓 -瞖 -瞘 -瞚 -瞛 -瞜 -瞝 -瞞 -瞟 -瞠 -瞡 -瞢 -瞥 -瞧 -瞩 -瞪 -瞫 -瞬 -瞭 -瞰 -瞲 -瞳 -瞴 -瞵 -瞶 -瞷 -瞻 -瞼 -瞽 -瞿 -矁 -矂 -矇 -矉 -矊 -矌 -矍 -矎 -矏 -矐 -矓 -矔 -矕 -矗 -矘 -矙 -矚 -矛 -矜 -矞 -矠 -矢 -矣 -知 -矧 -矩 -矫 -矬 -短 -矮 -矯 -矰 -矱 -矲 -石 -矴 -矶 -矸 -矹 -矺 -矻 -矼 -矽 -矾 -矿 -砀 -码 -砂 -砄 -砅 -砆 -砉 -砋 -砌 -砍 -砎 -砏 -砐 -砑 -砒 -研 -砕 -砖 -砗 -砘 -砚 -砜 -砝 -砟 -砠 -砢 -砣 -砥 -砦 -砧 -砨 -砩 -砫 -砬 -砭 -砮 -砯 -砰 -砲 -砳 -破 -砵 -砷 -砸 -砹 -砺 -砻 -砼 -砾 -础 -硁 -硂 -硃 -硅 -硇 -硈 -硉 -硊 -硌 -硍 -硎 -硏 -硐 -硒 -硓 -硔 -硕 -硖 -硗 -硙 -硚 -硜 -硝 -硠 -硤 -硥 -硨 -硩 -硪 -硫 -硬 -硭 -确 -硯 -硰 -硱 -硼 -硾 -硿 -碁 -碃 -碄 -碅 -碆 -碇 -碈 -碉 -碌 -碍 -碎 -碏 -碑 -碓 -碔 -碕 -碗 -碘 -碚 -碛 -碜 -碞 -碟 -碡 -碣 -碥 -碧 -碨 -碩 -碪 -碫 -碭 -碰 -碱 -碲 -碳 -碴 -碶 -碸 -碹 -確 -碻 -碼 -碽 -碾 -磁 -磃 -磅 -磈 -磉 -磊 -磋 -磌 -磍 -磎 -磏 -磐 -磑 -磔 -磕 -磘 -磙 -磚 -磛 -磜 -磝 -磞 -磟 -磡 -磣 -磥 -磧 -磨 -磩 -磪 -磬 -磯 -磱 -磲 -磳 -磴 -磵 -磷 -磹 -磺 -磻 -磼 -磽 -磾 -磿 -礁 -礂 -礄 -礅 -礉 -礌 -礎 -礐 -礑 -礒 -礓 -礔 -礙 -礛 -礜 -礝 -礞 -礠 -礣 -礥 -礦 -礧 -礨 -礪 -礫 -礬 -礭 -礮 -礱 -礴 -礵 -示 -礼 -礽 -社 -礿 -祀 -祁 -祂 -祃 -祅 -祆 -祇 -祈 -祉 -祊 -祋 -祌 -祎 -祏 -祐 -祑 -祒 -祓 -祔 -祕 -祖 -祗 -祙 -祚 -祛 -祜 -祝 -神 -祟 -祠 -祡 -祢 -祣 -祤 -祥 -祧 -票 -祩 -祪 -祫 -祭 -祯 -祲 -祳 -祴 -祷 -祸 -祹 -祺 -祼 -祾 -祿 -禀 -禁 -禂 -禃 -禄 -禅 -禇 -禊 -禋 -禍 -禎 -福 -禑 -禒 -禓 -禔 -禕 -禖 -禗 -禘 -禚 -禛 -禜 -禟 -禠 -禡 -禤 -禦 -禧 -禨 -禩 -禪 -禫 -禬 -禭 -禮 -禰 -禱 -禳 -禴 -禵 -禷 -禸 -禹 -禺 -离 -禼 -禽 -禾 -禿 -秀 -私 -秃 -秅 -秆 -秈 -秉 -秊 -秋 -种 -秎 -秏 -科 -秒 -秔 -秕 -秖 -秘 -租 -秠 -秣 -秤 -秦 -秧 -秩 -秪 -秫 -秬 -秭 -秮 -积 -称 -秳 -秶 -秸 -秺 -移 -秽 -秾 -稀 -稂 -稃 -稅 -稆 -稈 -稉 -稊 -程 -稌 -稍 -税 -稑 -稒 -稔 -稗 -稘 -稙 -稚 -稜 -稞 -稟 -稠 -稣 -稫 -稬 -稭 -種 -稯 -稰 -稱 -稲 -稳 -稷 -稹 -稺 -稻 -稼 -稽 -稾 -稿 -穀 -穂 -穄 -穅 -穆 -穇 -穈 -穉 -穊 -穋 -穌 -積 -穎 -穏 -穑 -穖 -穗 -穙 -穛 -穜 -穟 -穠 -穡 -穢 -穣 -穤 -穧 -穨 -穩 -穫 -穬 -穭 -穮 -穰 -穱 -穴 -穵 -究 -穷 -穸 -穹 -空 -穽 -穾 -穿 -窀 -突 -窃 -窄 -窅 -窆 -窈 -窉 -窊 -窋 -窌 -窍 -窎 -窏 -窐 -窑 -窒 -窓 -窔 -窕 -窖 -窗 -窘 -窙 -窜 -窝 -窞 -窟 -窠 -窢 -窣 -窥 -窦 -窨 -窩 -窪 -窫 -窬 -窭 -窮 -窯 -窰 -窱 -窲 -窳 -窴 -窵 -窶 -窸 -窺 -窻 -窾 -窿 -竀 -竁 -竃 -竄 -竅 -竇 -竈 -竊 -立 -竑 -竖 -竘 -站 -竚 -竜 -竝 -竞 -竟 -章 -竢 -竣 -童 -竦 -竩 -竪 -竫 -竭 -端 -竴 -競 -竷 -竹 -竺 -竻 -竽 -竿 -笃 -笄 -笅 -笆 -笈 -笊 -笋 -笏 -笐 -笑 -笓 -笔 -笕 -笘 -笙 -笛 -笝 -笞 -笠 -笢 -笤 -笥 -符 -笨 -笩 -笪 -笫 -第 -笭 -笮 -笯 -笰 -笱 -笲 -笳 -笴 -笵 -笸 -笹 -笺 -笻 -笼 -笾 -筀 -筄 -筅 -筆 -筇 -筈 -等 -筊 -筋 -筌 -筍 -筏 -筐 -筑 -筒 -答 -策 -筘 -筚 -筛 -筜 -筝 -筞 -筠 -筡 -筢 -筤 -筥 -筦 -筧 -筩 -筬 -筭 -筮 -筯 -筰 -筱 -筲 -筳 -筴 -筵 -筶 -筷 -筹 -筻 -筼 -签 -简 -箄 -箅 -箆 -箇 -箈 -箊 -箋 -箌 -箍 -箎 -箏 -箐 -箑 -箒 -箓 -箔 -箕 -箖 -算 -箘 -箙 -箛 -箜 -箝 -箠 -管 -箢 -箦 -箧 -箨 -箩 -箪 -箫 -箬 -箭 -箯 -箱 -箴 -箷 -箸 -箹 -箾 -節 -篁 -範 -篆 -篇 -築 -篊 -篋 -篌 -篎 -篑 -篓 -篔 -篕 -篙 -篚 -篛 -篝 -篞 -篠 -篡 -篣 -篤 -篥 -篦 -篧 -篨 -篩 -篪 -篭 -篮 -篯 -篱 -篲 -篳 -篴 -篷 -篸 -篹 -篻 -篼 -篽 -篾 -篿 -簀 -簁 -簂 -簃 -簅 -簇 -簉 -簋 -簌 -簍 -簎 -簏 -簑 -簒 -簕 -簖 -簗 -簙 -簜 -簝 -簞 -簟 -簠 -簡 -簢 -簣 -簥 -簦 -簧 -簨 -簩 -簪 -簫 -簬 -簭 -簮 -簰 -簳 -簷 -簸 -簹 -簺 -簻 -簼 -簽 -簾 -簿 -籀 -籁 -籃 -籈 -籉 -籊 -籌 -籍 -籐 -籔 -籗 -籙 -籛 -籜 -籝 -籟 -籠 -籣 -籤 -籥 -籦 -籧 -籩 -籪 -籬 -籮 -籯 -籲 -米 -籴 -籸 -籹 -籺 -类 -籼 -籽 -籾 -粂 -粃 -粄 -粇 -粉 -粊 -粋 -粍 -粑 -粒 -粔 -粕 -粗 -粘 -粛 -粜 -粝 -粞 -粟 -粢 -粤 -粥 -粦 -粧 -粩 -粪 -粮 -粱 -粲 -粳 -粵 -粹 -粺 -粻 -粼 -粽 -精 -粿 -糀 -糁 -糅 -糇 -糈 -糉 -糊 -糌 -糍 -糎 -糒 -糔 -糕 -糖 -糗 -糙 -糜 -糝 -糞 -糟 -糠 -糢 -糧 -糨 -糪 -糬 -糯 -糰 -糱 -糲 -糴 -糵 -糶 -糷 -糸 -糹 -糺 -系 -糾 -紀 -紂 -紃 -約 -紅 -紆 -紇 -紈 -紉 -紊 -紋 -納 -紎 -紐 -紑 -紒 -紓 -純 -紕 -紖 -紗 -紘 -紙 -級 -紛 -紜 -紝 -紞 -紟 -素 -紡 -索 -紥 -紧 -紩 -紫 -紬 -紮 -累 -細 -紱 -紲 -紳 -紵 -紶 -紸 -紹 -紺 -紼 -紽 -紾 -紿 -絀 -絁 -終 -絃 -組 -絅 -絆 -絇 -経 -絎 -絏 -結 -絓 -絕 -絖 -絘 -絛 -絜 -絝 -絞 -絡 -絢 -絣 -給 -絧 -絨 -絪 -絫 -絭 -絮 -絯 -絰 -統 -絲 -絳 -絵 -絶 -絷 -絹 -絺 -絻 -絼 -絽 -絿 -綀 -綁 -綃 -綄 -綅 -綆 -綈 -綉 -綌 -綍 -綎 -綏 -綑 -經 -綔 -綖 -継 -続 -綜 -綝 -綞 -綟 -綠 -綢 -綣 -綦 -綧 -綪 -綫 -綬 -維 -綮 -綯 -綰 -綱 -網 -綳 -綴 -綵 -綷 -綸 -綹 -綺 -綻 -綼 -綽 -綾 -綿 -緀 -緁 -緂 -緃 -緄 -緅 -緆 -緇 -緈 -緊 -緋 -緌 -緎 -総 -緐 -緑 -緒 -緔 -緖 -緗 -緘 -緙 -線 -緛 -緜 -緝 -緞 -締 -緡 -緣 -緤 -緥 -緦 -緧 -編 -緩 -緪 -緬 -緯 -緰 -緱 -緲 -緳 -練 -緵 -緶 -緷 -緹 -緺 -緻 -縁 -縂 -縃 -縄 -縅 -縈 -縉 -縊 -縋 -縌 -縎 -縏 -縐 -縑 -縒 -縓 -縕 -縗 -縚 -縛 -縜 -縝 -縞 -縟 -縠 -縡 -縢 -縣 -縤 -縦 -縧 -縩 -縪 -縫 -縭 -縮 -縯 -縰 -縱 -縲 -縳 -縴 -縵 -縶 -縷 -縸 -縹 -縻 -縼 -總 -績 -縿 -繀 -繁 -繂 -繃 -繄 -繅 -繆 -繇 -繈 -繉 -繊 -繋 -繍 -繐 -繑 -繒 -織 -繕 -繖 -繘 -繙 -繚 -繜 -繝 -繞 -繟 -繠 -繡 -繢 -繣 -繦 -繧 -繩 -繪 -繫 -繭 -繮 -繯 -繰 -繲 -繳 -繴 -繵 -繶 -繷 -繸 -繹 -繺 -繻 -繼 -繽 -繾 -纀 -纁 -纂 -纆 -纇 -纈 -纊 -纋 -續 -纍 -纏 -纐 -纑 -纒 -纓 -纔 -纕 -纖 -纗 -纘 -纚 -纛 -纜 -纠 -纡 -红 -纣 -纤 -纥 -约 -级 -纨 -纩 -纪 -纫 -纬 -纭 -纮 -纯 -纰 -纱 -纲 -纳 -纴 -纵 -纶 -纷 -纸 -纹 -纺 -纻 -纼 -纽 -纾 -线 -绀 -绁 -绂 -练 -组 -绅 -细 -织 -终 -绉 -绊 -绋 -绌 -绍 -绎 -经 -绐 -绑 -绒 -结 -绔 -绕 -绖 -绗 -绘 -给 -绚 -绛 -络 -绝 -绞 -统 -绠 -绡 -绢 -绣 -绤 -绥 -绦 -继 -绨 -绩 -绪 -绫 -续 -绮 -绯 -绰 -绱 -绲 -绳 -维 -绵 -绶 -绷 -绸 -绹 -绺 -绻 -综 -绽 -绾 -绿 -缀 -缁 -缂 -缃 -缄 -缅 -缆 -缇 -缈 -缉 -缊 -缌 -缎 -缐 -缑 -缒 -缓 -缔 -缕 -编 -缗 -缘 -缙 -缚 -缛 -缜 -缝 -缞 -缟 -缠 -缡 -缢 -缣 -缤 -缥 -缦 -缧 -缨 -缩 -缪 -缫 -缬 -缭 -缮 -缯 -缰 -缱 -缲 -缳 -缴 -缵 -缶 -缸 -缹 -缺 -缽 -缾 -缿 -罂 -罃 -罄 -罅 -罇 -罈 -罉 -罊 -罋 -罌 -罍 -罎 -罏 -罐 -网 -罔 -罕 -罗 -罘 -罚 -罛 -罜 -罝 -罞 -罟 -罠 -罡 -罢 -罣 -罥 -罦 -罧 -罨 -罩 -罪 -罫 -罬 -罭 -置 -罰 -罱 -署 -罳 -罴 -罵 -罶 -罷 -罹 -罺 -罻 -罼 -罽 -罾 -罿 -羁 -羂 -羃 -羅 -羆 -羇 -羈 -羉 -羊 -羋 -羌 -羍 -美 -羑 -羒 -羓 -羔 -羕 -羖 -羗 -羙 -羚 -羛 -羜 -羝 -羞 -羟 -羠 -羡 -羢 -羣 -群 -羥 -羦 -羧 -羨 -義 -羬 -羭 -羯 -羰 -羱 -羲 -羳 -羴 -羵 -羶 -羷 -羸 -羹 -羺 -羼 -羽 -羾 -羿 -翀 -翁 -翂 -翃 -翄 -翅 -翇 -翈 -翊 -翋 -翌 -翍 -翎 -翏 -翐 -翑 -習 -翔 -翕 -翘 -翙 -翚 -翛 -翜 -翟 -翠 -翡 -翢 -翣 -翥 -翦 -翨 -翩 -翪 -翫 -翬 -翭 -翮 -翯 -翰 -翱 -翲 -翳 -翴 -翷 -翹 -翺 -翻 -翼 -翽 -翾 -翿 -耀 -老 -考 -耄 -者 -耆 -耇 -耋 -而 -耍 -耎 -耏 -耐 -耑 -耒 -耔 -耕 -耖 -耗 -耘 -耙 -耜 -耞 -耟 -耠 -耡 -耢 -耤 -耥 -耦 -耧 -耨 -耩 -耪 -耬 -耮 -耰 -耱 -耳 -耴 -耵 -耶 -耷 -耸 -耹 -耻 -耽 -耾 -耿 -聂 -聃 -聆 -聇 -聈 -聊 -聋 -职 -聍 -聏 -聑 -聒 -联 -聖 -聘 -聚 -聝 -聞 -聟 -聡 -聨 -聩 -聪 -聯 -聰 -聱 -聲 -聳 -聴 -聵 -聶 -職 -聹 -聽 -聾 -聿 -肂 -肃 -肄 -肅 -肆 -肇 -肉 -肋 -肌 -肎 -肏 -肐 -肒 -肓 -肕 -肖 -肘 -肙 -肚 -肛 -肜 -肝 -肟 -肠 -股 -肢 -肣 -肤 -肥 -肧 -肩 -肪 -肫 -肭 -肮 -肯 -肱 -育 -肴 -肵 -肷 -肸 -肹 -肺 -肼 -肽 -肾 -肿 -胀 -胁 -胂 -胃 -胄 -胅 -胆 -胇 -胈 -胉 -胊 -背 -胍 -胎 -胏 -胐 -胑 -胔 -胕 -胖 -胗 -胘 -胙 -胚 -胛 -胜 -胝 -胞 -胠 -胡 -胣 -胤 -胥 -胧 -胨 -胩 -胪 -胫 -胬 -胭 -胯 -胰 -胱 -胲 -胳 -胴 -胵 -胶 -胷 -胸 -胹 -胺 -胻 -胼 -能 -胾 -脀 -脁 -脂 -脃 -脅 -脆 -脇 -脈 -脉 -脊 -脍 -脎 -脏 -脐 -脑 -脒 -脓 -脔 -脕 -脖 -脗 -脘 -脙 -脚 -脛 -脞 -脟 -脡 -脣 -脤 -脥 -脧 -脩 -脫 -脬 -脭 -脯 -脰 -脱 -脲 -脳 -脶 -脷 -脸 -脹 -脽 -脾 -脿 -腃 -腄 -腆 -腇 -腈 -腊 -腋 -腌 -腍 -腎 -腏 -腐 -腑 -腒 -腓 -腔 -腕 -腖 -腘 -腙 -腚 -腛 -腜 -腞 -腠 -腢 -腤 -腥 -腦 -腧 -腨 -腩 -腫 -腭 -腮 -腯 -腰 -腱 -腲 -腳 -腴 -腶 -腷 -腸 -腹 -腺 -腻 -腼 -腽 -腾 -腿 -膀 -膂 -膃 -膆 -膇 -膈 -膉 -膊 -膋 -膌 -膍 -膏 -膑 -膕 -膘 -膙 -膚 -膛 -膜 -膝 -膞 -膟 -膠 -膢 -膣 -膥 -膦 -膨 -膩 -膫 -膬 -膮 -膰 -膱 -膲 -膳 -膴 -膵 -膷 -膹 -膺 -膻 -膽 -膾 -膿 -臀 -臂 -臃 -臄 -臅 -臆 -臇 -臈 -臉 -臊 -臋 -臌 -臍 -臏 -臐 -臑 -臒 -臓 -臕 -臗 -臘 -臙 -臚 -臛 -臜 -臝 -臞 -臟 -臠 -臡 -臢 -臣 -臥 -臦 -臧 -臨 -臩 -自 -臬 -臭 -臮 -臯 -臲 -至 -致 -臷 -臸 -臺 -臻 -臼 -臾 -臿 -舀 -舁 -舂 -舄 -舅 -舆 -與 -興 -舉 -舊 -舋 -舌 -舍 -舎 -舐 -舑 -舒 -舔 -舕 -舖 -舗 -舘 -舛 -舜 -舝 -舞 -舟 -舠 -舡 -舢 -舣 -舥 -舨 -舩 -航 -舫 -般 -舭 -舯 -舰 -舱 -舲 -舳 -舴 -舵 -舶 -舷 -舸 -船 -舺 -舻 -舼 -舽 -舾 -艀 -艂 -艄 -艅 -艇 -艉 -艋 -艎 -艏 -艐 -艑 -艒 -艓 -艔 -艕 -艘 -艙 -艚 -艛 -艟 -艤 -艦 -艨 -艫 -艮 -良 -艰 -艱 -色 -艳 -艴 -艵 -艶 -艷 -艸 -艹 -艺 -艼 -艽 -艾 -艿 -芀 -节 -芃 -芄 -芅 -芈 -芊 -芋 -芍 -芎 -芏 -芐 -芑 -芒 -芓 -芔 -芗 -芘 -芙 -芚 -芛 -芜 -芝 -芞 -芟 -芠 -芡 -芣 -芤 -芥 -芦 -芧 -芨 -芩 -芪 -芫 -芬 -芭 -芮 -芯 -芰 -花 -芲 -芳 -芴 -芵 -芷 -芸 -芹 -芺 -芻 -芼 -芽 -芾 -芿 -苀 -苁 -苄 -苅 -苇 -苈 -苉 -苊 -苋 -苌 -苍 -苎 -苏 -苑 -苒 -苓 -苔 -苕 -苖 -苗 -苘 -苙 -苛 -苜 -苝 -苞 -苟 -苠 -苡 -苣 -苤 -若 -苦 -苧 -苨 -苪 -苫 -苬 -苯 -英 -苲 -苳 -苴 -苵 -苶 -苷 -苹 -苺 -苻 -苼 -苾 -茀 -茁 -茂 -范 -茄 -茅 -茆 -茇 -茈 -茉 -茋 -茌 -茍 -茎 -茏 -茑 -茓 -茔 -茕 -茖 -茗 -茘 -茙 -茚 -茛 -茜 -茝 -茠 -茢 -茤 -茥 -茦 -茧 -茨 -茩 -茪 -茫 -茬 -茭 -茮 -茯 -茱 -茲 -茳 -茴 -茵 -茶 -茷 -茸 -茹 -茺 -茻 -茼 -茽 -茿 -荀 -荁 -荂 -荃 -荄 -荅 -荆 -荇 -草 -荊 -荍 -荎 -荏 -荐 -荑 -荒 -荓 -荔 -荖 -荘 -荙 -荚 -荛 -荜 -荞 -荟 -荠 -荡 -荣 -荤 -荥 -荦 -荧 -荨 -荩 -荪 -荫 -荬 -荭 -荮 -药 -荳 -荴 -荵 -荷 -荸 -荺 -荻 -荼 -荽 -荾 -莁 -莃 -莅 -莆 -莉 -莊 -莋 -莌 -莍 -莎 -莏 -莐 -莒 -莓 -莔 -莕 -莖 -莘 -莙 -莛 -莜 -莝 -莞 -莠 -莢 -莣 -莤 -莥 -莧 -莨 -莩 -莪 -莫 -莬 -莰 -莱 -莲 -莳 -莴 -莵 -莶 -获 -莸 -莹 -莺 -莼 -莽 -莿 -菀 -菁 -菂 -菅 -菆 -菇 -菈 -菉 -菊 -菋 -菌 -菍 -菎 -菏 -菑 -菓 -菔 -菕 -菖 -菘 -菙 -菜 -菝 -菞 -菟 -菠 -菡 -菣 -菤 -菥 -菧 -菨 -菩 -菪 -菫 -華 -菰 -菱 -菲 -菳 -菴 -菵 -菶 -菸 -菹 -菺 -菼 -菽 -菾 -菿 -萁 -萃 -萄 -萆 -萇 -萉 -萊 -萋 -萌 -萍 -萎 -萏 -萐 -萑 -萒 -萘 -萚 -萜 -萝 -萠 -萡 -萣 -萤 -营 -萦 -萧 -萨 -萩 -萬 -萭 -萯 -萰 -萱 -萲 -萳 -萴 -萵 -萷 -萸 -萹 -萺 -萼 -落 -萿 -葀 -葂 -葃 -葅 -葆 -葉 -葊 -葋 -葌 -葍 -葎 -葑 -葒 -葖 -著 -葙 -葚 -葛 -葜 -葝 -葞 -葟 -葡 -董 -葤 -葥 -葦 -葧 -葩 -葫 -葬 -葭 -葯 -葰 -葱 -葳 -葴 -葵 -葶 -葷 -葸 -葹 -葺 -葽 -蒂 -蒄 -蒆 -蒇 -蒈 -蒉 -蒋 -蒌 -蒍 -蒎 -蒏 -蒐 -蒓 -蒔 -蒗 -蒘 -蒙 -蒚 -蒛 -蒜 -蒝 -蒞 -蒟 -蒠 -蒡 -蒢 -蒤 -蒧 -蒨 -蒩 -蒪 -蒫 -蒬 -蒭 -蒮 -蒯 -蒱 -蒲 -蒴 -蒶 -蒸 -蒹 -蒺 -蒻 -蒼 -蒽 -蒾 -蒿 -蓀 -蓁 -蓂 -蓄 -蓆 -蓇 -蓉 -蓊 -蓋 -蓌 -蓍 -蓎 -蓏 -蓐 -蓑 -蓓 -蓖 -蓘 -蓚 -蓝 -蓟 -蓠 -蓢 -蓣 -蓥 -蓦 -蓧 -蓨 -蓩 -蓪 -蓫 -蓬 -蓭 -蓮 -蓯 -蓰 -蓱 -蓲 -蓳 -蓴 -蓶 -蓷 -蓹 -蓺 -蓻 -蓼 -蓽 -蓾 -蓿 -蔀 -蔂 -蔃 -蔄 -蔆 -蔇 -蔈 -蔉 -蔊 -蔌 -蔍 -蔎 -蔏 -蔑 -蔒 -蔓 -蔔 -蔕 -蔖 -蔗 -蔘 -蔚 -蔜 -蔝 -蔞 -蔟 -蔠 -蔡 -蔣 -蔤 -蔥 -蔦 -蔧 -蔨 -蔩 -蔪 -蔫 -蔬 -蔭 -蔮 -蔰 -蔱 -蔴 -蔵 -蔷 -蔸 -蔹 -蔺 -蔻 -蔼 -蔽 -蔾 -蔿 -蕀 -蕁 -蕃 -蕄 -蕅 -蕆 -蕇 -蕈 -蕉 -蕊 -蕋 -蕍 -蕎 -蕑 -蕒 -蕓 -蕕 -蕖 -蕗 -蕘 -蕙 -蕚 -蕛 -蕝 -蕞 -蕟 -蕠 -蕡 -蕢 -蕣 -蕤 -蕦 -蕧 -蕨 -蕩 -蕪 -蕫 -蕭 -蕮 -蕰 -蕱 -蕲 -蕴 -蕵 -蕷 -蕸 -蕹 -蕺 -蕻 -蕼 -蕾 -蕿 -薀 -薁 -薂 -薃 -薄 -薅 -薆 -薇 -薈 -薉 -薊 -薋 -薌 -薍 -薎 -薏 -薐 -薑 -薔 -薕 -薖 -薗 -薘 -薙 -薚 -薛 -薜 -薝 -薞 -薟 -薠 -薡 -薢 -薣 -薤 -薦 -薧 -薨 -薩 -薪 -薫 -薬 -薭 -薮 -薯 -薰 -薱 -薲 -薳 -薴 -薵 -薶 -薷 -薸 -薹 -薺 -薽 -薾 -薿 -藀 -藁 -藂 -藃 -藄 -藅 -藆 -藇 -藈 -藉 -藋 -藍 -藎 -藏 -藐 -藑 -藒 -藓 -藔 -藕 -藗 -藘 -藙 -藚 -藜 -藝 -藟 -藠 -藡 -藣 -藤 -藥 -藦 -藨 -藩 -藪 -藫 -藬 -藭 -藰 -藱 -藲 -藴 -藶 -藷 -藸 -藹 -藺 -藻 -藼 -藾 -藿 -蘀 -蘁 -蘂 -蘄 -蘅 -蘆 -蘇 -蘉 -蘊 -蘋 -蘌 -蘐 -蘑 -蘓 -蘖 -蘗 -蘘 -蘙 -蘚 -蘛 -蘜 -蘞 -蘟 -蘠 -蘡 -蘢 -蘣 -蘤 -蘥 -蘦 -蘧 -蘩 -蘪 -蘬 -蘭 -蘮 -蘱 -蘳 -蘴 -蘵 -蘶 -蘸 -蘹 -蘺 -蘻 -蘼 -蘾 -蘿 -虀 -虃 -虆 -虇 -虈 -虉 -虋 -虌 -虎 -虏 -虐 -虑 -虒 -虓 -虔 -處 -虖 -虙 -虚 -虛 -虜 -虞 -號 -虡 -虢 -虣 -虤 -虧 -虨 -虩 -虪 -虫 -虬 -虭 -虮 -虯 -虰 -虱 -虴 -虵 -虷 -虸 -虹 -虺 -虻 -虼 -虽 -虾 -虿 -蚀 -蚁 -蚂 -蚄 -蚅 -蚆 -蚇 -蚊 -蚋 -蚌 -蚍 -蚑 -蚓 -蚕 -蚖 -蚗 -蚘 -蚙 -蚚 -蚜 -蚝 -蚞 -蚡 -蚢 -蚣 -蚤 -蚥 -蚧 -蚨 -蚩 -蚪 -蚬 -蚯 -蚰 -蚱 -蚲 -蚳 -蚴 -蚵 -蚶 -蚷 -蚸 -蚹 -蚺 -蚻 -蚼 -蚿 -蛀 -蛁 -蛂 -蛃 -蛄 -蛅 -蛆 -蛇 -蛈 -蛉 -蛊 -蛋 -蛌 -蛍 -蛎 -蛏 -蛐 -蛑 -蛓 -蛔 -蛕 -蛖 -蛗 -蛘 -蛙 -蛚 -蛛 -蛜 -蛝 -蛞 -蛟 -蛢 -蛣 -蛤 -蛦 -蛩 -蛪 -蛫 -蛬 -蛭 -蛮 -蛯 -蛰 -蛱 -蛲 -蛳 -蛴 -蛵 -蛶 -蛷 -蛸 -蛹 -蛺 -蛻 -蛾 -蜀 -蜁 -蜂 -蜃 -蜄 -蜆 -蜇 -蜈 -蜉 -蜊 -蜋 -蜌 -蜍 -蜎 -蜐 -蜑 -蜒 -蜓 -蜕 -蜖 -蜗 -蜘 -蜙 -蜚 -蜛 -蜜 -蜞 -蜠 -蜡 -蜢 -蜣 -蜤 -蜥 -蜦 -蜧 -蜨 -蜩 -蜪 -蜬 -蜭 -蜮 -蜯 -蜰 -蜱 -蜲 -蜳 -蜴 -蜵 -蜷 -蜸 -蜺 -蜻 -蜼 -蜾 -蜿 -蝀 -蝁 -蝂 -蝃 -蝆 -蝇 -蝈 -蝉 -蝋 -蝌 -蝍 -蝎 -蝐 -蝑 -蝒 -蝓 -蝔 -蝕 -蝖 -蝗 -蝘 -蝙 -蝚 -蝛 -蝜 -蝝 -蝞 -蝟 -蝠 -蝡 -蝣 -蝤 -蝥 -蝦 -蝨 -蝪 -蝫 -蝬 -蝭 -蝮 -蝯 -蝰 -蝱 -蝲 -蝳 -蝴 -蝶 -蝷 -蝸 -蝹 -蝺 -蝻 -蝼 -蝽 -蝾 -蝿 -螁 -螂 -螃 -螄 -螅 -螇 -螈 -螋 -融 -螎 -螏 -螐 -螑 -螒 -螓 -螔 -螖 -螗 -螘 -螚 -螛 -螜 -螝 -螞 -螟 -螠 -螡 -螢 -螣 -螤 -螥 -螨 -螪 -螫 -螬 -螭 -螮 -螯 -螰 -螱 -螳 -螴 -螵 -螶 -螷 -螸 -螹 -螺 -螻 -螼 -螽 -螾 -螿 -蟀 -蟁 -蟂 -蟃 -蟄 -蟅 -蟆 -蟇 -蟈 -蟉 -蟊 -蟋 -蟌 -蟎 -蟏 -蟑 -蟒 -蟓 -蟔 -蟗 -蟘 -蟙 -蟛 -蟜 -蟞 -蟟 -蟠 -蟡 -蟢 -蟣 -蟤 -蟥 -蟦 -蟧 -蟨 -蟪 -蟫 -蟬 -蟭 -蟮 -蟯 -蟲 -蟳 -蟴 -蟶 -蟷 -蟹 -蟺 -蟻 -蟼 -蟾 -蟿 -蠀 -蠁 -蠂 -蠃 -蠄 -蠅 -蠆 -蠈 -蠉 -蠊 -蠋 -蠌 -蠍 -蠏 -蠐 -蠑 -蠓 -蠔 -蠕 -蠖 -蠗 -蠘 -蠙 -蠛 -蠜 -蠝 -蠟 -蠠 -蠡 -蠢 -蠣 -蠤 -蠥 -蠦 -蠨 -蠩 -蠪 -蠫 -蠬 -蠭 -蠮 -蠯 -蠰 -蠱 -蠲 -蠳 -蠵 -蠶 -蠷 -蠸 -蠹 -蠻 -蠼 -蠽 -蠾 -蠿 -血 -衁 -衂 -衃 -衄 -衅 -衆 -衇 -衈 -衊 -衋 -行 -衍 -衎 -衒 -術 -衔 -衕 -衖 -街 -衙 -衚 -衛 -衜 -衝 -衞 -衠 -衡 -衢 -衣 -补 -衧 -表 -衩 -衪 -衫 -衬 -衭 -衮 -衯 -衰 -衱 -衲 -衵 -衶 -衷 -衹 -衺 -衽 -衾 -衿 -袀 -袁 -袂 -袃 -袄 -袅 -袆 -袈 -袉 -袋 -袌 -袍 -袑 -袒 -袓 -袕 -袖 -袗 -袘 -袙 -袚 -袛 -袜 -袞 -袟 -袠 -袡 -袢 -袤 -袧 -袨 -袪 -被 -袬 -袭 -袯 -袰 -袱 -袲 -袴 -袵 -袶 -袷 -袸 -袹 -袺 -袼 -袽 -袾 -袿 -裀 -裁 -裂 -裃 -装 -裆 -裈 -裉 -裊 -裋 -裍 -裎 -裏 -裒 -裔 -裕 -裖 -裗 -裘 -裙 -裚 -裛 -補 -裝 -裞 -裟 -裠 -裡 -裢 -裣 -裤 -裥 -裧 -裨 -裬 -裮 -裯 -裰 -裱 -裲 -裳 -裴 -裵 -裶 -裷 -裸 -裹 -裺 -裻 -裼 -製 -裾 -裿 -褀 -褁 -褂 -褄 -褅 -褆 -複 -褊 -褋 -褌 -褍 -褎 -褐 -褑 -褒 -褓 -褔 -褕 -褖 -褗 -褘 -褙 -褚 -褛 -褞 -褟 -褡 -褢 -褥 -褦 -褧 -褩 -褪 -褫 -褭 -褮 -褯 -褰 -褱 -褲 -褳 -褴 -褵 -褶 -褷 -褸 -褻 -褼 -褽 -褾 -襁 -襂 -襃 -襄 -襆 -襇 -襋 -襌 -襏 -襐 -襒 -襓 -襕 -襖 -襗 -襘 -襚 -襛 -襜 -襝 -襞 -襟 -襠 -襡 -襢 -襣 -襤 -襦 -襩 -襪 -襫 -襬 -襭 -襮 -襯 -襲 -襳 -襴 -襶 -襷 -襹 -襺 -襻 -襾 -西 -要 -覂 -覃 -覅 -覆 -覇 -覈 -見 -覌 -覎 -規 -覓 -覕 -視 -覗 -覘 -覚 -覛 -覜 -覝 -覡 -覢 -覣 -覤 -覦 -覧 -覩 -親 -覬 -覭 -覮 -覯 -覲 -観 -覶 -覷 -覺 -覽 -覿 -觀 -见 -观 -觃 -规 -觅 -视 -觇 -览 -觉 -觊 -觋 -觌 -觎 -觏 -觐 -觑 -角 -觓 -觔 -觕 -觖 -觙 -觚 -觜 -觝 -觞 -觟 -觠 -觡 -觢 -解 -觤 -觥 -触 -觩 -觫 -觬 -觭 -觯 -觰 -觱 -觲 -觳 -觴 -觶 -觷 -觸 -觺 -觻 -觼 -觾 -觿 -言 -訁 -訂 -訃 -訄 -訇 -計 -訊 -訌 -討 -訏 -訐 -訑 -訒 -訓 -訔 -訕 -訖 -託 -記 -訚 -訛 -訝 -訞 -訟 -訢 -訣 -訥 -訧 -訩 -訪 -訬 -設 -訰 -許 -訳 -訴 -訶 -訹 -診 -註 -証 -訾 -訿 -詀 -詁 -詄 -詅 -詆 -詈 -詍 -詎 -詐 -詒 -詔 -評 -詖 -詗 -詘 -詙 -詛 -詝 -詞 -詟 -詠 -詡 -詢 -詣 -詥 -試 -詧 -詩 -詪 -詫 -詬 -詭 -詮 -詰 -話 -該 -詳 -詵 -詶 -詷 -詹 -詻 -詼 -詿 -誂 -誃 -誄 -誅 -誆 -誇 -誉 -誊 -誋 -誌 -認 -誑 -誒 -誓 -誕 -誖 -誘 -誙 -誚 -語 -誠 -誡 -誣 -誤 -誥 -誦 -誨 -說 -誫 -説 -読 -誰 -課 -誴 -誶 -誸 -誹 -誻 -誼 -誾 -調 -諀 -諂 -諄 -諅 -諆 -談 -諈 -諉 -請 -諌 -諍 -諏 -諐 -諑 -諒 -諓 -諔 -諕 -論 -諗 -諚 -諛 -諜 -諝 -諞 -諟 -諠 -諡 -諢 -諤 -諦 -諧 -諪 -諫 -諭 -諮 -諰 -諱 -諲 -諳 -諴 -諵 -諶 -諷 -諸 -諺 -諼 -諾 -謀 -謁 -謂 -謄 -謅 -謇 -謈 -謊 -謋 -謌 -謍 -謎 -謏 -謐 -謑 -謒 -謔 -謕 -謖 -謗 -謘 -謙 -謚 -講 -謜 -謝 -謞 -謠 -謡 -謢 -謣 -謤 -謥 -謦 -謧 -謨 -謩 -謪 -謫 -謬 -謯 -謰 -謱 -謳 -謵 -謷 -謹 -謼 -謾 -譀 -譁 -譅 -譆 -譇 -譈 -證 -譊 -譋 -譌 -譎 -譏 -譐 -譑 -譓 -譔 -譕 -譖 -識 -譙 -譚 -譛 -譜 -譝 -譞 -譟 -譠 -譣 -譥 -警 -譨 -譪 -譫 -譬 -譭 -譯 -議 -譲 -譳 -譴 -護 -譸 -譹 -譺 -譻 -譽 -譾 -譿 -讀 -讂 -讃 -讄 -讆 -變 -讋 -讌 -讎 -讐 -讒 -讓 -讔 -讕 -讖 -讘 -讙 -讚 -讜 -讞 -讟 -计 -订 -讣 -认 -讥 -讦 -讧 -讨 -让 -讪 -讫 -训 -议 -讯 -记 -讱 -讲 -讳 -讴 -讵 -讶 -讷 -许 -讹 -论 -讻 -讼 -讽 -设 -访 -诀 -证 -诂 -诃 -评 -诅 -识 -诇 -诈 -诉 -诊 -诋 -诌 -词 -诎 -诏 -诐 -译 -诒 -诓 -诔 -试 -诖 -诗 -诘 -诙 -诚 -诛 -诜 -话 -诞 -诟 -诠 -诡 -询 -诣 -诤 -该 -详 -诧 -诨 -诩 -诫 -诬 -语 -诮 -误 -诰 -诱 -诲 -诳 -说 -诵 -请 -诸 -诹 -诺 -读 -诼 -诽 -课 -诿 -谀 -谁 -谂 -调 -谄 -谅 -谆 -谇 -谈 -谊 -谋 -谌 -谍 -谎 -谏 -谐 -谑 -谒 -谓 -谔 -谕 -谖 -谗 -谙 -谚 -谛 -谜 -谝 -谞 -谟 -谠 -谡 -谢 -谣 -谤 -谥 -谦 -谧 -谨 -谩 -谪 -谫 -谬 -谭 -谮 -谯 -谰 -谱 -谲 -谳 -谴 -谵 -谶 -谷 -谹 -谻 -谼 -谽 -谾 -谿 -豁 -豃 -豅 -豆 -豇 -豈 -豉 -豊 -豋 -豌 -豍 -豎 -豏 -豐 -豔 -豕 -豗 -豚 -豜 -豝 -豟 -象 -豢 -豤 -豥 -豦 -豨 -豪 -豫 -豬 -豭 -豮 -豯 -豰 -豱 -豲 -豳 -豵 -豶 -豷 -豸 -豹 -豺 -豻 -豽 -豿 -貀 -貁 -貂 -貄 -貅 -貆 -貉 -貊 -貌 -貍 -貏 -貐 -貑 -貒 -貓 -貔 -貕 -貗 -貘 -貙 -貛 -貜 -貝 -貞 -負 -財 -貢 -貣 -貤 -貧 -貨 -販 -貪 -貫 -責 -貭 -貮 -貯 -貰 -貲 -貳 -貴 -貶 -買 -貸 -貺 -費 -貼 -貽 -貾 -貿 -賀 -賁 -賂 -賃 -賄 -賅 -資 -賈 -賉 -賊 -賌 -賎 -賑 -賒 -賓 -賔 -賕 -賙 -賚 -賛 -賜 -賞 -賟 -賠 -賡 -賢 -賣 -賤 -賥 -賦 -賧 -賨 -質 -賫 -賬 -賭 -賮 -賴 -賵 -賸 -賹 -賺 -賻 -購 -賽 -賾 -贄 -贅 -贆 -贇 -贈 -贊 -贋 -贌 -贍 -贏 -贐 -贓 -贔 -贕 -贖 -贗 -贙 -贛 -贜 -贝 -贞 -负 -贡 -财 -责 -贤 -败 -账 -货 -质 -贩 -贪 -贫 -贬 -购 -贮 -贯 -贰 -贱 -贲 -贳 -贴 -贵 -贶 -贷 -贸 -费 -贺 -贻 -贼 -贽 -贾 -贿 -赀 -赁 -赂 -赃 -资 -赅 -赆 -赇 -赈 -赉 -赊 -赋 -赌 -赍 -赎 -赏 -赐 -赑 -赒 -赓 -赔 -赕 -赖 -赗 -赘 -赙 -赚 -赛 -赜 -赝 -赞 -赟 -赠 -赡 -赢 -赣 -赤 -赦 -赧 -赨 -赩 -赪 -赫 -赬 -赭 -赮 -走 -赳 -赴 -赵 -赶 -起 -赸 -趀 -趁 -趂 -趄 -超 -越 -趋 -趌 -趍 -趎 -趏 -趐 -趑 -趓 -趔 -趕 -趖 -趙 -趜 -趟 -趠 -趡 -趣 -趥 -趧 -趨 -趪 -趫 -趬 -趭 -趮 -趯 -趱 -趲 -足 -趴 -趵 -趷 -趸 -趹 -趺 -趼 -趾 -趿 -跂 -跃 -跄 -跅 -跆 -跇 -跈 -跋 -跌 -跍 -跎 -跏 -跐 -跑 -跓 -跕 -跖 -跗 -跘 -跙 -跚 -跛 -跜 -距 -跞 -跟 -跠 -跡 -跢 -跣 -跤 -跥 -跦 -跧 -跨 -跩 -跪 -跫 -跬 -跮 -路 -跰 -跱 -跲 -跳 -跴 -践 -跶 -跷 -跸 -跹 -跺 -跻 -跼 -跽 -跾 -跿 -踂 -踃 -踅 -踆 -踇 -踉 -踊 -踌 -踍 -踎 -踏 -踐 -踑 -踒 -踓 -踔 -踕 -踖 -踘 -踙 -踛 -踝 -踞 -踟 -踠 -踡 -踢 -踣 -踤 -踥 -踦 -踧 -踩 -踪 -踫 -踬 -踮 -踯 -踰 -踱 -踳 -踴 -踵 -踶 -踸 -踹 -踺 -踼 -踽 -踾 -蹀 -蹁 -蹂 -蹄 -蹅 -蹇 -蹈 -蹉 -蹊 -蹋 -蹌 -蹍 -蹎 -蹏 -蹐 -蹑 -蹒 -蹓 -蹔 -蹕 -蹖 -蹗 -蹙 -蹚 -蹛 -蹜 -蹝 -蹞 -蹟 -蹠 -蹡 -蹢 -蹣 -蹤 -蹥 -蹦 -蹧 -蹩 -蹪 -蹬 -蹭 -蹯 -蹰 -蹲 -蹳 -蹴 -蹵 -蹶 -蹸 -蹺 -蹻 -蹼 -蹽 -蹾 -蹿 -躁 -躂 -躄 -躅 -躆 -躇 -躈 -躉 -躊 -躋 -躍 -躏 -躐 -躑 -躒 -躓 -躔 -躕 -躖 -躗 -躙 -躚 -躜 -躝 -躞 -躟 -躠 -躡 -躣 -躤 -躥 -躦 -躨 -躩 -躪 -身 -躬 -躭 -躯 -躰 -躲 -躳 -躶 -躺 -躽 -軀 -車 -軋 -軌 -軍 -軎 -軏 -軑 -軒 -軓 -軔 -軘 -軛 -軝 -軞 -軟 -転 -軤 -軥 -軦 -軧 -軨 -軫 -軬 -軮 -軯 -軱 -軲 -軵 -軶 -軷 -軸 -軹 -軺 -軻 -軼 -軽 -軾 -軿 -輀 -輁 -輂 -較 -輄 -輅 -輆 -輇 -輈 -載 -輊 -輋 -輌 -輐 -輑 -輒 -輓 -輔 -輕 -輖 -輗 -輘 -輚 -輛 -輜 -輝 -輞 -輟 -輠 -輣 -輤 -輥 -輦 -輩 -輪 -輬 -輭 -輮 -輯 -輲 -輳 -輴 -輵 -輶 -輷 -輸 -輹 -輻 -輾 -輿 -轀 -轂 -轃 -轄 -轅 -轆 -轇 -轈 -轉 -轍 -轎 -轏 -轐 -轑 -轒 -轓 -轔 -轕 -轖 -轗 -轘 -轙 -轚 -轛 -轝 -轞 -轟 -轠 -轡 -轢 -轤 -车 -轧 -轨 -轩 -轪 -轫 -转 -轭 -轮 -软 -轰 -轱 -轲 -轳 -轴 -轵 -轶 -轷 -轸 -轹 -轺 -轻 -轼 -载 -轾 -轿 -辀 -辁 -辂 -较 -辄 -辅 -辆 -辇 -辈 -辉 -辊 -辋 -辌 -辍 -辎 -辏 -辐 -辑 -辒 -输 -辔 -辕 -辖 -辗 -辘 -辙 -辚 -辛 -辜 -辞 -辟 -辠 -辢 -辣 -辥 -辦 -辧 -辨 -辩 -辫 -辭 -辮 -辯 -辰 -辱 -農 -辴 -辵 -边 -辺 -辻 -込 -辽 -达 -辿 -迁 -迂 -迄 -迅 -迆 -过 -迈 -迉 -迋 -迍 -迎 -运 -近 -迒 -迓 -返 -迕 -迖 -还 -这 -进 -远 -违 -连 -迟 -迡 -迢 -迣 -迤 -迥 -迦 -迨 -迩 -迪 -迫 -迭 -迮 -述 -迳 -迴 -迵 -迷 -迸 -迹 -迺 -迻 -追 -迾 -迿 -退 -送 -适 -逃 -逄 -逅 -逆 -逈 -选 -逊 -逋 -逌 -逍 -逎 -透 -逐 -逑 -递 -逓 -途 -逕 -逖 -逗 -這 -通 -逛 -逜 -逝 -逞 -速 -造 -逡 -逢 -連 -逤 -逦 -逨 -逩 -逭 -逮 -逯 -週 -進 -逴 -逵 -逶 -逸 -逹 -逺 -逻 -逼 -逾 -逿 -遁 -遂 -遄 -遅 -遆 -遇 -遊 -運 -遍 -過 -遏 -遐 -遑 -遒 -道 -達 -違 -遗 -遘 -遙 -遛 -遜 -遝 -遞 -遠 -遡 -遢 -遣 -遥 -遨 -適 -遭 -遮 -遯 -遰 -遲 -遳 -遴 -遵 -遶 -遷 -選 -遹 -遺 -遻 -遼 -遽 -遾 -避 -邀 -邁 -邂 -邃 -還 -邅 -邆 -邇 -邈 -邉 -邊 -邋 -邍 -邏 -邐 -邑 -邓 -邔 -邕 -邗 -邘 -邙 -邛 -邝 -邞 -邟 -邠 -邡 -邢 -那 -邥 -邦 -邧 -邨 -邪 -邬 -邮 -邯 -邰 -邱 -邲 -邳 -邴 -邵 -邶 -邸 -邹 -邺 -邻 -邽 -邾 -邿 -郁 -郃 -郄 -郅 -郇 -郈 -郊 -郋 -郎 -郏 -郐 -郑 -郓 -郔 -郕 -郖 -郗 -郘 -郙 -郚 -郛 -郜 -郝 -郞 -郟 -郠 -郡 -郢 -郣 -郤 -郥 -郦 -郧 -部 -郪 -郫 -郭 -郯 -郰 -郱 -郲 -郳 -郴 -郵 -郷 -郸 -郹 -郺 -郻 -郼 -都 -郾 -郿 -鄀 -鄁 -鄂 -鄃 -鄄 -鄅 -鄆 -鄇 -鄈 -鄉 -鄋 -鄌 -鄍 -鄎 -鄏 -鄐 -鄑 -鄒 -鄔 -鄖 -鄗 -鄘 -鄙 -鄚 -鄛 -鄜 -鄝 -鄞 -鄟 -鄠 -鄡 -鄢 -鄣 -鄤 -鄦 -鄧 -鄨 -鄩 -鄪 -鄫 -鄬 -鄭 -鄮 -鄯 -鄰 -鄱 -鄲 -鄳 -鄴 -鄵 -鄶 -鄸 -鄹 -鄺 -鄻 -鄾 -酀 -酁 -酂 -酃 -酄 -酅 -酆 -酇 -酈 -酉 -酊 -酋 -酌 -配 -酎 -酏 -酐 -酒 -酓 -酔 -酖 -酗 -酘 -酚 -酝 -酞 -酟 -酡 -酢 -酣 -酤 -酥 -酦 -酧 -酨 -酩 -酪 -酬 -酮 -酯 -酰 -酱 -酲 -酳 -酴 -酵 -酶 -酷 -酸 -酹 -酺 -酼 -酽 -酾 -酿 -醁 -醂 -醃 -醅 -醆 -醇 -醉 -醊 -醋 -醌 -醍 -醐 -醑 -醒 -醓 -醕 -醖 -醗 -醙 -醚 -醛 -醜 -醞 -醟 -醢 -醣 -醤 -醥 -醧 -醨 -醪 -醫 -醬 -醭 -醮 -醯 -醰 -醱 -醲 -醳 -醴 -醵 -醷 -醸 -醹 -醺 -醻 -醼 -醽 -醾 -醿 -釀 -釁 -釂 -釃 -釅 -釆 -采 -釈 -釉 -释 -釋 -里 -重 -野 -量 -釐 -金 -釓 -釔 -釕 -釗 -釘 -釙 -釚 -釜 -針 -釢 -釣 -釤 -釦 -釧 -釩 -釪 -釬 -釭 -釱 -釳 -釴 -釵 -釷 -釸 -釹 -釺 -釽 -釿 -鈀 -鈁 -鈃 -鈄 -鈆 -鈇 -鈈 -鈉 -鈊 -鈌 -鈍 -鈎 -鈏 -鈐 -鈑 -鈒 -鈔 -鈕 -鈖 -鈚 -鈞 -鈢 -鈣 -鈥 -鈦 -鈧 -鈫 -鈮 -鈰 -鈳 -鈴 -鈶 -鈷 -鈸 -鈹 -鈺 -鈽 -鈾 -鈿 -鉀 -鉄 -鉅 -鉆 -鉇 -鉈 -鉉 -鉊 -鉋 -鉌 -鉍 -鉏 -鉑 -鉒 -鉓 -鉕 -鉗 -鉚 -鉛 -鉞 -鉟 -鉠 -鉢 -鉤 -鉥 -鉦 -鉧 -鉬 -鉭 -鉮 -鉯 -鉱 -鉲 -鉴 -鉶 -鉷 -鉸 -鉹 -鉺 -鉻 -鉼 -鉾 -鉿 -銀 -銂 -銃 -銅 -銈 -銋 -銍 -銎 -銑 -銓 -銔 -銕 -銖 -銗 -銘 -銚 -銛 -銜 -銠 -銣 -銤 -銥 -銦 -銨 -銩 -銪 -銫 -銬 -銭 -銮 -銱 -銲 -銳 -銶 -銷 -銹 -銻 -銼 -銾 -鋀 -鋁 -鋂 -鋃 -鋄 -鋅 -鋆 -鋇 -鋈 -鋊 -鋋 -鋌 -鋏 -鋐 -鋒 -鋕 -鋗 -鋘 -鋙 -鋝 -鋟 -鋡 -鋤 -鋥 -鋦 -鋨 -鋩 -鋪 -鋭 -鋮 -鋯 -鋰 -鋱 -鋲 -鋳 -鋶 -鋸 -鋹 -鋺 -鋼 -錀 -錁 -錄 -錆 -錈 -錍 -錎 -錏 -錐 -錒 -錕 -錘 -錙 -錚 -錛 -錞 -錟 -錠 -錡 -錢 -錣 -錦 -錧 -錨 -錫 -錬 -錭 -錮 -錯 -録 -錳 -錵 -錶 -錸 -錻 -錼 -錾 -鍀 -鍁 -鍆 -鍇 -鍈 -鍉 -鍊 -鍋 -鍌 -鍍 -鍏 -鍐 -鍑 -鍔 -鍖 -鍘 -鍚 -鍛 -鍜 -鍝 -鍟 -鍠 -鍤 -鍥 -鍩 -鍪 -鍬 -鍭 -鍮 -鍰 -鍱 -鍳 -鍵 -鍶 -鍷 -鍺 -鍼 -鍾 -鎂 -鎃 -鎅 -鎉 -鎊 -鎌 -鎎 -鎏 -鎒 -鎓 -鎔 -鎖 -鎗 -鎘 -鎚 -鎛 -鎝 -鎞 -鎡 -鎢 -鎣 -鎦 -鎧 -鎩 -鎪 -鎬 -鎭 -鎮 -鎯 -鎰 -鎳 -鎵 -鎷 -鎸 -鎹 -鎻 -鏂 -鏃 -鏇 -鏈 -鏊 -鏌 -鏍 -鏏 -鏐 -鏑 -鏓 -鏖 -鏗 -鏘 -鏙 -鏚 -鏜 -鏝 -鏞 -鏟 -鏡 -鏢 -鏤 -鏦 -鏨 -鏬 -鏰 -鏳 -鏴 -鏵 -鏷 -鏹 -鏺 -鏻 -鏽 -鏾 -鐀 -鐃 -鐇 -鐈 -鐋 -鐍 -鐎 -鐏 -鐐 -鐓 -鐔 -鐕 -鐖 -鐘 -鐙 -鐚 -鐝 -鐠 -鐡 -鐤 -鐦 -鐧 -鐨 -鐫 -鐬 -鐭 -鐮 -鐯 -鐲 -鐳 -鐵 -鐶 -鐸 -鐺 -鐻 -鐽 -鐾 -鐿 -鑀 -鑁 -鑂 -鑄 -鑅 -鑊 -鑋 -鑌 -鑐 -鑑 -鑒 -鑔 -鑕 -鑚 -鑛 -鑞 -鑠 -鑢 -鑣 -鑤 -鑨 -鑪 -鑫 -鑭 -鑮 -鑯 -鑰 -鑱 -鑲 -鑴 -鑵 -鑷 -鑹 -鑼 -鑽 -鑾 -鑿 -钀 -钁 -钂 -钃 -钆 -钇 -针 -钉 -钊 -钋 -钌 -钍 -钎 -钏 -钐 -钒 -钓 -钔 -钕 -钖 -钗 -钘 -钙 -钚 -钛 -钜 -钝 -钞 -钟 -钠 -钡 -钢 -钣 -钤 -钥 -钦 -钧 -钨 -钩 -钪 -钫 -钬 -钭 -钮 -钯 -钰 -钱 -钲 -钳 -钴 -钵 -钷 -钹 -钺 -钻 -钼 -钽 -钾 -钿 -铀 -铁 -铂 -铃 -铄 -铅 -铆 -铈 -铉 -铊 -铋 -铌 -铍 -铎 -铏 -铐 -铑 -铒 -铕 -铖 -铗 -铘 -铙 -铚 -铛 -铜 -铝 -铞 -铟 -铠 -铡 -铢 -铣 -铤 -铥 -铧 -铨 -铩 -铪 -铫 -铬 -铭 -铮 -铯 -铰 -铱 -铲 -铳 -铴 -铵 -银 -铷 -铸 -铹 -铺 -铻 -铼 -铽 -链 -铿 -销 -锁 -锂 -锃 -锄 -锅 -锆 -锇 -锈 -锉 -锊 -锋 -锌 -锍 -锎 -锏 -锐 -锑 -锒 -锓 -锔 -锕 -锖 -锗 -锘 -错 -锚 -锛 -锜 -锝 -锞 -锟 -锡 -锢 -锣 -锤 -锥 -锦 -锧 -锨 -锩 -锪 -锫 -锬 -锭 -键 -锯 -锰 -锱 -锲 -锳 -锴 -锵 -锶 -锷 -锸 -锹 -锺 -锻 -锼 -锽 -锾 -锿 -镀 -镁 -镂 -镃 -镄 -镅 -镆 -镇 -镈 -镉 -镊 -镋 -镌 -镍 -镎 -镏 -镐 -镑 -镒 -镓 -镔 -镕 -镖 -镗 -镘 -镚 -镛 -镜 -镝 -镞 -镠 -镡 -镢 -镣 -镤 -镥 -镦 -镧 -镨 -镩 -镪 -镫 -镬 -镭 -镮 -镯 -镰 -镱 -镲 -镳 -镴 -镵 -镶 -長 -镺 -镻 -镼 -镽 -长 -門 -閂 -閃 -閆 -閇 -閈 -閉 -開 -閌 -閍 -閎 -閏 -閑 -閒 -間 -閔 -閘 -閜 -閞 -閟 -閡 -関 -閣 -閤 -閥 -閦 -閧 -閨 -閩 -閫 -閬 -閭 -閰 -閱 -閲 -閵 -閶 -閷 -閹 -閺 -閻 -閼 -閽 -閾 -閿 -闀 -闃 -闅 -闆 -闇 -闈 -闉 -闊 -闋 -闌 -闍 -闐 -闑 -闒 -闓 -闔 -闕 -闖 -闘 -闚 -闛 -關 -闞 -闟 -闠 -闡 -闢 -闤 -闥 -门 -闩 -闪 -闫 -闭 -问 -闯 -闰 -闱 -闲 -闳 -间 -闵 -闶 -闷 -闸 -闹 -闺 -闻 -闼 -闽 -闾 -闿 -阀 -阁 -阂 -阃 -阄 -阅 -阆 -阇 -阈 -阉 -阊 -阋 -阌 -阍 -阎 -阏 -阐 -阑 -阒 -阔 -阕 -阖 -阗 -阘 -阙 -阚 -阜 -阝 -阞 -队 -阠 -阡 -阢 -阤 -阨 -阪 -阬 -阭 -阮 -阯 -阰 -阱 -防 -阳 -阴 -阵 -阶 -阸 -阹 -阺 -阻 -阼 -阽 -阿 -陀 -陁 -陂 -附 -际 -陆 -陇 -陈 -陉 -陊 -陋 -陌 -降 -陎 -陏 -限 -陑 -陓 -陔 -陕 -陗 -陘 -陛 -陜 -陝 -陞 -陟 -陡 -院 -陣 -除 -陥 -陧 -陨 -险 -陪 -陫 -陬 -陭 -陰 -陲 -陳 -陴 -陵 -陶 -陷 -陸 -険 -陻 -陼 -陽 -陾 -隃 -隄 -隅 -隆 -隇 -隈 -隉 -隊 -隋 -隍 -階 -随 -隐 -隑 -隒 -隓 -隔 -隕 -隗 -隘 -隙 -際 -障 -隞 -隠 -隣 -隤 -隧 -隨 -隩 -險 -隮 -隰 -隱 -隲 -隳 -隴 -隶 -隷 -隸 -隹 -隺 -隻 -隼 -隽 -难 -隿 -雀 -雁 -雂 -雄 -雅 -集 -雇 -雈 -雉 -雊 -雋 -雌 -雍 -雎 -雏 -雑 -雒 -雓 -雔 -雕 -雖 -雗 -雘 -雙 -雚 -雛 -雜 -雝 -雞 -雟 -雠 -離 -難 -雨 -雩 -雪 -雫 -雯 -雰 -雱 -雲 -雳 -零 -雷 -雹 -雺 -電 -雽 -雾 -雿 -需 -霁 -霂 -霄 -霅 -霆 -震 -霈 -霉 -霊 -霋 -霍 -霎 -霏 -霐 -霑 -霒 -霓 -霖 -霙 -霜 -霞 -霠 -霢 -霣 -霤 -霧 -霨 -霩 -霪 -霫 -霬 -霭 -霮 -霰 -露 -霵 -霶 -霸 -霹 -霽 -霾 -霿 -靁 -靂 -靃 -靄 -靆 -靈 -靉 -靑 -青 -靓 -靖 -静 -靚 -靛 -靜 -非 -靠 -靡 -面 -靥 -靦 -靨 -革 -靪 -靫 -靬 -靭 -靮 -靰 -靱 -靲 -靳 -靴 -靶 -靷 -靸 -靺 -靻 -靼 -靽 -靾 -靿 -鞀 -鞁 -鞂 -鞃 -鞄 -鞅 -鞆 -鞈 -鞊 -鞋 -鞌 -鞍 -鞎 -鞏 -鞑 -鞒 -鞔 -鞗 -鞘 -鞙 -鞚 -鞜 -鞞 -鞠 -鞡 -鞣 -鞤 -鞥 -鞦 -鞧 -鞨 -鞪 -鞫 -鞬 -鞭 -鞮 -鞯 -鞲 -鞳 -鞴 -鞵 -鞶 -鞷 -鞹 -鞻 -鞽 -鞾 -鞿 -韁 -韂 -韃 -韄 -韅 -韆 -韇 -韉 -韋 -韌 -韍 -韎 -韏 -韐 -韑 -韓 -韔 -韕 -韗 -韙 -韜 -韝 -韞 -韟 -韠 -韡 -韣 -韥 -韦 -韧 -韨 -韩 -韪 -韫 -韬 -韭 -韮 -韰 -韱 -音 -韵 -韶 -韹 -韺 -韻 -韽 -韾 -響 -頁 -頂 -頃 -頄 -項 -順 -頇 -須 -頊 -頌 -頍 -頎 -頏 -預 -頑 -頒 -頓 -頔 -頖 -頗 -領 -頚 -頜 -頝 -頞 -頠 -頡 -頤 -頦 -頨 -頩 -頫 -頬 -頭 -頯 -頰 -頲 -頴 -頵 -頷 -頸 -頹 -頻 -頼 -頽 -顁 -顄 -顅 -顆 -顇 -顈 -顉 -顊 -顋 -題 -額 -顎 -顏 -顐 -顑 -顒 -顓 -顔 -顕 -顗 -願 -顙 -顛 -顜 -顝 -類 -顟 -顠 -顢 -顣 -顤 -顥 -顦 -顧 -顩 -顪 -顫 -顯 -顰 -顱 -顲 -顳 -顴 -页 -顶 -顷 -顸 -项 -顺 -须 -顼 -顽 -顾 -顿 -颀 -颁 -颂 -颃 -预 -颅 -领 -颇 -颈 -颉 -颊 -颋 -颌 -颍 -颎 -颏 -颐 -频 -颓 -颔 -颖 -颗 -题 -颙 -颚 -颛 -颜 -额 -颞 -颟 -颠 -颡 -颢 -颤 -颥 -颦 -颧 -風 -颬 -颭 -颮 -颯 -颱 -颲 -颳 -颶 -颸 -颺 -颻 -颼 -颽 -颾 -颿 -飁 -飂 -飄 -飆 -飈 -飉 -飋 -飌 -风 -飏 -飐 -飑 -飒 -飓 -飔 -飕 -飗 -飘 -飙 -飛 -飜 -飞 -食 -飡 -飢 -飣 -飥 -飧 -飨 -飩 -飪 -飫 -飬 -飭 -飮 -飯 -飱 -飲 -飴 -飶 -飺 -飼 -飽 -飾 -飿 -餀 -餂 -餃 -餄 -餅 -餈 -餉 -養 -餌 -餍 -餎 -餏 -餐 -餑 -餒 -餓 -餔 -餕 -餖 -餗 -餘 -餚 -餛 -餜 -餝 -餞 -餟 -餠 -餡 -餤 -餥 -餧 -館 -餪 -餫 -餬 -餭 -餮 -餯 -餰 -餱 -餲 -餳 -餵 -餷 -餹 -餺 -餻 -餼 -餽 -餾 -餿 -饁 -饂 -饃 -饅 -饇 -饈 -饉 -饊 -饋 -饌 -饍 -饎 -饐 -饑 -饒 -饔 -饕 -饗 -饘 -饙 -饛 -饜 -饞 -饟 -饡 -饢 -饥 -饧 -饨 -饩 -饪 -饫 -饬 -饭 -饮 -饯 -饰 -饱 -饲 -饳 -饴 -饵 -饶 -饷 -饸 -饹 -饺 -饻 -饼 -饽 -饿 -馁 -馃 -馄 -馅 -馆 -馇 -馈 -馉 -馊 -馋 -馌 -馍 -馏 -馐 -馑 -馒 -馓 -馔 -馕 -首 -馗 -馘 -香 -馛 -馝 -馞 -馣 -馥 -馦 -馧 -馨 -馬 -馭 -馮 -馯 -馰 -馱 -馲 -馳 -馴 -馵 -馹 -馺 -馻 -馼 -馽 -駁 -駂 -駃 -駄 -駅 -駆 -駈 -駉 -駍 -駎 -駏 -駐 -駑 -駒 -駓 -駔 -駕 -駖 -駘 -駙 -駛 -駜 -駝 -駟 -駡 -駢 -駣 -駤 -駥 -駩 -駪 -駬 -駭 -駮 -駰 -駱 -駴 -駶 -駷 -駸 -駹 -駺 -駻 -駼 -駽 -駾 -駿 -騁 -騂 -騃 -騄 -騅 -騇 -騉 -騊 -騋 -騌 -騍 -騎 -騏 -騑 -騒 -験 -騕 -騖 -騙 -騚 -騛 -騜 -騝 -騞 -騠 -騢 -騣 -騤 -騥 -騧 -騨 -騩 -騪 -騫 -騬 -騭 -騮 -騰 -騱 -騲 -騴 -騵 -騶 -騷 -騸 -騹 -騺 -騽 -騾 -驀 -驁 -驂 -驃 -驄 -驅 -驆 -驈 -驉 -驊 -驌 -驍 -驎 -驏 -驒 -驓 -驔 -驕 -驖 -驗 -驚 -驛 -驞 -驟 -驠 -驢 -驤 -驥 -驦 -驨 -驩 -驪 -驫 -马 -驭 -驮 -驯 -驰 -驱 -驲 -驳 -驴 -驵 -驶 -驷 -驸 -驹 -驺 -驻 -驼 -驽 -驾 -驿 -骀 -骁 -骂 -骃 -骄 -骅 -骆 -骇 -骈 -骉 -骊 -骋 -验 -骍 -骎 -骏 -骐 -骑 -骒 -骓 -骕 -骖 -骗 -骘 -骙 -骚 -骛 -骜 -骝 -骞 -骟 -骠 -骡 -骢 -骣 -骤 -骥 -骦 -骧 -骨 -骫 -骭 -骯 -骰 -骱 -骴 -骶 -骷 -骸 -骹 -骺 -骼 -骽 -骾 -骿 -髀 -髁 -髂 -髃 -髄 -髅 -髆 -髇 -髈 -髊 -髋 -髌 -髍 -髎 -髏 -髐 -髑 -髒 -髓 -體 -髕 -髖 -高 -髙 -髟 -髡 -髢 -髣 -髥 -髦 -髧 -髪 -髫 -髬 -髭 -髮 -髯 -髲 -髳 -髴 -髶 -髷 -髹 -髺 -髻 -髼 -髽 -髾 -鬀 -鬁 -鬃 -鬄 -鬅 -鬆 -鬈 -鬉 -鬊 -鬋 -鬌 -鬍 -鬎 -鬏 -鬐 -鬒 -鬓 -鬕 -鬖 -鬗 -鬘 -鬙 -鬚 -鬞 -鬟 -鬠 -鬢 -鬣 -鬤 -鬥 -鬧 -鬨 -鬩 -鬪 -鬫 -鬬 -鬮 -鬯 -鬱 -鬲 -鬳 -鬵 -鬶 -鬷 -鬹 -鬺 -鬻 -鬼 -鬾 -鬿 -魁 -魂 -魃 -魄 -魅 -魆 -魇 -魈 -魉 -魊 -魋 -魌 -魍 -魎 -魏 -魑 -魔 -魕 -魖 -魘 -魚 -魛 -魞 -魟 -魠 -魡 -魣 -魦 -魧 -魨 -魩 -魮 -魯 -魱 -魴 -魵 -魶 -魷 -魻 -魼 -魾 -鮀 -鮁 -鮂 -鮃 -鮄 -鮅 -鮆 -鮇 -鮊 -鮋 -鮍 -鮎 -鮐 -鮑 -鮒 -鮓 -鮗 -鮚 -鮛 -鮜 -鮝 -鮞 -鮟 -鮠 -鮡 -鮣 -鮤 -鮥 -鮦 -鮨 -鮪 -鮫 -鮭 -鮮 -鮰 -鮵 -鮸 -鮹 -鮻 -鮿 -鯀 -鯁 -鯃 -鯆 -鯇 -鯈 -鯉 -鯊 -鯏 -鯒 -鯓 -鯔 -鯕 -鯖 -鯗 -鯙 -鯛 -鯝 -鯞 -鯠 -鯡 -鯢 -鯤 -鯦 -鯧 -鯨 -鯪 -鯫 -鯬 -鯭 -鯮 -鯰 -鯱 -鯶 -鯷 -鯸 -鯻 -鯽 -鯿 -鰂 -鰃 -鰅 -鰆 -鰈 -鰉 -鰋 -鰌 -鰍 -鰏 -鰐 -鰒 -鰓 -鰕 -鰗 -鰛 -鰜 -鰝 -鰟 -鰣 -鰤 -鰥 -鰧 -鰨 -鰩 -鰫 -鰬 -鰭 -鰮 -鰯 -鰰 -鰱 -鰲 -鰳 -鰴 -鰶 -鰷 -鰹 -鰺 -鰻 -鰼 -鰾 -鰿 -鱀 -鱁 -鱂 -鱄 -鱅 -鱆 -鱇 -鱈 -鱉 -鱊 -鱋 -鱍 -鱐 -鱒 -鱓 -鱔 -鱕 -鱖 -鱗 -鱘 -鱚 -鱝 -鱞 -鱟 -鱠 -鱢 -鱣 -鱥 -鱦 -鱧 -鱨 -鱬 -鱭 -鱮 -鱰 -鱲 -鱳 -鱴 -鱵 -鱷 -鱸 -鱹 -鱺 -鱻 -鱼 -鱽 -鱾 -鱿 -鲀 -鲁 -鲂 -鲃 -鲅 -鲆 -鲇 -鲈 -鲉 -鲊 -鲋 -鲌 -鲍 -鲎 -鲏 -鲐 -鲑 -鲒 -鲔 -鲕 -鲖 -鲗 -鲘 -鲙 -鲚 -鲛 -鲜 -鲝 -鲞 -鲟 -鲠 -鲡 -鲢 -鲣 -鲤 -鲥 -鲦 -鲧 -鲨 -鲩 -鲪 -鲫 -鲬 -鲭 -鲮 -鲯 -鲰 -鲱 -鲲 -鲳 -鲴 -鲵 -鲷 -鲸 -鲹 -鲺 -鲻 -鲼 -鲽 -鲾 -鲿 -鳀 -鳁 -鳂 -鳃 -鳄 -鳅 -鳇 -鳈 -鳉 -鳊 -鳌 -鳍 -鳎 -鳏 -鳐 -鳑 -鳒 -鳓 -鳔 -鳕 -鳖 -鳗 -鳘 -鳙 -鳚 -鳛 -鳜 -鳝 -鳞 -鳟 -鳠 -鳡 -鳢 -鳣 -鳤 -鳥 -鳦 -鳧 -鳩 -鳪 -鳬 -鳭 -鳯 -鳰 -鳱 -鳲 -鳳 -鳴 -鳵 -鳶 -鳷 -鳹 -鳺 -鳻 -鳼 -鳽 -鳾 -鳿 -鴀 -鴃 -鴅 -鴆 -鴇 -鴈 -鴉 -鴎 -鴐 -鴒 -鴓 -鴔 -鴕 -鴗 -鴘 -鴛 -鴝 -鴞 -鴟 -鴠 -鴡 -鴢 -鴣 -鴥 -鴦 -鴨 -鴩 -鴫 -鴮 -鴯 -鴰 -鴱 -鴳 -鴴 -鴶 -鴷 -鴸 -鴻 -鴽 -鴾 -鴿 -鵀 -鵁 -鵂 -鵃 -鵄 -鵅 -鵊 -鵋 -鵌 -鵎 -鵏 -鵐 -鵑 -鵒 -鵓 -鵔 -鵖 -鵗 -鵙 -鵛 -鵜 -鵝 -鵞 -鵟 -鵠 -鵡 -鵧 -鵩 -鵪 -鵫 -鵬 -鵮 -鵯 -鵰 -鵱 -鵲 -鵳 -鵴 -鵵 -鵷 -鵸 -鵹 -鵺 -鵻 -鵼 -鵽 -鵾 -鶀 -鶁 -鶂 -鶄 -鶅 -鶆 -鶇 -鶈 -鶉 -鶊 -鶋 -鶌 -鶏 -鶐 -鶒 -鶓 -鶔 -鶖 -鶗 -鶘 -鶙 -鶚 -鶛 -鶜 -鶝 -鶞 -鶟 -鶠 -鶡 -鶢 -鶣 -鶤 -鶥 -鶨 -鶩 -鶪 -鶬 -鶭 -鶯 -鶱 -鶲 -鶴 -鶵 -鶶 -鶷 -鶹 -鶺 -鶻 -鶼 -鶾 -鶿 -鷀 -鷁 -鷂 -鷃 -鷄 -鷅 -鷇 -鷈 -鷉 -鷊 -鷋 -鷎 -鷏 -鷐 -鷑 -鷒 -鷓 -鷕 -鷖 -鷗 -鷘 -鷙 -鷚 -鷛 -鷜 -鷞 -鷟 -鷡 -鷢 -鷣 -鷤 -鷥 -鷦 -鷩 -鷫 -鷮 -鷯 -鷰 -鷲 -鷳 -鷵 -鷶 -鷷 -鷸 -鷹 -鷺 -鷻 -鷽 -鷾 -鷿 -鸀 -鸁 -鸂 -鸃 -鸄 -鸅 -鸆 -鸇 -鸉 -鸊 -鸋 -鸌 -鸍 -鸏 -鸐 -鸑 -鸒 -鸓 -鸔 -鸕 -鸗 -鸘 -鸙 -鸚 -鸛 -鸜 -鸝 -鸞 -鸟 -鸠 -鸡 -鸢 -鸣 -鸤 -鸥 -鸦 -鸧 -鸨 -鸩 -鸪 -鸫 -鸬 -鸭 -鸮 -鸯 -鸰 -鸱 -鸲 -鸳 -鸵 -鸶 -鸷 -鸸 -鸹 -鸺 -鸻 -鸼 -鸽 -鸾 -鸿 -鹀 -鹁 -鹂 -鹃 -鹄 -鹅 -鹆 -鹇 -鹈 -鹉 -鹊 -鹋 -鹌 -鹍 -鹎 -鹏 -鹐 -鹑 -鹒 -鹔 -鹕 -鹖 -鹗 -鹘 -鹙 -鹚 -鹛 -鹜 -鹝 -鹞 -鹟 -鹠 -鹡 -鹢 -鹣 -鹤 -鹦 -鹧 -鹨 -鹩 -鹪 -鹫 -鹬 -鹭 -鹮 -鹯 -鹰 -鹱 -鹲 -鹳 -鹴 -鹵 -鹸 -鹹 -鹺 -鹻 -鹼 -鹽 -鹾 -鹿 -麀 -麁 -麂 -麃 -麅 -麆 -麇 -麈 -麉 -麊 -麋 -麌 -麎 -麐 -麑 -麒 -麓 -麔 -麖 -麗 -麙 -麚 -麛 -麜 -麝 -麞 -麟 -麠 -麡 -麤 -麥 -麦 -麧 -麩 -麪 -麮 -麯 -麰 -麴 -麵 -麶 -麷 -麸 -麹 -麺 -麻 -麼 -麽 -麾 -麿 -黀 -黁 -黂 -黃 -黄 -黇 -黈 -黉 -黌 -黍 -黎 -黏 -黐 -黑 -黒 -黓 -黔 -黕 -黖 -默 -黙 -黚 -黛 -黜 -黝 -點 -黟 -黠 -黡 -黢 -黤 -黥 -黧 -黨 -黩 -黪 -黫 -黭 -黮 -黯 -黰 -黲 -黳 -黴 -黵 -黶 -黷 -黹 -黺 -黻 -黼 -黽 -黾 -黿 -鼀 -鼁 -鼂 -鼃 -鼆 -鼇 -鼈 -鼉 -鼊 -鼋 -鼍 -鼎 -鼏 -鼐 -鼒 -鼓 -鼕 -鼖 -鼗 -鼘 -鼙 -鼛 -鼜 -鼠 -鼢 -鼣 -鼤 -鼥 -鼨 -鼩 -鼪 -鼫 -鼬 -鼭 -鼮 -鼯 -鼰 -鼱 -鼲 -鼳 -鼴 -鼵 -鼶 -鼷 -鼸 -鼹 -鼻 -鼽 -鼾 -齀 -齁 -齂 -齃 -齇 -齉 -齊 -齋 -齌 -齍 -齎 -齏 -齐 -齑 -齒 -齔 -齕 -齖 -齗 -齘 -齙 -齛 -齜 -齝 -齞 -齟 -齠 -齡 -齢 -齣 -齤 -齥 -齦 -齧 -齩 -齪 -齫 -齬 -齮 -齯 -齰 -齱 -齲 -齴 -齵 -齶 -齷 -齸 -齹 -齺 -齻 -齾 -齿 -龀 -龁 -龂 -龃 -龄 -龅 -龆 -龇 -龈 -龉 -龊 -龋 -龌 -龍 -龎 -龐 -龑 -龒 -龔 -龕 -龗 -龙 -龚 -龛 -龜 -龝 -龟 -龠 -龢 -龤 -鿍 -鿎 -鿏 -郎 -凉 -︽ -︾ -﹏ -﹥ -﹪ -! -# -% -& -( -) -+ -, -- -. -/ -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -: -; -= -? -@ -A -B -C -D -E -F -G -H -I -J -K -L -M -N -O -P -Q -R -S -T -U -V -W -X -Y -Z -a -b -c -d -e -f -g -h -i -j -k -l -m -n -o -p -q -r -s -t -u -v -w -x -y -z -| -~ -・ -ヲ -ァ -ィ -ゥ -ェ -ォ -ャ -ュ -ョ -ッ -ア -イ -ウ -エ -オ -カ -キ -ク -ケ -コ -サ -シ -ス -セ -ソ -タ -チ -ツ -テ -ト -ナ -ニ -ヌ -ネ -ノ -ハ -ヒ -フ -ヘ -ホ -マ -ミ -ム -メ -モ -ヤ -ユ -ヨ -ラ -リ -ル -レ -ロ -ワ -ン -゙ -゚ -¥ -𠅤 -𠙶 -𠳐 -𠴰 -𡎚 -𡐓 -𡑍 -𡚁 -𣇉 -𣗋 -𣲗 -𣲘 -𣸣 -𤋮 -𤧛 -𤩽 -𤫉 -𤺥 -𥔲 -𥕢 -𥖨 -𥻗 -𦈡 -𦍑 -𦒍 -𦙶 -𦝼 -𦭜 -𦰡 -𧿹 -𨐈 -𨙸 -𨚕 -𨟠 -𨭉 -𨱇 -𨱏 -𨱑 -𨱔 -𨺙 -𩓐 -𩽾 -𩾃 -𩾌 -𪟝 -𪣻 -𪤗 -𪨰 -𪨶 -𪩘 -𪾢 -𫄧 -𫄨 -𫄷 -𫄸 -𫇭 -𫌀 -𫍣 -𫍯 -𫍲 -𫍽 -𫐄 -𫐐 -𫐓 -𫑡 -𫓧 -𫓯 -𫓶 -𫓹 -𫔍 -𫔎 -𫔶 -𫖮 -𫖯 -𫖳 -𫗧 -𫗴 -𫘜 -𫘝 -𫘦 -𫘧 -𫘨 -𫘪 -𫘬 -𫚕 -𫚖 -𫚭 -𫛭 -𫞩 -𫟅 -𫟦 -𫟹 -𫟼 -𫠆 -𫠊 -𫠜 -𫢸 -𫫇 -𫭟 -𫭢 -𫭼 -𫮃 -𫰛 -𫵷 -𫶇 -𫷷 -𫸩 -𬀩 -𬀪 -𬂩 -𬃊 -𬇕 -𬇙 -𬇹 -𬉼 -𬊈 -𬊤 -𬌗 -𬍛 -𬍡 -𬍤 -𬒈 -𬒔 -𬒗 -𬕂 -𬘓 -𬘘 -𬘡 -𬘩 -𬘫 -𬘬 -𬘭 -𬘯 -𬙂 -𬙊 -𬙋 -𬜬 -𬜯 -𬞟 -𬟁 -𬟽 -𬣙 -𬣞 -𬣡 -𬣳 -𬤇 -𬤊 -𬤝 -𬨂 -𬨎 -𬩽 -𬪩 -𬬩 -𬬭 -𬬮 -𬬱 -𬬸 -𬬹 -𬬻 -𬬿 -𬭁 -𬭊 -𬭎 -𬭚 -𬭛 -𬭤 -𬭩 -𬭬 -𬭯 -𬭳 -𬭶 -𬭸 -𬭼 -𬮱 -𬮿 -𬯀 -𬯎 -𬱖 -𬱟 -𬳵 -𬳶 -𬳽 -𬳿 -𬴂 -𬴃 -𬴊 -𬶋 -𬶍 -𬶏 -𬶐 -𬶟 -𬶠 -𬶨 -𬶭 -𬶮 -𬷕 -𬸘 -𬸚 -𬸣 -𬸦 -𬸪 -𬹼 -𬺈 -𬺓 -🀄 -🃏 -🅰 -🅱 -🅾 -🅿 -🆎 -🆑 -🆒 -🆓 -🆔 -🆕 -🆖 -🆗 -🆘 -🆙 -🆚 -🇨 -🇩 -🇪 -🇫 -🇬 -🇮 -🇯 -🇰 -🇷 -🇺 -🈁 -🌀 -🌁 -🌂 -🌃 -🌄 -🌅 -🌆 -🌇 -🌈 -🌉 -🌊 -🌋 -🌌 -🌍 -🌎 -🌏 -🌐 -🌑 -🌒 -🌓 -🌔 -🌕 -🌖 -🌗 -🌘 -🌙 -🌚 -🌛 -🌜 -🌝 -🌞 -🌟 -🌠 -🌰 -🌱 -🌲 -🌳 -🌴 -🌵 -🌷 -🌸 -🌹 -🌺 -🌻 -🌼 -🌽 -🌾 -🌿 -🍀 -🍁 -🍂 -🍃 -🍄 -🍅 -🍆 -🍇 -🍈 -🍉 -🍊 -🍋 -🍌 -🍍 -🍎 -🍏 -🍐 -🍑 -🍒 -🍓 -🍔 -🍕 -🍖 -🍗 -🍘 -🍙 -🍚 -🍛 -🍜 -🍝 -🍞 -🍟 -🍠 -🍡 -🍢 -🍣 -🍤 -🍥 -🍦 -🍧 -🍨 -🍩 -🍪 -🍫 -🍬 -🍭 -🍮 -🍯 -🍰 -🍱 -🍲 -🍳 -🍴 -🍵 -🍶 -🍷 -🍸 -🍹 -🍺 -🍻 -🍼 -🎀 -🎁 -🎂 -🎃 -🎄 -🎅 -🎆 -🎇 -🎈 -🎉 -🎊 -🎋 -🎌 -🎍 -🎎 -🎏 -🎐 -🎑 -🎒 -🎓 -🎠 -🎡 -🎢 -🎣 -🎤 -🎥 -🎦 -🎧 -🎨 -🎩 -🎪 -🎫 -🎬 -🎭 -🎮 -🎯 -🎰 -🎱 -🎲 -🎳 -🎴 -🎵 -🎶 -🎷 -🎸 -🎹 -🎺 -🎻 -🎼 -🎽 -🎾 -🎿 -🏀 -🏁 -🏂 -🏃 -🏄 -🏆 -🏇 -🏈 -🏉 -🏊 -🏠 -🏡 -🏢 -🏣 -🏤 -🏥 -🏦 -🏧 -🏨 -🏩 -🏪 -🏫 -🏬 -🏭 -🏮 -🏯 -🏰 -🐀 -🐁 -🐂 -🐃 -🐄 -🐅 -🐆 -🐇 -🐈 -🐉 -🐊 -🐋 -🐌 -🐍 -🐎 -🐏 -🐐 -🐑 -🐒 -🐓 -🐔 -🐕 -🐖 -🐗 -🐘 -🐙 -🐚 -🐛 -🐜 -🐝 -🐞 -🐟 -🐠 -🐡 -🐢 -🐣 -🐤 -🐥 -🐦 -🐧 -🐨 -🐩 -🐪 -🐫 -🐬 -🐭 -🐮 -🐯 -🐰 -🐱 -🐲 -🐳 -🐴 -🐵 -🐶 -🐷 -🐸 -🐹 -🐺 -🐻 -🐼 -🐽 -🐾 -👀 -👂 -👃 -👄 -👅 -👆 -👇 -👈 -👉 -👊 -👋 -👌 -👍 -👎 -👏 -👐 -👑 -👒 -👓 -👔 -👕 -👖 -👗 -👘 -👙 -👚 -👛 -👜 -👝 -👞 -👟 -👠 -👡 -👢 -👣 -👤 -👥 -👦 -👧 -👨 -👩 -👪 -👫 -👬 -👭 -👮 -👯 -👰 -👱 -👲 -👳 -👴 -👵 -👶 -👷 -👸 -👹 -👺 -👻 -👼 -👽 -👾 -👿 -💀 -💁 -💂 -💃 -💄 -💅 -💆 -💇 -💈 -💉 -💊 -💋 -💌 -💍 -💎 -💏 -💐 -💑 -💒 -💓 -💔 -💕 -💖 -💗 -💘 -💙 -💚 -💛 -💜 -💝 -💞 -💟 -💠 -💡 -💢 -💣 -💤 -💥 -💦 -💧 -💨 -💩 -💪 -💫 -💬 -💭 -💮 -💯 -💰 -💲 -💳 -💴 -💵 -💶 -💷 -💸 -💹 -💺 -💻 -💼 -💽 -💾 -💿 -📀 -📁 -📂 -📃 -📄 -📅 -📆 -📇 -📈 -📉 -📊 -📋 -📌 -📍 -📎 -📏 -📐 -📑 -📒 -📓 -📔 -📕 -📖 -📗 -📘 -📙 -📚 -📛 -📜 -📝 -📞 -📟 -📠 -📡 -📢 -📣 -📤 -📥 -📦 -📧 -📨 -📩 -📪 -📫 -📬 -📭 -📮 -📯 -📰 -📱 -📲 -📳 -📴 -📵 -📶 -📷 -📹 -📺 -📻 -📼 -🔀 -🔁 -🔂 -🔃 -🔄 -🔅 -🔆 -🔇 -🔉 -🔊 -🔋 -🔌 -🔍 -🔎 -🔏 -🔐 -🔑 -🔒 -🔓 -🔔 -🔕 -🔖 -🔗 -🔘 -🔙 -🔚 -🔛 -🔜 -🔝 -🔞 -🔟 -🔠 -🔡 -🔢 -🔣 -🔤 -🔥 -🔦 -🔧 -🔨 -🔩 -🔪 -🔫 -🔬 -🔭 -🔮 -🔯 -🔰 -🔱 -🔲 -🔳 -🔴 -🔵 -🔶 -🔷 -🔸 -🔹 -🔺 -🔻 -🔼 -🔽 -🕐 -🕑 -🕒 -🕓 -🕔 -🕕 -🕖 -🕗 -🕘 -🕙 -🕚 -🕛 -🕜 -🕝 -🕞 -🕟 -🕠 -🕡 -🕢 -🕣 -🕤 -🕥 -🕦 -🕧 -🗻 -🗼 -🗽 -🗾 -🗿 -😀 -😁 -😂 -😃 -😄 -😅 -😆 -😇 -😈 -😉 -😊 -😋 -😌 -😍 -😎 -😏 -😐 -😑 -😒 -😓 -😔 -😕 -😖 -😗 -😘 -😙 -😚 -😛 -😜 -😝 -😞 -😟 -😠 -😡 -😢 -😣 -😤 -😥 -😦 -😧 -😨 -😩 -😪 -😫 -😬 -😭 -😮 -😯 -😰 -😱 -😲 -😳 -😴 -😵 -😶 -😷 -😸 -😹 -😺 -😻 -😼 -😽 -😾 -😿 -🙀 -🙅 -🙆 -🙇 -🙈 -🙉 -🙊 -🙋 -🙌 -🙍 -🙎 -🙏 -🚀 -🚁 -🚂 -🚃 -🚄 -🚅 -🚆 -🚇 -🚈 -🚉 -🚊 -🚌 -🚍 -🚎 -🚏 -🚐 -🚑 -🚒 -🚓 -🚔 -🚕 -🚖 -🚗 -🚘 -🚙 -🚚 -🚛 -🚜 -🚝 -🚞 -🚟 -🚠 -🚡 -🚢 -🚣 -🚤 -🚥 -🚦 -🚧 -🚨 -🚩 -🚪 -🚫 -🚬 -🚭 -🚮 -🚯 -🚰 -🚱 -🚲 -🚳 -🚴 -🚵 -🚶 -🚷 -🚸 -🚹 -🚺 -🚻 -🚼 -🚽 -🚾 -🚿 -🛀 -🛁 -🛂 -🛃 -🛄 -🛅 diff --git a/plugins/ppocr/assets/ppocrv6_rec.onnx b/plugins/ppocr/assets/ppocrv6_rec.onnx deleted file mode 100644 index 7c05294..0000000 Binary files a/plugins/ppocr/assets/ppocrv6_rec.onnx and /dev/null differ diff --git a/plugins/screenshot-monitor/app.plugin.js b/plugins/screenshot-monitor/app.plugin.js index d3daacf..5ed329a 100644 --- a/plugins/screenshot-monitor/app.plugin.js +++ b/plugins/screenshot-monitor/app.plugin.js @@ -46,12 +46,11 @@ function withScreenshotMonitor(config) { let content = modConfig.modResults.contents; // 2a. 注入 import - if (!content.includes(`import ${PACKAGE}.ScreenshotPackage`)) { - content = content.replace( - /^(package\s+[\w.]+;?\s*)$/m, - `$1\nimport ${PACKAGE}.ScreenshotPackage`, - ); - } + content = content.replace(/^import\s+[\w.]+\.ScreenshotPackage\s*$/gm, ''); + content = content.replace( + /^(package\s+[\w.]+;?\s*)$/m, + `$1\nimport ${PACKAGE}.ScreenshotPackage`, + ); // 2b. 在 getPackages() 的 .apply {} 块里注入 add(ScreenshotPackage()) if (!content.includes('add(ScreenshotPackage())')) { diff --git a/plugins/size-optimization/app.plugin.js b/plugins/size-optimization/app.plugin.js index feaea2e..d22a034 100644 --- a/plugins/size-optimization/app.plugin.js +++ b/plugins/size-optimization/app.plugin.js @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); function withSizeOptimization(config) { - // 1. 在 prebuild 时修改 gradle.properties + // 1. 在 prebuild 时修改 gradle.properties (开启 R8/ProGuard 代码混淆 + 资源裁剪 + .so 库压缩) config = withDangerousMod(config, [ 'android', async (modConfig) => { @@ -11,18 +11,42 @@ function withSizeOptimization(config) { const propertiesPath = path.join(projectRoot, 'gradle.properties'); if (fs.existsSync(propertiesPath)) { let content = fs.readFileSync(propertiesPath, 'utf8'); + + // 限制打包架构为 arm64-v8a if (content.includes('reactNativeArchitectures=')) { content = content.replace(/reactNativeArchitectures=.*/, 'reactNativeArchitectures=arm64-v8a'); } else { content += '\nreactNativeArchitectures=arm64-v8a\n'; } + + // 1. 开启 R8 代码混淆与摇树裁剪 + if (!content.includes('android.enableMinifyInReleaseBuilds=')) { + content += 'android.enableMinifyInReleaseBuilds=true\n'; + } else { + content = content.replace(/android\.enableMinifyInReleaseBuilds=.*/, 'android.enableMinifyInReleaseBuilds=true'); + } + + // 1. 开启无用资源裁剪 + if (!content.includes('android.enableShrinkResourcesInReleaseBuilds=')) { + content += 'android.enableShrinkResourcesInReleaseBuilds=true\n'; + } else { + content = content.replace(/android\.enableShrinkResourcesInReleaseBuilds=.*/, 'android.enableShrinkResourcesInReleaseBuilds=true'); + } + + // 2. 开启 .so 动态库在 APK 内的高倍率压缩 (useLegacyPackaging=true) + if (!content.includes('expo.useLegacyPackaging=')) { + content += 'expo.useLegacyPackaging=true\n'; + } else { + content = content.replace(/expo\.useLegacyPackaging=.*/, 'expo.useLegacyPackaging=true'); + } + fs.writeFileSync(propertiesPath, content, 'utf8'); } return modConfig; } ]); - // 2. 在 prebuild 时修改 app/build.gradle 启用 ABI Splits 分包 + // 2. 在 prebuild 时修改 app/build.gradle 启用 ABI Splits 分包 + 字体裁剪 config = withDangerousMod(config, [ 'android', async (modConfig) => { @@ -30,19 +54,71 @@ function withSizeOptimization(config) { const buildGradlePath = path.join(projectRoot, 'app/build.gradle'); if (fs.existsSync(buildGradlePath)) { let content = fs.readFileSync(buildGradlePath, 'utf8'); + + // ABI Splits 分包配置 if (!content.includes('splits {')) { content = content.replace( /android\s*\{/, `android {\n splits {\n abi {\n enable true\n reset()\n include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"\n universalApk true\n }\n }` ); - fs.writeFileSync(buildGradlePath, content, 'utf8'); + } + + // 3. 裁剪未使用的 @expo/vector-icons 矢量字体 + if (!content.includes('variant.mergeAssetsProvider.configure')) { + content += ` +android.applicationVariants.all { variant -> + if (variant.buildType.name == "release") { + variant.mergeAssetsProvider.configure { + doLast { + delete fileTree(dir: outputDir, includes: [ + "**/fonts/AntDesign.ttf", + "**/fonts/Entypo.ttf", + "**/fonts/EvilIcons.ttf", + "**/fonts/Feather.ttf", + "**/fonts/FontAwesome*.ttf", + "**/fonts/Fontisto.ttf", + "**/fonts/Foundation.ttf", + "**/fonts/MaterialCommunityIcons.ttf", + "**/fonts/MaterialIcons.ttf", + "**/fonts/Octicons.ttf", + "**/fonts/SimpleLineIcons.ttf", + "**/fonts/Zocial.ttf" + ]) + } + } + } +} +`; + } + fs.writeFileSync(buildGradlePath, content, 'utf8'); + } + return modConfig; + } + ]); + + // 3. 在 prebuild 时修改 proguard-rules.pro 保护原生插件与 ONNX 模块 + config = withDangerousMod(config, [ + 'android', + async (modConfig) => { + const projectRoot = modConfig.modRequest.platformProjectRoot; + const proguardPath = path.join(projectRoot, 'app/proguard-rules.pro'); + if (fs.existsSync(proguardPath)) { + let content = fs.readFileSync(proguardPath, 'utf8'); + if (!content.includes('com.beancount.mobile')) { + content += ` +# Size optimization: keep custom native packages & ONNX +-keep class com.beancount.mobile.** { *; } +-keep class com.example.driftledger.** { *; } +-keep class ai.onnxruntime.** { *; } +`; + fs.writeFileSync(proguardPath, content, 'utf8'); } } return modConfig; } ]); - // 3. 在 prebuild 时修改 root build.gradle 强制指定所有模块的 NDK 版本以匹配 27.1.12297006 + // 4. 在 prebuild 时修改 root build.gradle 强制指定所有模块的 NDK 版本以匹配 27.1.12297006 config = withDangerousMod(config, [ 'android', async (modConfig) => { diff --git a/src/ai/chatAssistant.ts b/src/ai/chatAssistant.ts index 87224b3..f415e68 100644 --- a/src/ai/chatAssistant.ts +++ b/src/ai/chatAssistant.ts @@ -39,6 +39,8 @@ export function isTransactionIntent(input: string): boolean { return hasAmount && hasKeyword; } +import { logger } from '../utils/logger'; + /** * 处理用户消息:判定意图 → 记账或自由对话。 */ @@ -47,11 +49,14 @@ export async function processChatMessage( provider: AiProvider, conversation?: ChatConversation, ): Promise { + logger.info('aiChat', `收到 AI 助手对话请求 [文本: "${input}"]`); try { // 意图判定 if (isTransactionIntent(input)) { + logger.info('aiChat', '检测到记账意图,触发自然语言识别流水线'); const result = await processNaturalLanguage(input, provider); if (result.type === 'draft') { + logger.info('aiChat', `AI 识别生成交易草稿: [时间: ${result.draft.time}, 金额: ${result.draft.amount} ${result.draft.currency}, 叙述: "${result.draft.narration}"]`); return { type: 'bill_card', billCards: [result.draft] }; } // 非 draft 但有意图 → 返回 AI 文本 @@ -59,10 +64,13 @@ export async function processChatMessage( } // 自由对话 + logger.info('aiChat', '未命中记账模式,发起 AI 自由问答'); const messages = buildChatContext(input, conversation); const response = await provider.chat(messages); + logger.info('aiChat', 'AI 自由问答回复成功'); return { type: 'text', text: removeThink(response) }; } catch (e) { + logger.error('aiChat', 'AI 对话助手响应失败', e); return { type: 'error', error: e instanceof Error ? e.message : String(e) }; } } diff --git a/src/ai/monthlySummary.ts b/src/ai/monthlySummary.ts index 42bc6f8..2aa50c8 100644 --- a/src/ai/monthlySummary.ts +++ b/src/ai/monthlySummary.ts @@ -50,6 +50,8 @@ export function calculateMonthlyStats( }; } +import { logger } from '../utils/logger'; + /** * 生成 AI 月度总结。 */ @@ -59,20 +61,27 @@ export async function generateMonthlySummary( month: number, provider: AiProvider, ): Promise { - const stats = calculateMonthlyStats(transactions, year, month); + logger.info('aiSummary', `开始生成 AI 月度财务总结 [${year}年${month}月]`); + try { + const stats = calculateMonthlyStats(transactions, year, month); - const messages = buildMonthlySummaryPrompt({ - totalExpense: stats.totalExpense, - totalIncome: stats.totalIncome, - transactionCount: stats.transactionCount, - topCategories: stats.topCategories, - }); + const messages = buildMonthlySummaryPrompt({ + totalExpense: stats.totalExpense, + totalIncome: stats.totalIncome, + transactionCount: stats.transactionCount, + topCategories: stats.topCategories, + }); - const response = await provider.chat(messages); - return { - stats, - summary: removeThink(response), - }; + const response = await provider.chat(messages); + logger.info('aiSummary', `AI 月度财务总结生成成功 [${year}年${month}月]`); + return { + stats, + summary: removeThink(response), + }; + } catch (e) { + logger.error('aiSummary', `生成 AI 月度总结失败 [${year}年${month}月]`, e); + throw e; + } } /** 无 AI 的纯文本总结(降级,不调 AI)。 */ diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx index 625abca..435470c 100644 --- a/src/app/(tabs)/index.tsx +++ b/src/app/(tabs)/index.tsx @@ -141,6 +141,13 @@ export default function HomeScreen() { + {/* 月度趋势 */} + {monthlyData.length > 0 && ( + + + + )} + {/* 最近 5 条交易 */} {recentTxs.length > 0 && ( @@ -152,13 +159,6 @@ export default function HomeScreen() { )} - - {/* 月度趋势 */} - {monthlyData.length > 0 && ( - - - - )} ); diff --git a/src/app/(tabs)/settings.tsx b/src/app/(tabs)/settings.tsx index ed77120..b1ad980 100644 --- a/src/app/(tabs)/settings.tsx +++ b/src/app/(tabs)/settings.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter, type Href } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../../theme'; import { useT } from '../../i18n'; import { Card } from '../../components/Card'; +import { Touchable } from '../../components/Touchable'; export default function SettingsScreen() { const { theme } = useTheme(); @@ -13,15 +14,14 @@ export default function SettingsScreen() { const router = useRouter(); const renderNavLink = (icon: keyof typeof Ionicons.glyphMap, label: string, path: Href) => ( - router.push(path)} accessibilityRole="link" accessibilityLabel={label} - style={({ pressed }) => [ + style={[ styles.navRow, { borderBottomColor: theme.colors.divider, - opacity: pressed ? 0.6 : 1, }, ]} > @@ -30,7 +30,7 @@ export default function SettingsScreen() { {label} - + ); return ( @@ -67,6 +67,7 @@ export default function SettingsScreen() { {renderNavLink('cloud-upload-outline', t('settings.syncBackup'), '/settings/sync')} {renderNavLink('pulse-outline', t('settings.diagnostics'), '/settings/diagnostics' as Href)} + {renderNavLink('document-text-outline', t('settings.appLogs'), '/settings/logs' as Href)} @@ -74,7 +75,7 @@ export default function SettingsScreen() { {renderNavLink('phone-portrait-outline', t('settings.preferencesEntry'), '/settings/preferences')} - {renderNavLink('cog-outline', t('settings.aiSettingsTitle'), '/settings/ai')} + {renderNavLink('cog-outline', t('settings.llmTitle'), '/settings/ai')} {renderNavLink('chatbubble-ellipses-outline', t('ai.chatTitle'), '/ai/chat' as Href)} diff --git a/src/app/(tabs)/transactions.tsx b/src/app/(tabs)/transactions.tsx index 73b91eb..7c601db 100644 --- a/src/app/(tabs)/transactions.tsx +++ b/src/app/(tabs)/transactions.tsx @@ -11,6 +11,7 @@ import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useLedgerStore } from '../../store/ledgerStore'; import { useMetadataStore } from '../../store/metadataStore'; +import { useNumpadUiStore } from '../../store/numpadUiStore'; import { useTheme, createCommonStyles } from '../../theme'; import { useT } from '../../i18n'; import { Card } from '../../components/Card'; @@ -85,7 +86,7 @@ export default function TransactionsScreen() { // cost/price 不带:复制是新建交易,丢 cost 属可接受简化(含 cost 的交易可从编辑入口进高级模式) postings: tx.postings.map(p => ({ account: p.account, amount: p.amount, currency: p.currency })), }); - router.push({ pathname: '/transaction/new', params: { draftJson } }); + useNumpadUiStore.getState().open({ draftJson }); }; // 左滑:删除(确认后从 main.bean 移除) @@ -137,7 +138,7 @@ export default function TransactionsScreen() { item.id} + keyExtractor={(item, index) => `${item.id}-${index}`} renderItem={({ item: tx }) => ( s.locale); + const t = useT(); + useEffect(() => { + // useT() 每次渲染返回新 bind,故依赖用 locale 而非 t; + // locale 变化时 useT 内部已将 i18n.locale 指向新语言 + void pushFloatingUiConfig(theme, t); + }, [theme, locale]); + return null; +} + /** 原生悬浮窗跳转 App 事件类型 */ interface NativeOpenAppEvent { draftId?: string; @@ -41,6 +62,15 @@ interface NativeOpenAppEvent { direction?: string; merchant?: string; narration?: string; + currency?: string; +} + +function sanitizeLogText(text: string): string { + if (!text) return ''; + // 脱敏验证码 (4-6位) 及长卡号/身份证 (8位以上数字) + return text + .replace(/\b(\d{4,6})\b(?=.*(?:验证码|动态码|校验码|code|Code))/gi, '***') + .replace(/\b(\d{4})\d{8,11}(\d{4})\b/g, '$1****$2'); } /** 示例账本(后续由文件选择器导入,当前 demo 用内置)。 */ @@ -109,9 +139,19 @@ function AppShell() { persistenceInitRef.current = true; // 1. 初始化持久化并还原数据 initPersistence().then(async () => { + // 初始化磁盘日志持久化系统 + logger.initFileBackend(expoLogBackend).catch(e => { + logger.warn('layout', '磁盘日志初始化失败', e); + }); + // 加载已处理的交易键(跨会话去重) await loadProcessedTxKeys(); + // P8:确保 OCR 模型文件可用(首次启动从 APK assets 拷贝到 filesDir) + ensureOcrModels().catch(e => { + logger.warn('layout', 'OCR 模型初始化失败(非关键路径,截图 OCR 将降级)', e); + }); + const mainPath = FileSystem.documentDirectory + 'main.bean'; // 架构说明:main.bean 作为主账本文件,包含所有 Beancount 指令(open/close/option/include 等), @@ -133,6 +173,14 @@ function AppShell() { const completed = useSettingsStore.getState().onboardingCompleted; const locked = useSettingsStore.getState().appLockEnabled; + const floatingEnabled = useSettingsStore.getState().floatingBallEnabled; + + // 开启/初始化时同步悬浮球开关至原生无障碍服务 + const bridge = getAccessibilityBridge(); + if (bridge) { + bridge.setFloatingBallEnabled(floatingEnabled).catch(() => {}); + } + if (!completed) { setPhase('onboarding'); } else if (locked) { @@ -154,7 +202,7 @@ function AppShell() { if (phase === 'ready') { const cleanup = setupDeepLinking((action) => { if (action.type === 'add-transaction') { - router.push('/transaction/new'); + useNumpadUiStore.getState().open(); } else if (action.type === 'open-tab') { if (action.tab === 'home') router.push('/(tabs)'); else if (action.tab === 'transactions') router.push('/(tabs)/transactions'); @@ -164,7 +212,7 @@ function AppShell() { } else if (action.type === 'import-csv') { router.push('/import'); } else if (action.type === 'ocr-camera' || action.type === 'ocr-image') { - router.push({ pathname: '/transaction/new', params: { mode: 'ocr' } }); + useNumpadUiStore.getState().open({ autoOcr: true }); } else if (action.type === 'voice-input') { router.push('/(tabs)/settings'); } @@ -182,13 +230,14 @@ function AppShell() { const subscriptions = [ DeviceEventEmitter.addListener('billingNotification', (event) => { try { - logger.debug('layout', `收到原生通知, 包名: ${event.packageName}, 标题: ${event.title}, 内容: ${event.text}`); + const safeText = sanitizeLogText(event.text || ''); + logger.debug('layout', `收到原生通知, 包名: ${event.packageName}, 标题: ${event.title}, 内容: ${safeText}`); const bill = parseNotification(event); if (bill) { - logger.debug('layout', `通知解析成功 [时间: ${bill.occurredAt}, 方向: ${bill.direction === 'income' ? '收入' : '支出'}, 金额: ${bill.amount} ${bill.currency}, 商户: ${bill.counterparty}, 备注: ${bill.memo}] | 原始通知: [${event.title}] ${event.text}`); + logger.debug('layout', `通知解析成功 [时间: ${bill.occurredAt}, 方向: ${bill.direction === 'income' ? '收入' : '支出'}, 金额: ${bill.amount} ${bill.currency}, 商户: ${bill.counterparty}, 备注: ${bill.memo}] | 原始通知: [${event.title}] ${safeText}`); handleIncomingBillEvent('notification', bill, `[${event.title}] ${event.text}`); } else { - logger.debug('layout', `通知未匹配为账单: [${event.title}] ${event.text}`); + logger.debug('layout', `通知未匹配为账单: [${event.title}] ${safeText}`); } } catch (e) { logger.error('layout', '通知解析失败', e); @@ -196,7 +245,8 @@ function AppShell() { }), DeviceEventEmitter.addListener('billingSms', (event) => { try { - logger.debug('layout', `收到原生短信, 发送者: ${event.address || event.sender || '未知'}, 内容: ${event.body}`); + const safeBody = sanitizeLogText(event.body || ''); + logger.debug('layout', `收到原生短信, 发送者: ${event.address || event.sender || '未知'}, 内容: ${safeBody}`); const smsEvent = { sender: event.sender || event.address || '', body: event.body, @@ -204,73 +254,74 @@ function AppShell() { }; const bill = parseSms(smsEvent); if (bill) { - logger.debug('layout', `短信解析成功 [时间: ${bill.occurredAt}, 方向: ${bill.direction === 'income' ? '收入' : '支出'}, 金额: ${bill.amount} ${bill.currency}, 商户: ${bill.counterparty}, 备注: ${bill.memo}] | 原始短信: ${event.body}`); + logger.debug('layout', `短信解析成功 [时间: ${bill.occurredAt}, 方向: ${bill.direction === 'income' ? '收入' : '支出'}, 金额: ${bill.amount} ${bill.currency}, 商户: ${bill.counterparty}, 备注: ${bill.memo}] | 原始短信: ${safeBody}`); handleIncomingBillEvent('sms', bill, event.body); } else { - logger.debug('layout', `短信未匹配为账单: ${event.body}`); + logger.debug('layout', `短信未匹配为账单: ${safeBody}`); } } catch (e) { logger.error('layout', '短信解析失败', e); } }), - DeviceEventEmitter.addListener('billingScreenshot', (event) => { - logger.debug('layout', `收到原生截图/无障碍截图, 包名: ${event.packageName}`); - processScreenshotEvent(event).catch((e: unknown) => { - logger.error('layout', '截图 OCR 处理失败', e); - }); - }), - DeviceEventEmitter.addListener('billingOpenApp', (res: NativeOpenAppEvent) => { + DeviceEventEmitter.addListener('billingScreenshot', async (event) => { try { + logger.debug('layout', `收到原生截图/无障碍截图, 包名: ${event.packageName}`); + await processScreenshotEvent(event); + } catch (e) { + logger.error('layout', `截图 OCR 处理失败 (包名: ${event.packageName}, 是否有Base64: ${Boolean(event.imageBase64)})`, e instanceof Error ? { message: e.message, stack: e.stack } : String(e)); + } + }), + DeviceEventEmitter.addListener('billingOpenApp', async (event) => { + try { + const res = event as NativeOpenAppEvent; logger.info('layout', `收到原生悬浮窗跳转 App 请求: ${JSON.stringify(res)}`); - if (res.draftId) { - try { - pendingDrafts.delete(res.draftId); - } catch (err) { - // 忽略 - } + if (res.confirmed) { + // 已由原生悬浮窗确认入账,前端仅记录日志或提示 + return; } - const amt = res.amount || '0'; - if (!res.account || !res.category || !amt || amt === '0') { + if (!res.amount && !res.merchant) { logger.warn('layout', 'billingOpenApp 数据不完整,跳过', JSON.stringify(res)); return; } - try { - const parsed = parseDecimal(amt); - if (parsed.coefficient <= 0n) { + const amt = res.amount ?? '0'; + const validAmount = parseDecimal(amt); + if (!validAmount) { + if (amt.length > 0) { + logger.warn('layout', 'billingOpenApp 金额格式错误', amt); + } else { logger.warn('layout', 'billingOpenApp 金额无效', amt); - return; } - } catch { - logger.warn('layout', 'billingOpenApp 金额格式错误', amt); return; } - const currency = 'CNY'; - const dir = (res.direction || 'expense') as 'income' | 'expense' | 'transfer'; - const postings = buildPostings(dir, res.account, res.category, amt, currency); + const direction = res.direction ?? 'expense'; + const categoryAccount = res.category ?? 'Expenses:未分类'; + const sourceAccount = res.account ?? 'Assets:未知'; + const currency = res.currency ?? 'CNY'; + const postings = [ + { account: sourceAccount, amount: direction === 'expense' ? `-${validAmount}` : validAmount, currency }, + { account: categoryAccount, amount: direction === 'expense' ? validAmount : `-${validAmount}`, currency } + ]; const draft = { date: res.time ? res.time.split(' ')[0] : new Date().toISOString().split('T')[0], payee: res.merchant || undefined, narration: res.narration || '', postings }; - router.push({ - pathname: '/transaction/new', - params: { draftJson: JSON.stringify(draft) } - }); + useNumpadUiStore.getState().open({ draftJson: JSON.stringify(draft) }); } catch (e) { logger.error('layout', '处理悬浮窗跳转 App 失败', e); } }), DeviceEventEmitter.addListener('billingPageRemembered', (event) => { - logger.info('layout', `[无障碍调试] 成功记住页面签名: ${event.signature} (包名: ${event.package}, 类名: ${event.activity})`); - logger.info('layout', `[无障碍调试] 记住该页面时捕获的所有文本内容: ${JSON.stringify(event.texts)}`); + logger.debug('layout', `[无障碍调试] 成功记住页面签名: ${event.signature} (包名: ${event.package}, 类名: ${event.activity})`); + logger.debug('layout', `[无障碍调试] 记住该页面时捕获的所有文本内容: ${JSON.stringify(event.texts)}`); }), DeviceEventEmitter.addListener('billingDebugNodes', async (event) => { const { package: pkg, activity, texts, isManual } = event; const sigKey = `${pkg}|${activity}`; - logger.info('layout', `[无障碍监听] 页面特征信号: ${sigKey}`); - logger.info('layout', `[无障碍监听] 页面提取到的文本内容: ${JSON.stringify(texts)}`); + logger.debug('layout', `[无障碍监听] 页面特征信号: ${sigKey}`); + logger.debug('layout', `[无障碍监听] 页面提取到的文本内容: ${JSON.stringify(texts?.slice(0, 5))}`); try { const bridge = getAccessibilityBridge(); @@ -279,7 +330,7 @@ function AppShell() { const signatures = await bridge.getPageSignatures(); const isWhitelisted = signatures.some(s => s.signature === sigKey); if (!isWhitelisted) { - logger.info('layout', `[无障碍监听] 页面 ${sigKey} 未在自动记账白名单中,拒绝弹窗`); + logger.debug('layout', `[无障碍监听] 页面 ${sigKey} 未在自动记账白名单中,拒绝弹窗`); return; } } @@ -298,11 +349,12 @@ function AppShell() { if (isDetail) { const success = await parseAndProcessAccessibilityTexts(texts, pkg); if (!success) { - logger.info('layout', '[无障碍] 微信详情页直接文本解析未成功,降级触发 OCR 识别'); + const textSnippet = texts.slice(0, 5).join(' | '); + logger.info('layout', `[无障碍] 微信详情页直接文本解析未成功 (截获文本前5句: [${textSnippet}]),降级触发 OCR 识别`); bridge?.triggerManualOcr(); } } else { - logger.info('layout', '[无障碍] 微信非详情页面,不触发记账与截图'); + logger.debug('layout', '[无障碍] 微信非详情页面,不触发记账与截图'); } } else if (pkg === 'com.eg.android.AlipayGphone') { // 支付宝:识别是否是详情页 @@ -310,11 +362,12 @@ function AppShell() { if (isDetail) { const success = await parseAndProcessAccessibilityTexts(texts, pkg); if (!success) { - logger.info('layout', '[无障碍] 支付宝详情页直接文本解析未成功,降级触发 OCR 识别'); + const textSnippet = texts.slice(0, 5).join(' | '); + logger.info('layout', `[无障碍] 支付宝详情页直接文本解析未成功 (截获文本前5句: [${textSnippet}]),降级触发 OCR 识别`); bridge?.triggerManualOcr(); } } else { - logger.info('layout', '[无障碍] 支付宝非详情页面,不触发记账与截图'); + logger.debug('layout', '[无障碍] 支付宝非详情页面,不触发记账与截图'); } } else { // 其他白名单应用(如各大银行、QQ、TIM等),直接通过截图 + OCR 识别 @@ -377,7 +430,6 @@ function AppShell() { - @@ -413,6 +465,7 @@ export default function RootLayout() { return ( + diff --git a/src/app/_onboarding.tsx b/src/app/_onboarding.tsx index 8994ad8..b932013 100644 --- a/src/app/_onboarding.tsx +++ b/src/app/_onboarding.tsx @@ -12,14 +12,16 @@ * 完成后标记 onboarding 已完成(持久化),后续启动不再显示。 */ -import React, { useState } from 'react'; -import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { AppState, Platform, PermissionsAndroid, Linking, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../theme'; import { useT } from '../i18n'; import { useSettingsStore, type Locale, type ThemeMode } from '../store/settingsStore'; import { Button } from '../components/Button'; +import { getAccessibilityBridge } from '../services/accessibilityBridge'; +import { Card } from '../components/Card'; export interface OnboardingStep { key: string; @@ -52,6 +54,68 @@ export function OnboardingScreen({ onComplete }: OnboardingProps) { const current = steps[step]; const isLast = step === steps.length - 1; + // ── 权限状态管理(P7 Task 2) ── + const [permStates, setPermStates] = useState>({ + accessibility: null, notification: null, sms: null, storage: null, overlay: null, + }); + const permsChecked = !Object.values(permStates).some(v => v === null); + const allGranted = permsChecked && Object.values(permStates).every(Boolean); + + useEffect(() => { + if (Platform.OS !== 'android') return; + + const checkAllPermissions = async () => { + if (current.key !== 'permissions') return; + const bridge = getAccessibilityBridge(); + let a = false, n = false, o = false, s = false, st = false; + if (bridge) { + try { a = await bridge.isServiceRunning(); } catch { /* false */ } + try { n = await bridge.isNotificationListenerEnabled(); } catch { /* false */ } + try { o = await bridge.canDrawOverlays(); } catch { /* false */ } + } + try { s = await PermissionsAndroid.check('android.permission.RECEIVE_SMS') as boolean; } catch { /* false */ } + const storagePerm = parseInt(String(Platform.Version), 10) >= 33 + ? 'android.permission.READ_MEDIA_IMAGES' : 'android.permission.READ_EXTERNAL_STORAGE'; + try { st = await PermissionsAndroid.check(storagePerm) as boolean; } catch { /* false */ } + setPermStates({ accessibility: a, notification: n, overlay: o, sms: s, storage: st }); + }; + + checkAllPermissions(); + + // 从系统设置返回后重新检查 + const sub = AppState.addEventListener('change', (state) => { + if (state === 'active') checkAllPermissions(); + }); + return () => sub.remove(); + }, [current.key]); + + const handlePermAction = async (key: string) => { + const storagePerm = parseInt(String(Platform.Version), 10) >= 33 + ? 'android.permission.READ_MEDIA_IMAGES' + : 'android.permission.READ_EXTERNAL_STORAGE'; + switch (key) { + case 'accessibility': + await Linking.sendIntent('android.settings.ACCESSIBILITY_SETTINGS'); + break; + case 'notification': + await Linking.sendIntent('android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS'); + break; + case 'sms': { + const result = await PermissionsAndroid.request('android.permission.RECEIVE_SMS'); + setPermStates(p => ({ ...p, sms: result === PermissionsAndroid.RESULTS.GRANTED })); + break; + } + case 'storage': { + const result = await PermissionsAndroid.request(storagePerm); + setPermStates(p => ({ ...p, storage: result === PermissionsAndroid.RESULTS.GRANTED })); + break; + } + case 'overlay': + await Linking.sendIntent('android.settings.action.MANAGE_OVERLAY_PERMISSION'); + break; + } + }; + return ( @@ -97,6 +161,55 @@ export function OnboardingScreen({ onComplete }: OnboardingProps) { )} + {current.key === 'permissions' && Platform.OS === 'android' && ( + + + {[ + { key: 'accessibility', icon: 'accessibility-outline' as const, label: t('onboarding.permAccessibility') }, + { key: 'notification', icon: 'notifications-outline' as const, label: t('onboarding.permNotification') }, + { key: 'sms', icon: 'chatbubble-outline' as const, label: t('onboarding.permSms') }, + { key: 'storage', icon: 'images-outline' as const, label: t('onboarding.permStorage') }, + { key: 'overlay', icon: 'tablet-landscape-outline' as const, label: t('onboarding.permOverlay') }, + ].map(item => { + const status = permStates[item.key]; + const granted = status === true; + const statusColor = granted ? theme.colors.success : theme.colors.fgSecondary; + const statusText = status === null ? '...' : granted ? t('onboarding.permGranted') : t('onboarding.permNotGranted'); + return ( + + + + + {item.label} + + + + + {statusText} + + {!granted && !allGranted && ( +