diff --git a/.gitignore b/.gitignore index 13b6122..978c90c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,8 @@ web-build/ expo-env.d.ts # Native -android/ -ios/ +/android/ +/ios/ *.hprof # Metro @@ -35,17 +35,16 @@ yarn-error.* .env.* # Outputs -outputs/ +/outputs/ # Example financial data (may contain real PII) -example/ +/example/ # AI assistant config CLAUDE.md # MiMoCode -.mimocode/ -.agent/ -reference_project/ -.zcode/ -example/ \ No newline at end of file +/.mimocode/ +/.agents/ +/reference_project/ +/.zcode/ \ No newline at end of file diff --git a/app.json b/app.json index ff8a683..2111b08 100644 --- a/app.json +++ b/app.json @@ -17,6 +17,7 @@ "./plugins/notification-listener", "./plugins/sms-receiver", "./plugins/screenshot-monitor", + "./plugins/size-optimization", "expo-secure-store" ], "experiments": { diff --git a/design-system/beancount-mobile/MASTER.md b/design-system/beancount-mobile/MASTER.md new file mode 100644 index 0000000..a72519b --- /dev/null +++ b/design-system/beancount-mobile/MASTER.md @@ -0,0 +1,206 @@ +# Design System Master File + +> **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`. +> If that file exists, its rules **override** this Master file. +> If not, strictly follow the rules below. + +--- + +**Project:** beancount-mobile +**Generated:** 2026-07-16 16:32:57 +**Category:** Personal Finance Tracker + +--- + +## Global Rules + +### Color Palette + +| Role | Hex | CSS Variable | +|------|-----|--------------| +| Primary | `#1E40AF` | `--color-primary` | +| On Primary | `#FFFFFF` | `--color-on-primary` | +| Secondary | `#3B82F6` | `--color-secondary` | +| Accent/CTA | `#059669` | `--color-accent` | +| Background | `#0F172A` | `--color-background` | +| Foreground | `#FFFFFF` | `--color-foreground` | +| Muted | `#101A34` | `--color-muted` | +| Border | `rgba(255,255,255,0.08)` | `--color-border` | +| Destructive | `#DC2626` | `--color-destructive` | +| Ring | `#1E40AF` | `--color-ring` | + +**Color Notes:** Trust blue + profit green on dark + +### Typography + +- **Heading Font:** Caveat +- **Body Font:** Quicksand +- **Mood:** handwritten, personal, friendly, casual, warm, charming +- **Google Fonts:** [Caveat + Quicksand](https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600;700&family=Quicksand:wght@300;400;500;600;700&display=swap) + +**CSS Import:** +```css +@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600;700&family=Quicksand:wght@300;400;500;600;700&display=swap'); +``` + +### Spacing Variables + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-xs` | `4px` / `0.25rem` | Tight gaps | +| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing | +| `--space-md` | `16px` / `1rem` | Standard padding | +| `--space-lg` | `24px` / `1.5rem` | Section padding | +| `--space-xl` | `32px` / `2rem` | Large gaps | +| `--space-2xl` | `48px` / `3rem` | Section margins | +| `--space-3xl` | `64px` / `4rem` | Hero padding | + +### Shadow Depths + +| Level | Value | Usage | +|-------|-------|-------| +| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift | +| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons | +| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns | +| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards | + +--- + +## Component Specs + +### Buttons + +```css +/* Primary Button */ +.btn-primary { + background: #059669; + color: white; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} + +.btn-primary:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +/* Secondary Button */ +.btn-secondary { + background: transparent; + color: #1E40AF; + border: 2px solid #1E40AF; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} +``` + +### Cards + +```css +.card { + background: #0F172A; + border-radius: 12px; + padding: 24px; + box-shadow: var(--shadow-md); + transition: all 200ms ease; + cursor: pointer; +} + +.card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} +``` + +### Inputs + +```css +.input { + padding: 12px 16px; + border: 1px solid #E2E8F0; + border-radius: 8px; + font-size: 16px; + transition: border-color 200ms ease; +} + +.input:focus { + border-color: #1E40AF; + outline: none; + box-shadow: 0 0 0 3px #1E40AF20; +} +``` + +### Modals + +```css +.modal-overlay { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.modal { + background: white; + border-radius: 16px; + padding: 32px; + box-shadow: var(--shadow-xl); + max-width: 500px; + width: 90%; +} +``` + +--- + +## Style Guidelines + +**Style:** Dark Mode (OLED) + +**Keywords:** Dark theme, low light, high contrast, deep black, midnight blue, eye-friendly, OLED, night mode, power efficient + +**Best For:** Night-mode apps, coding platforms, entertainment, eye-strain prevention, OLED devices, low-light + +**Key Effects:** Minimal glow (text-shadow: 0 0 10px), dark-to-light transitions, low white emission, high readability, visible focus + +### Page Pattern + +**Pattern Name:** Interactive Product Demo + +- **CTA Placement:** Above fold +- **Section Order:** Hero > Features > CTA + +--- + +## Anti-Patterns (Do NOT Use) + +- ❌ Pure white backgrounds + +### Additional Forbidden Patterns + +- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons) +- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer +- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout +- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio +- ❌ **Instant state changes** — Always use transitions (150-300ms) +- ❌ **Invisible focus states** — Focus states must be visible for a11y + +--- + +## Pre-Delivery Checklist + +Before delivering any UI code, verify: + +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons from consistent icon set (Heroicons/Lucide) +- [ ] `cursor-pointer` on all clickable elements +- [ ] Hover states with smooth transitions (150-300ms) +- [ ] Light mode: text contrast 4.5:1 minimum +- [ ] Focus states visible for keyboard navigation +- [ ] `prefers-reduced-motion` respected +- [ ] Responsive: 375px, 768px, 1024px, 1440px +- [ ] No content hidden behind fixed navbars +- [ ] No horizontal scroll on mobile diff --git a/docs/android-build-guide.md b/docs/android-build-guide.md index 1e78aad..7f9acdb 100644 --- a/docs/android-build-guide.md +++ b/docs/android-build-guide.md @@ -1,529 +1,100 @@ -# Android 编译与真机运行完整指南 +# Android 安装包打包与体积优化指南 (Android Build Guide) -> 本文档记录了从零开始搭建 Android 编译环境、修复 Config Plugin / Kotlin / 依赖冲突、到最终在真机上成功运行 debug APK 的完整流程。 -> -> 适用环境:Windows 11 + Expo SDK 54 + React Native 0.81 + PP-OCRv5 (ONNX Runtime)。 +在引入机器学习引擎(ONNX Runtime)及 React Native 新架构后,本项目的原生 C/C++ SO 库体积大幅增加。为避免将无意义的冗余架构包打包给最终手机用户,本项目对 Android 构建配置进行了专项体积优化。 + +本篇指南将为您介绍项目中的打包机制、优化细节,以及如何通过命令行进行差异化编译。 --- -## 目录 +## 1. 体积优化核心原理 -1. [环境准备](#1-环境准备) -2. [安装 Android SDK + NDK](#2-安装-android-sdk--ndk) -3. [ONNX 模型获取](#3-onnx-模型获取) -4. [expo prebuild](#4-expo-prebuild) -5. [Gradle 编译](#5-gradle-编译) -6. [安装到真机](#6-安装到真机) -7. [Metro + 运行](#7-metro--运行) -8. [常见问题与解决方案](#8-常见问题与解决方案) -9. [已修复的 Bug 清单](#9-已修复的-bug-清单) +### 1.1 ABI 分包 (ABI Splits) +在 Android 系统的 `build.gradle` 中,我们启用了分包编译: +```groovy +splits { + abi { + enable true + reset() + include "armeabi-v7a", "arm64-v8a", "x86", "x86_64" + universalApk true + } +} +``` +* **效果**:系统会为每种 CPU 架构单独输出一个体积小巧的 APK,并额外保留一个兼容所有架构的通用包(Universal APK)。 +* **独立包大小**:~35MB - ~40MB +* **通用包大小**:~140MB - ~170MB + +### 1.2 默认编译架构限制 +我们在 `android/gradle.properties` 中指定了默认编译目标为: +```properties +reactNativeArchitectures=arm64-v8a +``` +* **为什么只包含 arm64-v8a**:目前 99% 的主流现代 Android 实体机都是 64 位 ARM 架构(`arm64-v8a`)。在进行日常分发与本地 Release 测试时,默认只编译 arm64-v8a,能够省去编译另外 3 个架构的机器指令时间,**编译速度提升 3~4 倍**,且输出的包体最小。 + +### 1.3 Expo 持续原生生成 (Config Plugin) 的自动应用 +> [!IMPORTANT] +> 由于 `android/` 目录被 Git 忽略,**不要直接手动在其他设备上提交原生配置变更**。 +> 本项目已编写了专门的本地 Config Plugin:[size-optimization](file:///c:/Users/fmq/Documents/work/beancount-mobile/plugins/size-optimization/app.plugin.js)。 +> 当在新设备上重新 `git clone` 项目后,运行以下指令即可自动拉起 Config Plugin 并在重新生成的 `android/` 目录中完美注入上述所有的体积优化配置(ABI 分包、默认单架构编译): +> ```bash +> npx expo prebuild --platform android +> ``` --- -## 1. 环境准备 +## 2. 编译命令与打包指令 -### JDK - -React Native 0.81 + Gradle 8.x 需要 **JDK 17+**。本项目使用 **JDK 21**。 - -```bash -# 验证 -java -version -# 应输出: java version "21.0.x" - -# 确认 JAVA_HOME 指向正确的 JDK -echo $JAVA_HOME -# 应输出: C:\Program Files\Java\jdk-21 -``` - -> **注意**:如果 `JAVA_HOME` 指向了别的用户的残留目录(如 `D:\Users\yxp\JDK`),需要通过「系统属性 → 环境变量 → 系统变量」修正为实际 JDK 路径。 - -### Node.js - -```bash -node -v # 需要 v20+ -npm -v -``` - ---- - -## 2. 安装 Android SDK + NDK - -### 方式 A:Android Studio(推荐) - -1. 下载 Android Studio:https://developer.android.com/studio -2. 安装时勾选 Android SDK / SDK Platform / Android Virtual Device -3. 首次启动自动下载 SDK 到默认位置 `C:\Users\<用户>\AppData\Local\Android\Sdk` - -### 方式 B:命令行(轻量) - -如果已装 Android Studio 但缺少 cmdline-tools: - -```bash -SDK="$HOME/AppData/Local/Android/Sdk" -TMP="/tmp" - -# 下载 cmdline-tools(官方,约 130MB) -curl -L -o "$TMP/cmdline-tools.zip" \ - "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip" - -# 解压到标准位置 -mkdir -p "$SDK/cmdline-tools" -unzip -q "$TMP/cmdline-tools.zip" -d "$TMP/" -cp -r "$TMP/cmdline-tools" "$SDK/cmdline-tools/latest" -``` - -### 必装组件 - -```bash -export JAVA_HOME="C:/Program Files/Java/jdk-21" -export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" -SDKMGR="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager.bat" - -# 接受所有 license -yes | "$SDKMGR" --licenses - -# 安装必需组件 -yes | "$SDKMGR" \ - "platform-tools" \ - "platforms;android-35" \ - "build-tools;35.0.0" -``` - -| 组件 | 用途 | -|------|------| -| `platform-tools` | adb(设备连接) | -| `platforms;android-35` | 编译目标 SDK | -| `build-tools;35.0.0` | aapt/dex/打包工具 | -| `cmdline-tools;latest` | sdkmanager(expo prebuild 需要) | - -### NDK(React Native 编译 C++ 需要) - -React Native 0.81 要求 **NDK 27.1.12297006**(~745MB)。 - -#### 国内推荐:腾讯云镜像下载 - -Google 官方源在国内经常断连导致 zip 损坏。**强烈推荐用腾讯云镜像**: - -```bash -SDK="$HOME/AppData/Local/Android/Sdk" -TMP="/tmp" - -# 从腾讯云镜像下载(55MB/s 稳定,约 13 秒) -curl -L -o "$TMP/ndk.zip" \ - "https://mirrors.cloud.tencent.com/AndroidSDK/android-ndk-r27b-windows.zip" - -# 解压 -mkdir -p "$TMP/ndk-extract" -unzip -q "$TMP/ndk.zip" -d "$TMP/ndk-extract" - -# 安装到标准位置 -mv "$TMP/ndk-extract/android-ndk-r27b" "$SDK/ndk/27.1.12297006" -``` - -#### expo-sqlite 的 NDK 版本冲突 - -`expo-sqlite` 可能要求 NDK `27.0.12077973`(不同于 RN 要求的 `27.1.12297006`)。 -解决方式:用 Windows 目录联接(junction)让两个版本指向同一份 NDK: +请在项目的根目录(若已在 `android/` 目录中则不需要前缀 `cd android`)执行以下指令: +### 2.1 本地测试/实体分发(仅编译 arm64-v8a,最快最推荐) +直接运行默认编译,会使用 `gradle.properties` 中配置 of `arm64-v8a`: ```powershell -# 在 PowerShell 中执行 -cmd /c mklink /J ` - "C:\Users\<用户>\AppData\Local\Android\Sdk\ndk\27.0.12077973" ` - "C:\Users\<用户>\AppData\Local\Android\Sdk\ndk\27.1.12297006" -``` - -> 两个版本号 ABI/API 兼容,联接不会导致编译问题。 - -#### NDK 完整性验证 - -NDK 解压后必须验证 arm64 目标库完整,否则 CMake 会报 "clang broken": - -```bash -SDK="$HOME/AppData/Local/Android/Sdk" -NDK="$SDK/ndk/27.1.12297006" - -# 关键文件必须存在 -ls "$NDK/sysroot/usr/lib/aarch64-linux-android/24/crtbegin_dynamic.o" \ - "$NDK/sysroot/usr/lib/aarch64-linux-android/libc.a" \ - "$NDK/toolchains/llvm/prebuilt/windows-x86_64/lib/clang/18/lib/linux/libclang_rt.builtins-aarch64-android.a" -``` - -> ⚠️ 如果 C 盘空间不足(NDK 解压后约 2.3GB),解压会中途截断,导致 `libc.a` 等文件缺失。确保至少有 **5GB 可用空间**。 - ---- - -## 3. ONNX 模型获取 - -本项目使用 PP-OCRv5 + ONNX Runtime(替代 NCNN,见 plan.md 决策 7)。 - -### 下载社区 ONNX 模型 - -```bash -cd plugins/ppocr/assets -mkdir -p . && cd . - -# det 模型(4.8 MB) -curl -L -o ppocrv5_det.onnx \ - "https://huggingface.co/ilaylow/PP_OCRv5_mobile_onnx/resolve/main/ppocrv5_det.onnx" - -# rec 模型(16.6 MB) -curl -L -o ppocrv5_rec.onnx \ - "https://huggingface.co/ilaylow/PP_OCRv5_mobile_onnx/resolve/main/ppocrv5_rec.onnx" - -# CJK 字典(26 KB,PaddleOCR 标准字典) -curl -L -o ppocr_keys_v1.txt \ - "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/release/2.6/ppocr/utils/ppocr_keys_v1.txt" -``` - -### 验证模型 I/O 规格 - -```python -import onnx -for name in ['det', 'rec']: - m = onnx.load(f'plugins/ppocr/assets/ppocrv5_{name}.onnx') - print(f'{name}:') - for i in m.graph.input: - shape = [d.dim_value or d.dim_param for d in i.type.tensor_type.shape.dim] - print(f' input {i.name}: {shape}') - for o in m.graph.output: - shape = [d.dim_value or d.dim_param for d in o.type.tensor_type.shape.dim] - print(f' output {o.name}: {shape}') -``` - -预期输出: -- det: 输入 `x: [N, 3, H, W]`(动态),输出 `[N, 1, H, W]` -- rec: 输入 `x: [N, 3, 48, W]`(高固定 48),输出 `[N, T, 18385]` - ---- - -## 4. expo prebuild - -`expo prebuild` 把 Config Plugin 注入到原生工程(生成 `android/` 目录)。 - -```bash -export JAVA_HOME="C:/Program Files/Java/jdk-21" -export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" - -npx expo prebuild --platform android --no-install -``` - -### 验证注入结果 - -```bash -# Kotlin 源码(应有 9 个 .kt 文件) -find android/app/src/main/java/com/beancount -name "*.kt" | sort - -# ONNX 模型 + 字典 -ls android/app/src/main/assets/ - -# MainApplication 里 OcrPackage 注册 -grep "OcrPackage" android/app/src/main/java/com/example/beanmobile/MainApplication.kt - -# onnxruntime gradle 依赖 -grep "onnxruntime" android/app/build.gradle - -# AndroidManifest 服务注册 -grep "Billing" android/app/src/main/AndroidManifest.xml -``` - -### android/ 目录锁定问题(Windows + ZCode) - -如果 `android/` 目录被进程锁定导致 prebuild 无法 `rmdir`,在临时副本项目中 prebuild 再 robocopy 回来: - -```bash -# 1. 创建临时副本 -TMP="/tmp/bm-prebuild" -mkdir -p "$TMP" -cp app.json package.json package-lock.json tsconfig.json "$TMP/" -cp -r app/ src/ plugins/ "$TMP/" - -# node_modules 用 junction 共享 -powershell -NoProfile -Command "cmd /c mklink /J '$TMP/node_modules' '$(pwd)/node_modules'" - -# 2. 在临时项目 prebuild -cd "$TMP" -npx expo prebuild --platform android --no-install --clean - -# 3. robocopy 回原项目(能处理锁定) -powershell -NoProfile -Command "robocopy '$TMP/android' '$(pwd -W || pwd)/android' /E /PURGE" -``` - ---- - -## 5. Gradle 编译 - -### 前提:onnxruntime 版本修正 - -Config Plugin 注入的 `app/build.gradle` 中 onnxruntime 版本可能不匹配,每次 prebuild 后需检查: - -```bash -# 确认是 1.20.0(不是 1.20.1,后者不存在) -grep "onnxruntime" android/app/build.gradle -# 如果是 1.20.1,修正: -sed -i 's/onnxruntime-android:1.20.1/onnxruntime-android:1.20.0/' android/app/build.gradle -``` - -### 编译 Kotlin(快速验证,不打包) - -```bash +# 在 Windows Powershell 下执行: +$env:JAVA_HOME="C:\Program Files\Java\jdk-21" +$env:ANDROID_HOME="C:\Users\fmq\AppData\Local\Android\Sdk" cd android -export JAVA_HOME="C:/Program Files/Java/jdk-21" -export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" - -./gradlew :app:compileDebugKotlin --no-daemon -``` - -### 完整打包 APK - -```bash -./gradlew :app:assembleDebug --no-daemon -``` - -APK 输出位置:`android/app/build/outputs/apk/debug/app-debug.apk`(~250MB) - -### Gradle 缓存清理(遇到诡异问题时) - -```bash -# 删 build 缓存 -rm -rf app/build build app/.cxx - -# 彻底重来(连 Gradle daemon 也杀) -powershell -NoProfile -Command "Get-Process -Name 'java' -ErrorAction SilentlyContinue | Stop-Process -Force" +.\gradlew.bat :app:assembleRelease --offline --no-daemon ``` +编译完成后,可在以下路径找到适合真机安装的轻量版 APK(约 37MB): +* `android\app\build\outputs\apk\release\app-arm64-v8a-release.apk` --- -## 6. 安装到真机 - -### 开启 USB 调试 - -1. 手机「设置 → 关于手机」→ 连续点击「MIUI 版本」7 次 → 开启开发者选项 -2. 「设置 → 更多设置 → 开发者选项」→ 打开: - - ✅ USB 调试 - - ✅ **USB 安装**(红米/小米必须开,否则 `adb install` 被拒为 `INSTALL_FAILED_USER_RESTRICTED`) -3. USB 连接电脑(文件传输模式) - -### adb install - -```bash -export ANDROID_HOME="C:/Users/<用户>/AppData/Local/Android/Sdk" -ADB="$ANDROID_HOME/platform-tools/adb" - -# 确认设备连接 -"$ADB" devices -# 应显示 device 状态 - -# 安装 -"$ADB" install -r android/app/build/outputs/apk/debug/app-debug.apk -``` - ---- - -## 7. Metro + 运行 - -Debug APK 需要从 Metro server 加载 JS bundle(不内嵌 JS)。 - -### 启动 Metro - -```bash -npx expo start --dev-client --port 8081 -``` - -### adb reverse(USB 连接,手机和电脑不在同一 WiFi 时必需) - -```bash -"$ADB" reverse tcp:8081 tcp:8081 -``` - -### 启动 app - -```bash -# 命令行启动 -"$ADB" shell am start -n com.example.beanmobile/.MainActivity - -# 或直接在手机桌面点击「Bean Mobile」图标 -``` - -### 验证 - -```bash -# 进程存活? -"$ADB" shell pidof com.example.beanmobile - -# JS 日志 -"$ADB" logcat -d | grep "ReactNativeJS" - -# 截图 -"$ADB" shell screencap -p /sdcard/screen.png -"$ADB" pull /sdcard/screen.png /tmp/app-screen.png -``` - ---- - -## 8. 常见问题与解决方案 - -### `withAndroidMainApplication is not a function` - -**原因**:SDK 54 的 `@expo/config-plugins` 没有 `withAndroidMainApplication` / `withAndroidGradle`。 - -**解决**:改用 `withMainApplication` + `withAppBuildGradle` + `withDangerousMod`。 - -### `Cannot read properties of undefined (reading '0')` in AndroidManifest - -**原因**:`manifest.application` 在 prebuild 初始模板里可能不存在。 - -**解决**:操作前加防御性检查: -```javascript -if (!Array.isArray(manifest.application) || manifest.application.length === 0) { - manifest.application = [{ $: {} }]; -} -``` - -### `modResults` 结构搞错导致 manifest 重复 `` 标签 - -**原因**:`withAndroidManifest` 的 `modResults` 结构是 `{ manifest: { ... } }`,不是直接就是 manifest。 - -**解决**:所有权限和 application 操作通过 `modConfig.modResults.manifest`,不是 `modConfig.modResults` 本身。 - -### MainApplication 里 import OcrPackage 没注入 - -**原因**:Kotlin 的 package 声明**没有分号**(`package com.example.beanmobile`,不是 `package com.example.beanmobile;`),正则 `^(package [\w.]+;)` 匹配不到。 - -**解决**:正则去掉分号要求:`^(package\s+[\w.]+;?\s*)$`。 - -### Config Plugin 返回 undefined 导致整个 config 丢失 - -**原因**:插件函数最后漏了 `return config;`,导致 `withAndroidManifest` 的结果没返回,config 变成 undefined,后续所有插件崩溃。 - -**解决**:每个插件函数末尾必须有 `return config;`。 - -### `NDK at ...27.0.12077973 did not have a source.properties file` - -**原因**:expo-sqlite 要求 NDK 27.0,但只装了 27.1;Gradle 自动下载 27.0 失败留下空目录。 - -**解决**:用 `mklink /J` 创建目录联接(见第 2 节)。 - -### NDK `clang broken` / `cannot open crtbegin_dynamic.o` - -**原因**:NDK 解压时磁盘空间不足,arm64 目标库被截断。 - -**解决**:确保至少 5GB 可用空间,重新从腾讯云镜像下载并解压。 - -### `onnxruntime-android:1.20.1` not found - -**原因**:Maven Central 上最新稳定版是 `1.20.0`,`1.20.1` 不存在。 - -**解决**:`sed -i 's/1.20.1/1.20.0/' android/app/build.gradle` - -### `NoSuchMethodError: getDirectConverter` (expo-font 崩溃) - -**原因**:`@expo/vector-icons@15.x` 拉进了 `expo-font@57.0.0`(SDK 55 版本),与 `expo-modules-core@3.0.x`(SDK 54)不兼容。 - -**解决**:在 `package.json` 加 npm override 强制降级: -```json -"overrides": { - "expo-font": "~14.0.12" -} -``` - -### `Unable to resolve "expo-linking"` - -**原因**:expo-router 的 peer dependencies 没有全部安装。 - -**解决**:补全所有 peer deps: -```bash -npm install --legacy-peer-deps \ - @expo/metro-runtime expo-constants expo-linking \ - react-dom react-native-gesture-handler react-native-reanimated \ - @react-navigation/drawer -``` - -### `INSTALL_FAILED_USER_RESTRICTED` (红米/小米) - -**原因**:MIUI 的「USB 安装」开关默认关闭。 - -**解决**:开发者选项里打开「USB 安装」。 - -### `EBUSY: resource busy or locked, rmdir android/` - -**原因**:ZCode 的子进程(NodeBabyLinkService)持有 `android/` 目录句柄,`rmdir` 失败。 - -**解决**:在临时副本项目里 prebuild,再用 `robocopy /E /PURGE` 复制回原项目(见第 4 节)。 - -### `attribute android:canTakeScreenshots not found` - -**原因**:`canTakeScreenshots` 是 API 30+ 属性,部分 compileSdk 配置下 AAPT 不识别。 - -**解决**:从 `accessibility_service_config.xml` 删除该属性(非必需,截图功能在 Kotlin 代码里实现)。 - ---- - -## 9. 已修复的 Bug 清单 - -以下是真机编译过程中暴露并修复的全部 bug(共 26 个): - -### Config Plugin Bug(7 个) - -| 文件 | Bug | 修复 | -|------|-----|------| -| `plugins/ppocr/app.plugin.js` | `withAndroidMainApplication` 不存在 | 改用 `withMainApplication` + `withAppBuildGradle` + `withDangerousMod` | -| `plugins/ppocr/app.plugin.js` | `withDangerousMod` 回调里 `modConfig.platformProjectRoot` undefined | 改为 `modConfig.modRequest.platformProjectRoot` | -| `plugins/*/app.plugin.js`(3 个) | `manifest.application[0]` 崩溃(prebuild 初始 manifest 无 application 节点) | 加 `Array.isArray` 防御 | -| `plugins/ppocr/app.plugin.js` | MainApplication import 没注入(Kotlin package 无分号) | 正则去掉分号要求 | -| `plugins/accessibility/app.plugin.js` | `return config` 丢失导致整个 config 变 undefined | 补回 `return config` | - -### Kotlin 编译 Bug(11 个) - -| 文件 | Bug | 修复 | -|------|-----|------| -| `FloatingTip.kt:80` | `val layoutParams` 不可重赋值 + 类型不匹配 | 拆出 apply 块 + 用 LinearLayout.LayoutParams | -| `OcrTileService.kt:50` | `startActivityAndCollapse(intentSender)` 参数不匹配 | 改传 PendingIntent | -| `OcrModule.kt:179` | `putDouble(confidence)` Float 不匹配 | `.toDouble()` | -| `OcrModule.kt:215,235` | `longArrayOf(1,3,...)` Int vs Long | 显式 `.toLong()` + `L` 后缀 | -| `OcrModule.kt:221,242` | `detOutputs.forEach{it.close()}` 未解析 | 改用 `detOutputs.close()`(OrtSession.Result.close) | -| `OcrModule.kt:333` | det 输出维度多了 channel 维,`prob[y][x]` 是 FloatArray 不是 Float | `[0][0]` 剥掉 N+C 两维,函数签名改 `Array` | - -### 资源/环境 Bug(8 个) - -| 问题 | 修复 | -|------|------| -| cmdline-tools 缺失 | 从 Google 官方下载安装 | -| platform 35 缺失 | sdkmanager 安装 | -| NDK 27.0 vs 27.1 版本冲突 | `mklink /J` 目录联接 | -| NDK 下载损坏(Google 源断连) | 腾讯云镜像替代 | -| `accessibility_service_config.xml` canTakeScreenshots 属性 | 删除(非必需) | -| `onnxruntime:1.20.1` 不存在 | 改用 1.20.0 | -| expo-font 57.0 vs 14.0 版本冲突 | npm overrides 强制降级 | -| expo-router peer deps 缺失 | 补全 8 个依赖包 | - ---- - -## 附录:完整一键流程 - -```bash -# === 环境变量(每次新终端都要设) === -export JAVA_HOME="C:/Program Files/Java/jdk-21" -export ANDROID_HOME="C:/Users/$USER/AppData/Local/Android/Sdk" - -# === 1. 依赖 === -npm install --legacy-peer-deps - -# === 2. prebuild === -npx expo prebuild --platform android --no-install -sed -i 's/onnxruntime-android:1.20.1/onnxruntime-android:1.20.0/' android/app/build.gradle - -# === 3. 编译 === +### 2.2 全量分包发布(适合多设备兼容测试/全网发布) +若需要同时生成适配所有手机架构的独立 APK 以及一个通用包,可以在命令行中通过参数**覆盖默认架构**: +```powershell +$env:JAVA_HOME="C:\Program Files\Java\jdk-21" +$env:ANDROID_HOME="C:\Users\fmq\AppData\Local\Android\Sdk" cd android -./gradlew :app:assembleDebug --no-daemon - -# === 4. 安装 === -cd .. -"$ANDROID_HOME/platform-tools/adb" install -r android/app/build/outputs/apk/debug/app-debug.apk - -# === 5. Metro + 运行 === -npx expo start --dev-client --port 8081 & -sleep 10 -"$ANDROID_HOME/platform-tools/adb" reverse tcp:8081 tcp:8081 -"$ANDROID_HOME/platform-tools/adb" shell am start -n com.example.beanmobile/.MainActivity +.\gradlew.bat :app:assembleRelease -PreactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 --offline --no-daemon ``` +编译完成后,在输出目录会产生 5 个文件: +1. `app-arm64-v8a-release.apk` (推荐绝大多数真机,约 37MB) +2. `app-armeabi-v7a-release.apk` (适合极少数老旧真机) +3. `app-x86-release.apk` (适合 32 位模拟器) +4. `app-x86_64-release.apk` (适合 64 位模拟器) +5. `app-universal-release.apk` (包含上述全部架构的通用胖包,约 140MB) + +--- + +### 2.3 模拟器专用打包 (x86_64) +若需要直接打包在 Windows/macOS 的 x86_64 原生安卓模拟器上测试: +```powershell +.\gradlew.bat :app:assembleRelease -PreactNativeArchitectures=x86_64 --offline --no-daemon +``` +即可秒级编译出专门针对模拟器的 `app-x86_64-release.apk`。 + +--- + +## 3. 高级优化项:Proguard/R8 与混淆 (选填) +如需进一步将独立包体积压缩到 25MB 左右,可以考虑在 `gradle.properties` 中开启混淆并做裁剪防御。 +1. 在 `gradle.properties` 中添加: + ```properties + android.enableMinifyInReleaseBuilds=true + android.enableShrinkResourcesInReleaseBuilds=true + ``` +2. 注意:由于引入了 `ONNX Runtime` 动态调用,如果运行崩溃,需要在 `android/app/proguard-rules.pro` 中加入如下混淆保留白名单: + ```proguard + -keep class com.microsoft.onnxruntime.** { *; } + -dontwarn com.microsoft.onnxruntime.** + ``` diff --git a/package-lock.json b/package-lock.json index 65f2452..f11b910 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "beancount-mobile", "version": "0.1.0", "dependencies": { + "@expo-google-fonts/caveat": "^0.4.2", + "@expo-google-fonts/quicksand": "^0.4.1", "@expo/metro-runtime": "^6.1.2", "@expo/vector-icons": "^15.0.3", "@react-navigation/drawer": "^7.5.0", @@ -33,6 +35,7 @@ "react-native-reanimated": "^3.18.0", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", + "react-native-svg": "^15.15.5", "react-native-view-shot": "4.0.3", "text-encoding-gbk": "^0.7.3", "xlsx": "^0.18.5", @@ -2014,6 +2017,18 @@ "node": ">=18" } }, + "node_modules/@expo-google-fonts/caveat": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/@expo-google-fonts/caveat/-/caveat-0.4.2.tgz", + "integrity": "sha512-kCG35tRUX8Wi+u0Jh5ck3K1u7vZkdLnwRTuKMrEnlVmikvxQFXN1KjF5cL2vqRG7Hwm7648Zl8xe1BWAoIsPCA==", + "license": "MIT AND OFL-1.1" + }, + "node_modules/@expo-google-fonts/quicksand": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@expo-google-fonts/quicksand/-/quicksand-0.4.1.tgz", + "integrity": "sha512-LpGdmY+CDm/GTv29qDPy8Nz1a3idWO1R6Bb+ty7flS6vfHXsdOQTRKNz93SFHdXQj0FSvVhvMLe3wTtMgf312A==", + "license": "MIT AND OFL-1.1" + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmmirror.com/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -5097,6 +5112,12 @@ "integrity": "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -5708,6 +5729,56 @@ "utrie": "^1.0.2" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", @@ -5857,6 +5928,61 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.4.7.tgz", @@ -5925,6 +6051,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmmirror.com/env-editor/-/env-editor-0.4.2.tgz", @@ -8992,6 +9130,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-5.2.1.tgz", @@ -9567,6 +9711,18 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/nullthrows/-/nullthrows-1.1.1.tgz", @@ -10345,6 +10501,20 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.15.5", + "resolved": "https://registry.npmmirror.com/react-native-svg/-/react-native-svg-15.15.5.tgz", + "integrity": "sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-view-shot": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/react-native-view-shot/-/react-native-view-shot-4.0.3.tgz", diff --git a/package.json b/package.json index 0ed33c6..38f9a55 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@expo-google-fonts/caveat": "^0.4.2", + "@expo-google-fonts/quicksand": "^0.4.1", "@expo/metro-runtime": "^6.1.2", "@expo/vector-icons": "^15.0.3", "@react-navigation/drawer": "^7.5.0", @@ -36,6 +38,7 @@ "react-native-reanimated": "^3.18.0", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", + "react-native-svg": "^15.15.5", "react-native-view-shot": "4.0.3", "text-encoding-gbk": "^0.7.3", "xlsx": "^0.18.5", diff --git a/plugins/accessibility/android/AccessibilityBridgeModule.kt b/plugins/accessibility/android/AccessibilityBridgeModule.kt new file mode 100644 index 0000000..4662bb4 --- /dev/null +++ b/plugins/accessibility/android/AccessibilityBridgeModule.kt @@ -0,0 +1,233 @@ +package com.beancount.mobile.accessibility + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.WritableNativeArray +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.bridge.ReadableArray + +/** + * 无障碍服务 RN 桥接模块(plan.md「3.6 无障碍服务」JS 接线)。 + * + * BillingAccessibilityService 是 AccessibilityService 子类(非 RN 模块), + * 其方法无法直接从 JS 调用。本模块作为中间层,通过 instance 静态引用 + * 把 JS 调用委托给服务实例。 + * + * 由 Config Plugin 的 withMainApplication 注入 add(AccessibilityBridgePackage())。 + */ +class AccessibilityBridgeModule(private val reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + + init { + ReactContextHolder.context = reactContext + } + + override fun invalidate() { + super.invalidate() + if (ReactContextHolder.context === reactContext) { + ReactContextHolder.context = null + } + } + + override fun getName() = "AccessibilityBridge" + + /** 无障碍服务是否已连接(用户已在系统设置中启用)。 */ + @ReactMethod + fun isServiceRunning(promise: Promise) { + promise.resolve(BillingAccessibilityService.instance != null) + } + + /** + * 记住当前页面:把当前顶部 App 的 pkg|activity 加入白名单, + * 之后该页面内容变化时自动截图 → OCR。 + */ + @ReactMethod + fun rememberCurrentPage(promise: Promise) { + val service = BillingAccessibilityService.instance + if (service == null) { + promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") + return + } + val pkg = service.getTopPackage() + if (pkg == null) { + promise.reject("NO_TOP_PACKAGE", "当前没有检测到前台 App") + return + } + service.rememberCurrentPage() + val result = WritableNativeMap() + result.putString("package", pkg) + result.putString("activity", service.getTopActivity() ?: "") + result.putString("signature", "$pkg|${service.getTopActivity() ?: ""}") + promise.resolve(result) + } + + /** 手动触发一次 OCR(截取当前屏幕并发送给 JS 层处理)。 */ + @ReactMethod + fun triggerManualOcr(promise: Promise) { + val service = BillingAccessibilityService.instance + if (service == null) { + promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") + return + } + try { + service.triggerManualOcr() + promise.resolve(true) + } catch (e: Exception) { + promise.reject("OCR_TRIGGER_FAIL", e.message) + } + } + + /** 获取所有已记住的页面签名列表。 */ + @ReactMethod + fun getPageSignatures(promise: Promise) { + val service = BillingAccessibilityService.instance + val sigsSet = if (service != null) { + service.getPageSignatures() + } else { + try { + val prefs = reactContext.getSharedPreferences("billing_accessibility_prefs", android.content.Context.MODE_PRIVATE) + prefs.getStringSet("page_signatures", emptySet()) ?: emptySet() + } catch (e: Exception) { + emptySet() + } + } + val arr = WritableNativeArray() + for (sig in sigsSet) { + val parts = sig.split("|", limit = 2) + val map = WritableNativeMap() + map.putString("signature", sig) + map.putString("package", parts.getOrNull(0) ?: "") + map.putString("activity", parts.getOrNull(1) ?: "") + arr.pushMap(map) + } + promise.resolve(arr) + } + + /** 清空所有已记住的页面签名。 */ + @ReactMethod + fun clearPageSignatures(promise: Promise) { + val service = BillingAccessibilityService.instance + if (service != null) { + service.clearPageSignatures() + } else { + try { + val prefs = reactContext.getSharedPreferences("billing_accessibility_prefs", android.content.Context.MODE_PRIVATE) + prefs.edit().putStringSet("page_signatures", emptySet()).apply() + } catch (e: Exception) { + promise.reject("CLEAR_PREFS_FAIL", e.message) + return + } + } + promise.resolve(true) + } + + /** 删除指定页面签名。 */ + @ReactMethod + fun removePageSignature(signature: String, promise: Promise) { + val service = BillingAccessibilityService.instance + if (service != null) { + service.removePageSignature(signature) + } else { + try { + val prefs = reactContext.getSharedPreferences("billing_accessibility_prefs", android.content.Context.MODE_PRIVATE) + val saved = prefs.getStringSet("page_signatures", emptySet()) ?: emptySet() + val mutable = HashSet(saved) + if (mutable.remove(signature)) { + prefs.edit().putStringSet("page_signatures", mutable).apply() + } + } catch (e: Exception) { + promise.reject("REMOVE_PREFS_FAIL", e.message) + return + } + } + promise.resolve(true) + } + + /** 获取支付 App 白名单(供 JS 端展示)。 */ + @ReactMethod + fun getPaymentPackages(promise: Promise) { + val arr = WritableNativeArray() + for (pkg in BillingAccessibilityService.PAYMENT_PACKAGES) { + arr.pushString(pkg) + } + promise.resolve(arr) + } + + /** 获取当前顶部 App 信息(供 JS 判断用户是否在支付页面)。 */ + @ReactMethod + fun getTopApp(promise: Promise) { + val service = BillingAccessibilityService.instance + if (service == null) { + promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") + return + } + val map = WritableNativeMap() + map.putString("package", service.getTopPackage() ?: "") + map.putString("activity", service.getTopActivity() ?: "") + promise.resolve(map) + } + + /** 将当前应用拉起至前台,用以在后台识别出账单后,弹窗让用户进行交易确认 */ + @ReactMethod + fun bringAppToForeground(promise: Promise) { + val service = BillingAccessibilityService.instance + if (service == null) { + promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") + return + } + try { + service.bringAppToForeground() + promise.resolve(true) + } catch (e: Exception) { + promise.reject("FAIL", e.message) + } + } + + /** 显示账单浮窗(直接在当前其他应用上方渲染,不返回 App 内) */ + @ReactMethod + fun showFloatingBill( + amount: String, + merchant: String, + time: String, + packageName: String, + categories: ReadableArray, + accounts: ReadableArray, + direction: String, + draftId: String, + promise: Promise + ) { + val context = reactContext.currentActivity ?: BillingAccessibilityService.instance + if (context == null) { + promise.reject("NO_CONTEXT", "无法获取当前前台 Activity 或 AccessibilityService 实例") + return + } + + val categoryList = mutableListOf>() + for (i in 0 until categories.size()) { + val map = categories.getMap(i) + categoryList.add(mapOf( + "id" to (map?.getString("id") ?: ""), + "name" to (map?.getString("name") ?: ""), + "account" to (map?.getString("account") ?: ""), + "type" to (map?.getString("type") ?: "") + )) + } + + val accountList = mutableListOf() + for (i in 0 until accounts.size()) { + accountList.add(accounts.getString(i) ?: "") + } + + android.os.Handler(android.os.Looper.getMainLooper()).post { + try { + val floatingView = FloatingBillView(context, draftId, amount, merchant, time, packageName, categoryList, accountList, direction) + floatingView.show() + promise.resolve(true) + } catch (e: Exception) { + promise.reject("FAIL", e.message) + } + } + } +} diff --git a/plugins/accessibility/android/AccessibilityBridgePackage.kt b/plugins/accessibility/android/AccessibilityBridgePackage.kt new file mode 100644 index 0000000..caae6b5 --- /dev/null +++ b/plugins/accessibility/android/AccessibilityBridgePackage.kt @@ -0,0 +1,20 @@ +package com.beancount.mobile.accessibility + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +/** + * ReactPackage 注册 AccessibilityBridgeModule。 + * 由 Config Plugin 的 withMainApplication 注入 add(AccessibilityBridgePackage())。 + */ +class AccessibilityBridgePackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(AccessibilityBridgeModule(reactContext)) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} diff --git a/plugins/accessibility/android/BillingAccessibilityService.kt b/plugins/accessibility/android/BillingAccessibilityService.kt new file mode 100644 index 0000000..543d56c --- /dev/null +++ b/plugins/accessibility/android/BillingAccessibilityService.kt @@ -0,0 +1,578 @@ +package com.beancount.mobile.accessibility + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.AccessibilityServiceInfo +import android.content.Context +import android.graphics.Bitmap +import android.hardware.display.DisplayManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.Display +import android.view.Surface +import android.view.accessibility.AccessibilityEvent +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.bridge.WritableNativeArray +import java.io.ByteArrayOutputStream +import android.util.Base64 +import android.view.accessibility.AccessibilityNodeInfo + +/** + * 无障碍账单识别服务(plan.md「3.6 无障碍服务」+「决策 4 Config Plugin」)。 + * + * 参考 AutoAccounting 的 SelectToSpeakService: + * - 监听支付 App 的页面切换(TYPE_WINDOW_STATE_CHANGED) + * - 页面签名匹配时自动截图 → OCR → 推送到 JS 层 + * - 横屏免打扰(游戏/视频时不触发) + * - ocrDoing 守卫(防止重复触发) + * + * 伪装说明:plan.md 决策 3「纯开源侧载」保留无障碍伪装(非应用商店分发)。 + * 注意:本服务类名在 manifest 中声明为 BillingAccessibilityService, + * 伪装为系统服务(包名 com.beancount.mobile.accessibility)仅在侧载版本保留。 + * + * 通过 DeviceEventEmitter 把识别事件推送到 JS 层 automationStore。 + * + * ⚠️ 与 src/domain/constants.ts 同步:PAYMENT_PACKAGES + * 修改时需两边同时更新。 + */ +class BillingAccessibilityService : AccessibilityService() { + + companion object { + private const val TAG = "BillingAccessibility" + private const val PREFS_NAME = "billing_accessibility_prefs" + private const val PREF_PAGE_SIGNATURES = "page_signatures" + + @Volatile + var instance: BillingAccessibilityService? = null + private set + + /** 支付 App 白名单。 */ + val PAYMENT_PACKAGES = setOf( + "com.eg.android.AlipayGphone", // 支付宝 + "com.tencent.mm", // 微信 + "com.unionpay", // 银联 + "com.cmbchina", // 招商银行 + "com.icbc", // 工商银行 + "com.chinamworld.main", // 中国银行 + "com.ccbrcb", // 建设银行 + "com.bankcomm.Bankcomm", // 交通银行 + "com.tencent.mobileqq", // 手机QQ + "com.tencent.tim" // TIM + ) + + /** 厂商桌面包名(过滤,不触发 OCR)。 */ + private val LAUNCHER_PACKAGES = setOf( + "com.google.android.apps.nexuslauncher", + "com.sec.android.app.launcher", + "com.miui.home", + "com.huawei.android.launcher", + "com.oppo.launcher", + "com.bbk.launcher2", + "com.android.launcher3", + ) + + /** OCR 触发防抖:500ms 内的内容变化合并为一次。 */ + private const val CONTENT_CHANGE_DEBOUNCE_MS = 500L + } + + private var ocrDoing = false + private val handler = Handler(Looper.getMainLooper()) + private val debounceRunnable = Runnable { processContentChange() } + @Volatile private var topPackage: String? = null + @Volatile private var topActivity: String? = null + /** 已记住的页面签名(pkg|activity),匹配时自动触发 OCR。持久化到 SharedPreferences。 */ + private val pageSignatures = java.util.concurrent.CopyOnWriteArraySet() + private var floatingHelper: FloatingHelper? = null + + override fun onServiceConnected() { + super.onServiceConnected() + instance = this + Log.i(TAG, "无障碍账单识别服务已连接") + loadPageSignatures() + configureService() + } + + /** 动态配置服务能力(截图 + 页面变化监听)。 */ + private fun configureService() { + val info = AccessibilityServiceInfo().apply { + eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or + AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED + feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC + flags = AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY or + AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or + AccessibilityServiceInfo.DEFAULT + notificationTimeout = 100L + } + serviceInfo = info + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + if (ocrDoing) return // 处理中,跳过 + val eventPackage = event?.packageName?.toString() ?: return + + when (event.eventType) { + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> { + val activityName = event.className?.toString() ?: "" + if (filterPackage(eventPackage, activityName)) return + topPackage = eventPackage + topActivity = activityName + Log.d(TAG, "页面切换: $eventPackage / $activityName") + + // 更新悬浮窗助手状态 + updateFloatingHelperVisibility(eventPackage) + + if (PAYMENT_PACKAGES.contains(eventPackage)) { + scheduleContentChange() + } + + // 调试:如果是微信或支付宝,延迟 800ms 抓取并打印全屏无障碍文本内容 + if (eventPackage == "com.tencent.mm" || eventPackage == "com.eg.android.AlipayGphone") { + handler.postDelayed({ + val rootNode = rootInActiveWindow + val texts = mutableListOf() + dumpNodeTexts(rootNode, texts) + rootNode?.recycle() + + val reactContext = ReactContextHolder.context + if (reactContext != null) { + try { + val map = WritableNativeMap().apply { + putString("package", eventPackage) + putString("activity", activityName) + val array = WritableNativeArray() + for (t in texts) { + array.pushString(t) + } + putArray("texts", array) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingDebugNodes", map) + } catch (e: Exception) { + // 忽略 + } + } + }, 800) + } + } + AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> { + if (PAYMENT_PACKAGES.contains(eventPackage)) { + scheduleContentChange() + } + } + } + } + + private fun updateFloatingHelperVisibility(pkg: String?) { + handler.post { + if (pkg != null && PAYMENT_PACKAGES.contains(pkg)) { + if (floatingHelper == null) { + floatingHelper = FloatingHelper(this) + floatingHelper?.show() + } + } else { + floatingHelper?.dismiss() + floatingHelper = null + } + } + } + + /** 防抖:500ms 内的多次内容变化合并。 */ + private fun scheduleContentChange() { + handler.removeCallbacks(debounceRunnable) + handler.postDelayed(debounceRunnable, CONTENT_CHANGE_DEBOUNCE_MS) + } + + /** 内容变化处理:检查页面签名 → 提取文本 → 发送给 JS(JS 控制是否 OCR 兜底)。 */ + private fun processContentChange() { + if (ocrDoing) return + val pkg = topPackage ?: return + + // 横屏免打扰(plan.md「3.10」) + if (isLandscape()) { + Log.d(TAG, "横屏免打扰,跳过") + return + } + + // 页面签名匹配(若已记住页面则触发) + val activity = topActivity ?: "" + val sigKey = "$pkg|$activity" + if (!pageSignatures.contains(sigKey)) { + return // 未记住的页面不自动触发 + } + + // 抓取并提取屏幕所有无障碍文本,并推送至 JS 侧进行解析/控制 + val rootNode = rootInActiveWindow + val texts = mutableListOf() + dumpNodeTexts(rootNode, texts) + rootNode?.recycle() + + val reactContext = ReactContextHolder.context + if (reactContext != null) { + try { + val map = WritableNativeMap().apply { + putString("package", pkg) + putString("activity", activity) + putString("signature", sigKey) + val array = WritableNativeArray() + for (t in texts) { + array.pushString(t) + } + putArray("texts", array) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingDebugNodes", map) + } catch (e: Exception) { + Log.e(TAG, "推送内容变化节点文本失败: ${e.message}") + } + } + } + + /** 截图并触发 OCR 处理(Android 11+)。 */ + private fun takeScreenshotAndProcess(packageName: String) { + if (ocrDoing) return + ocrDoing = true + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + ocrDoing = false + return + } + try { + takeScreenshot( + Display.DEFAULT_DISPLAY, + mainExecutor, + object : TakeScreenshotCallback { + override fun onSuccess(result: ScreenshotResult) { + try { + val bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, result.colorSpace) + result.hardwareBuffer.close() + if (bitmap != null) { + val base64 = bitmapToBase64(bitmap) + bitmap.recycle() + // 推送到 JS 层(NativeEventEmitter) + sendScreenshotEvent(base64, packageName) + } + } finally { + ocrDoing = false + } + } + override fun onFailure(errorCode: Int) { + Log.e(TAG, "截图失败: errorCode=$errorCode") + ocrDoing = false + } + } + ) + } catch (e: Throwable) { + Log.e(TAG, "调用 takeScreenshot 失败: ${e.message}", e) + ocrDoing = false + } + } + + /** 把截图以 base64 推送到 JS 层(由 JS 端 OcrProcessor 处理)。 */ + private fun sendScreenshotEvent(base64: String, packageName: String) { + val reactContext = ReactContextHolder.context ?: return + try { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingScreenshot", writableMapOf( + "base64" to base64, + "packageName" to packageName, + "timestamp" to System.currentTimeMillis() + )) + } catch (e: Exception) { + Log.e(TAG, "推送截图事件失败: ${e.message}") + } + } + + /** 手动触发一次 OCR(临时隐藏悬浮窗避开遮挡,并在 150ms 后触发截图)。 */ + fun triggerManualOcr() { + val pkg = topPackage ?: return + handler.post { + floatingHelper?.collapse() + floatingHelper?.hideTemporarily() + } + + // 150ms 后触发截图(此时悬浮窗已瞬间隐藏,避免遮挡和误匹配) + handler.postDelayed({ + try { + takeScreenshotAndProcess(pkg) + } catch (e: Exception) { + Log.e(TAG, "手动触发 OCR 失败: ${e.message}") + } finally { + // 截图完成,瞬间恢复显示悬浮球 + handler.post { + floatingHelper?.showTemporarily() + } + } + }, 150) + } + + /** 手动触发一次节点文本提取并发送到 JS,从而让 JS 优先尝试直接文本解析。 */ + fun triggerManualExtraction() { + handler.post { + floatingHelper?.collapse() + } + val pkg = topPackage ?: return + val activity = topActivity ?: "" + val sigKey = "$pkg|$activity" + + val rootNode = rootInActiveWindow + val texts = mutableListOf() + dumpNodeTexts(rootNode, texts) + rootNode?.recycle() + + val reactContext = ReactContextHolder.context + if (reactContext != null) { + try { + val map = WritableNativeMap().apply { + putString("package", pkg) + putString("activity", activity) + putString("signature", sigKey) + putBoolean("isManual", true) + val array = WritableNativeArray() + for (t in texts) { + array.pushString(t) + } + putArray("texts", array) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingDebugNodes", map) + } catch (e: Exception) { + Log.e(TAG, "手动触发节点文本提取失败: ${e.message}") + } + } + } + + /** 记住当前页面(用户主动标记应触发 OCR 的页面)。持久化到 SharedPreferences。 */ + fun rememberCurrentPage() { + val pkg = topPackage ?: return + val activity = topActivity ?: "" + val sig = "$pkg|$activity" + if (pageSignatures.add(sig)) { + savePageSignatures() + Log.i(TAG, "已记住页面: $sig") + handler.post { + android.widget.Toast.makeText(this, "已记住页面签名:\n$sig", android.widget.Toast.LENGTH_LONG).show() + } + + // 抓取并提取屏幕所有无障碍文本 + val texts = mutableListOf() + val rootNode = rootInActiveWindow + dumpNodeTexts(rootNode, texts) + rootNode?.recycle() + + // 推送事件到 JS 端,使 npx expo start 终端控制台可以接收并打印日志 + val reactContext = ReactContextHolder.context + if (reactContext != null) { + try { + val map = WritableNativeMap().apply { + putString("package", pkg) + putString("activity", activity) + putString("signature", sig) + val array = WritableNativeArray() + for (t in texts) { + array.pushString(t) + } + putArray("texts", array) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingPageRemembered", map) + } catch (e: Exception) { + Log.e(TAG, "推送记住页面事件失败: ${e.message}") + } + } + } else { + handler.post { + android.widget.Toast.makeText(this, "该页面签名已存在:\n$sig", android.widget.Toast.LENGTH_LONG).show() + } + } + } + + /** 获取已记住的页面签名列表(供 JS 端展示)。 */ + fun getPageSignatures(): Set { + return pageSignatures.toSet() + } + + /** 清空所有已记住的页面签名。 */ + fun clearPageSignatures() { + pageSignatures.clear() + try { + val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit().remove(PREF_PAGE_SIGNATURES).apply() + Log.i(TAG, "已清空全部记住的页面并从 SharedPreferences 中移除键值") + } catch (e: Exception) { + Log.e(TAG, "清空 SharedPreferences 失败: ${e.message}") + } + } + + /** 删除指定页面签名。 */ + fun removePageSignature(sig: String) { + if (pageSignatures.remove(sig)) { + savePageSignatures() + Log.i(TAG, "已删除页面签名: $sig") + } + } + + /** 获取当前顶部包名(供 JS 判断当前页面)。 */ + fun getTopPackage(): String? = topPackage + + /** 获取当前顶部 Activity。 */ + fun getTopActivity(): String? = topActivity + + /** 从 SharedPreferences 加载已记住的页面签名。 */ + private fun loadPageSignatures() { + try { + val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val saved = prefs.getStringSet(PREF_PAGE_SIGNATURES, emptySet()) ?: emptySet() + pageSignatures.clear() + pageSignatures.addAll(saved) + Log.i(TAG, "已加载 ${pageSignatures.size} 个记住的页面签名") + } catch (e: Exception) { + Log.e(TAG, "加载页面签名失败: ${e.message}") + } + } + + /** 保存页面签名到 SharedPreferences。 */ + private fun savePageSignatures() { + try { + val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + if (pageSignatures.isEmpty()) { + prefs.edit().remove(PREF_PAGE_SIGNATURES).apply() + } else { + prefs.edit().putStringSet(PREF_PAGE_SIGNATURES, HashSet(pageSignatures)).apply() + } + } catch (e: Exception) { + Log.e(TAG, "保存页面签名失败: ${e.message}") + } + } + + /** 横屏检测(plan.md「3.10 横屏免打扰」)。 */ + private fun isLandscape(): Boolean { + val dm = getSystemService(DisplayManager::class.java)?.getDisplay(Display.DEFAULT_DISPLAY) + ?: return false + val rotation = dm.rotation + return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270 + } + + /** 过滤系统组件/桌面(不触发 OCR)。 */ + private fun filterPackage(pkg: String, className: String): Boolean { + val p = pkg.lowercase() + // 针对自身应用的特殊过滤:只允许 MainActivity 通过以触发隐藏悬浮窗; + // 其他自身组件(如悬浮球容器 LinearLayout)一律过滤,避免自毁式关闭。 + if (pkg == packageName) { + return className != "com.example.beanmobile.MainActivity" + } + if (p == "android" || p.startsWith("com.android.") || p.startsWith("com.google.android.")) return true + if (p.contains("systemui") || p.contains("settings") || p.contains("inputmethod") || p.contains("keyboard") || p.contains("input")) return true + if (LAUNCHER_PACKAGES.any { p.contains(it) }) return true + return false + } + + /** Bitmap → base64(JPEG 质量 60,参考 AutoAccounting bitmapToBase64)。 */ + private fun bitmapToBase64(bitmap: Bitmap): String { + // 安全起见,如果 bitmap 是 HARDWARE 格式,将其复制为 ARGB_8888 软件格式 + val softwareBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && bitmap.config == Bitmap.Config.HARDWARE) { + bitmap.copy(Bitmap.Config.ARGB_8888, false) + } else { + bitmap + } + val baos = ByteArrayOutputStream() + softwareBitmap.compress(Bitmap.CompressFormat.JPEG, 60, baos) + if (softwareBitmap !== bitmap) { + softwareBitmap.recycle() + } + return "data:image/jpeg;base64," + Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP) + } + + /** 辅助:构造 WritableNativeMap。 */ + private fun writableMapOf(vararg pairs: Pair): WritableNativeMap { + val map = WritableNativeMap() + for ((k, v) in pairs) { + when (v) { + is String -> map.putString(k, v) + is Int -> map.putInt(k, v) + is Long -> map.putDouble(k, v.toDouble()) + is Boolean -> map.putBoolean(k, v) + is Number -> map.putDouble(k, v.toDouble()) + is WritableNativeMap -> map.putMap(k, v) + is WritableNativeArray -> map.putArray(k, v) + else -> map.putNull(k) + } + } + return map + } + + /** 将当前应用拉到前台以显示确认弹窗 */ + fun bringAppToForeground() { + try { + val intent = packageManager.getLaunchIntentForPackage(packageName) + if (intent != null) { + intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + startActivity(intent) + Log.i(TAG, "已成功拉起本应用到前台显示弹窗") + } + } catch (e: Exception) { + Log.e(TAG, "拉起应用到前台失败: ${e.message}") + } + } + + /** 递归提取无障碍节点树的全部文本。 */ + private fun dumpNodeTexts(node: AccessibilityNodeInfo?, list: MutableList) { + if (node == null) return + val text = node.text?.toString() + if (!text.isNullOrBlank()) { + list.add(text) + } + val childCount = node.childCount + for (i in 0 until childCount) { + val child = node.getChild(i) ?: continue + dumpNodeTexts(child, list) + child.recycle() + } + } + + /** 递归遍历无障碍节点树,检查是否包含指定的任意一个关键字。 */ + private fun findTextInNode(node: AccessibilityNodeInfo?, keywords: List): Boolean { + if (node == null) return false + val text = node.text?.toString() + if (text != null) { + for (keyword in keywords) { + if (text.contains(keyword)) { + return true + } + } + } + val childCount = node.childCount + for (i in 0 until childCount) { + val child = node.getChild(i) ?: continue + val found = findTextInNode(child, keywords) + child.recycle() + if (found) return true + } + return false + } + + override fun onInterrupt() { + Log.w(TAG, "无障碍服务被中断") + } + + override fun onUnbind(intent: android.content.Intent?): Boolean { + floatingHelper?.dismiss() + floatingHelper = null + instance = null + return super.onUnbind(intent) + } +} + +/** + * RN 上下文持有者(由 MainApplication 注入)。 + * 无障碍服务运行在系统进程,需通过静态引用访问 RN 上下文以发送事件。 + */ +object ReactContextHolder { + @Volatile var context: ReactApplicationContext? = null +} diff --git a/plugins/accessibility/android/FloatingBillView.kt b/plugins/accessibility/android/FloatingBillView.kt new file mode 100644 index 0000000..686b112 --- /dev/null +++ b/plugins/accessibility/android/FloatingBillView.kt @@ -0,0 +1,739 @@ +package com.beancount.mobile.accessibility + +import android.content.Context +import android.graphics.PixelFormat +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.graphics.drawable.GradientDrawable +import android.view.Gravity +import android.view.View +import android.view.WindowManager +import android.widget.Button +import android.widget.TextView +import android.widget.EditText +import android.widget.HorizontalScrollView +import android.widget.LinearLayout +import android.text.InputType +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.modules.core.DeviceEventManagerModule + +/** + * 浮窗账单提示(直接呈现高度优化、支持三方向切换的修改入账面板)。 + */ +class FloatingBillView( + private val context: Context, + private val draftId: String, + private val amount: String, + private val merchant: String, + private val time: String, + private val packageName: String, + private val categories: List> = emptyList(), + private val accounts: List = emptyList(), + private val initialDirection: String = "expense" +) { + companion object { + private const val TAG = "FloatingBillView" + } + + private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private var view: View? = null + private val handler = Handler(Looper.getMainLooper()) + private var selectedCategoryAccount: String = "" + private var selectedSourceAccount: String = "" + private var currentDirection: String = "expense" + + private var categoryLabel: TextView? = null + private var categoryContainer: LinearLayout? = null + private var accountLabel: TextView? = null + private var accountContainer: LinearLayout? = null + private var saveBtn: Button? = null + + private fun dp(value: Float): Int { + val density = context.resources.displayMetrics.density + return (value * density).toInt() + } + + private fun createChipView(text: String): TextView { + return TextView(context).apply { + this.text = text + textSize = 11f + setPadding(dp(8f), dp(4f), dp(8f), dp(4f)) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + rightMargin = dp(6f) + } + } + } + + /** 显示悬浮窗。 */ + fun show() { + try { + currentDirection = if (initialDirection == "income" || initialDirection == "transfer") initialDirection else "expense" + + // 1. 设置 Window 布局参数使其可聚焦(用以键盘输入)并贴靠屏幕底部 + val windowParams = WindowManager.LayoutParams( + dp(310f), + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + 0, // 0 标志代表可获取焦点 + PixelFormat.TRANSLUCENT + ).apply { + gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + y = dp(12f) // 尽量靠下以留出上方账单对照区 + } + + // 2. 使用 55% 透明度 OLED 磨砂效果背景,发光靛蓝细边框 + val containerBg = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = dp(14f).toFloat() + setColor(0x8C050506.toInt()) // 55% 透明度 OLED 黑色 + setStroke(dp(1.2f), 0x995E6AD2.toInt()) // 60% 透明度靛蓝发光边框 + } + + val container = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + background = containerBg + setPadding(dp(12f), dp(8f), dp(12f), dp(8f)) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + + // 顶部标题与方向选择器布局 + val headerLayout = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + + val titleText = TextView(context).apply { + text = "调整交易草稿" + textSize = 12f + setTextColor(0xFF98A2FF.toInt()) + paint.isFakeBoldText = true + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + } + headerLayout.addView(titleText) + + // 三方向分段选择器 (支出 / 收入 / 转账) + val segmentContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + background = GradientDrawable().apply { + cornerRadius = dp(4f).toFloat() + setColor(0x20FFFFFF.toInt()) // 12% white opacity + } + } + + val tabTexts = listOf("支出", "收入", "转账") + val tabDirections = listOf("expense", "income", "transfer") + val tabViews = mutableListOf() + + fun updateTabStyle() { + for (i in tabViews.indices) { + val active = tabDirections[i] == currentDirection + tabViews[i].apply { + setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + background = if (active) { + GradientDrawable().apply { + cornerRadius = dp(4f).toFloat() + setColor(0xFF5E6AD2.toInt()) // Indigo highlight + } + } else { + null + } + } + } + } + + for (i in tabTexts.indices) { + val tab = TextView(context).apply { + text = tabTexts[i] + textSize = 10f + setPadding(dp(6f), dp(3f), dp(6f), dp(3f)) + gravity = Gravity.CENTER + setOnClickListener { + if (currentDirection != tabDirections[i]) { + currentDirection = tabDirections[i] + updateTabStyle() + rebuildChips() + } + } + } + segmentContainer.addView(tab) + tabViews.add(tab) + } + updateTabStyle() + headerLayout.addView(segmentContainer) + container.addView(headerLayout) + + // 金额编辑 + val amountLabel = TextView(context).apply { + text = "金额" + textSize = 9f + paint.isFakeBoldText = true + setTextColor(0xFF98A2FF.toInt()) + setPadding(0, dp(4f), 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 + 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)) }) + + // 商户编辑 + val merchantLabel = TextView(context).apply { + text = "交易对手" + textSize = 9f + paint.isFakeBoldText = true + setTextColor(0xFF98A2FF.toInt()) + setPadding(0, 0, 0, dp(1f)) + } + container.addView(merchantLabel) + + val merchantInput = EditText(context).apply { + setText(merchant) + textSize = 12f + setTextColor(0xFFFFFFFF.toInt()) + background = GradientDrawable().apply { + setColor(0x4012131A.toInt()) + cornerRadius = dp(6f).toFloat() + setStroke(dp(1f), 0x80222433.toInt()) + } + setPadding(dp(10f), dp(4f), dp(10f), dp(4f)) + 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)) }) + + // 叙述备注编辑 + val narrationLabel = TextView(context).apply { + text = "描述/备注" + textSize = 9f + paint.isFakeBoldText = true + setTextColor(0xFF98A2FF.toInt()) + setPadding(0, 0, 0, dp(1f)) + } + container.addView(narrationLabel) + + val narrationInput = EditText(context).apply { + hint = "输入交易叙述" + setHintTextColor(0xFF6B7280.toInt()) + textSize = 12f + setTextColor(0xFFFFFFFF.toInt()) + background = GradientDrawable().apply { + setColor(0x4012131A.toInt()) + cornerRadius = dp(6f).toFloat() + setStroke(dp(1f), 0x80222433.toInt()) + } + setPadding(dp(10f), dp(4f), dp(10f), dp(4f)) + 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)) }) + + // Row 1 分类/转入选择 + categoryLabel = TextView(context).apply { + text = "交易分类" + textSize = 9f + paint.isFakeBoldText = true + setTextColor(0xFF98A2FF.toInt()) + setPadding(0, 0, 0, dp(2f)) + } + container.addView(categoryLabel) + + categoryContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + } + + val categoryScroll = HorizontalScrollView(context).apply { + isHorizontalScrollBarEnabled = false + addView(categoryContainer) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + container.addView(categoryScroll) + container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(4f)) }) + + // Row 2 资金出入账户选择 + accountLabel = TextView(context).apply { + text = "资金来源账户" + textSize = 9f + paint.isFakeBoldText = true + setTextColor(0xFF98A2FF.toInt()) + setPadding(0, 0, 0, dp(2f)) + } + container.addView(accountLabel) + + accountContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + } + + val accountScroll = HorizontalScrollView(context).apply { + isHorizontalScrollBarEnabled = false + addView(accountContainer) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + container.addView(accountScroll) + container.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(1, dp(8f)) }) + + // 底部操作栏 + val btnContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + + // 1) 打开应用按钮 + val openAppBtn = Button(context).apply { + text = "打开应用" + setTextColor(0xFFD1D5DB.toInt()) + background = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = dp(8f).toFloat() + setColor(0xFF1F2937.toInt()) + setStroke(dp(1f), 0xFF374151.toInt()) + } + textSize = 11f + layoutParams = LinearLayout.LayoutParams(0, dp(32f), 1f).apply { rightMargin = dp(6f) } + setOnClickListener { + val newAmount = amountInput.text.toString().trim() + val newPayee = merchantInput.text.toString().trim() + val newNarration = narrationInput.text.toString().trim() + + sendOpenAppEvent(newAmount, newPayee, newNarration, selectedCategoryAccount, selectedSourceAccount) + dismiss() + BillingAccessibilityService.instance?.bringAppToForeground() + } + } + + // 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 + textSize = 11f + layoutParams = LinearLayout.LayoutParams(0, dp(32f), 1f).apply { rightMargin = dp(6f) } + setOnClickListener { + sendCancelEvent() + dismiss() + } + } + + // 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 + textSize = 11f + paint.isFakeBoldText = true + layoutParams = LinearLayout.LayoutParams(0, dp(32f), 1.3f) + setOnClickListener { + val newAmount = amountInput.text.toString().trim() + val newPayee = merchantInput.text.toString().trim() + val newNarration = narrationInput.text.toString().trim() + + sendSaveEvent(newAmount, newPayee, newNarration, selectedCategoryAccount, selectedSourceAccount) + dismiss() + } + } + + btnContainer.addView(openAppBtn) + btnContainer.addView(cancelBtn) + btnContainer.addView(saveBtn) + container.addView(btnContainer) + + // 构建金额安全校验 + 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 + saveBtn?.isEnabled = isValid + saveBtn?.background = GradientDrawable().apply { + cornerRadius = dp(8f).toFloat() + setColor(if (isValid) 0xFF5E6AD2.toInt() else 0xFF374151.toInt()) + } + saveBtn?.setTextColor(if (isValid) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } catch (e: Exception) { + Log.e(TAG, "金额校验异常", e) + saveBtn?.isEnabled = false + } + } + } + amountInput.addTextChangedListener(amountWatcher) + + // 填充金额触发 Watcher 校验 + amountInput.setText(amount) + + // 动态初始构建两行胶囊 + rebuildChips() + + view = container + windowManager.addView(view, windowParams) + Log.i(TAG, "悬浮修改记账面板已显示: ¥$amount") + + // 用户一旦进行任何交互(触摸面板或获得输入焦点),立刻取消自动消失定时器 + val cancelTimerListener = View.OnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + handler.removeCallbacksAndMessages(null) + Log.d(TAG, "已获得输入焦点,取消自动消失定时器") + } + } + amountInput.onFocusChangeListener = cancelTimerListener + merchantInput.onFocusChangeListener = cancelTimerListener + narrationInput.onFocusChangeListener = cancelTimerListener + + container.setOnTouchListener { _, _ -> + handler.removeCallbacksAndMessages(null) + Log.d(TAG, "已触摸卡片,取消自动消失定时器") + false + } + + // 15 秒无操作自动消失(如果用户没有交互的话) + handler.postDelayed({ + sendCancelEvent() + dismiss() + }, 15000L) + + } catch (e: Exception) { + Log.e(TAG, "悬浮账单面板显示失败: ${e.message}", e) + } + } + + /** 动态根据交易方向重绘第一行与第二行滑动的胶囊列表 */ + private fun rebuildChips() { + categoryContainer?.removeAllViews() + accountContainer?.removeAllViews() + + val categoryChips = mutableListOf>() + val accountChips = mutableListOf>() + + // === 1. 绘制第一行 (分类 / 转入) === + if (currentDirection == "expense") { + categoryLabel?.text = "交易分类" + val filteredCats = categories.filter { it["type"] == "expense" } + for (cat in filteredCats) { + val catAccount = cat["account"] ?: "" + val catName = cat["name"] ?: "" + val chip = createChipView(catName) + + fun updateStyle(selected: String) { + for (pair in categoryChips) { + val active = pair.first == selected + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) setColor(0xFF5E6AD2.toInt()) + else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + } + + chip.setOnClickListener { + selectedCategoryAccount = catAccount + updateStyle(catAccount) + } + categoryContainer?.addView(chip) + categoryChips.add(catAccount to chip) + } + selectedCategoryAccount = filteredCats.firstOrNull()?.get("account") ?: "" + categoryChips.forEach { pair -> + val active = pair.first == selectedCategoryAccount + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) setColor(0xFF5E6AD2.toInt()) + else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + + } else if (currentDirection == "income") { + categoryLabel?.text = "收入分类" + val filteredCats = categories.filter { it["type"] == "income" } + for (cat in filteredCats) { + val catAccount = cat["account"] ?: "" + val catName = cat["name"] ?: "" + val chip = createChipView(catName) + + fun updateStyle(selected: String) { + for (pair in categoryChips) { + val active = pair.first == selected + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) setColor(0xFF5E6AD2.toInt()) + else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + } + + chip.setOnClickListener { + selectedCategoryAccount = catAccount + updateStyle(catAccount) + } + categoryContainer?.addView(chip) + categoryChips.add(catAccount to chip) + } + selectedCategoryAccount = filteredCats.firstOrNull()?.get("account") ?: "" + categoryChips.forEach { pair -> + val active = pair.first == selectedCategoryAccount + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) setColor(0xFF5E6AD2.toInt()) + else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + + } else { + // transfer + categoryLabel?.text = "转入账户" + for (acct in accounts) { + val shortName = acct.split(":").lastOrNull() ?: acct + val chip = createChipView(shortName) + + fun updateStyle(selected: String) { + for (pair in categoryChips) { + val active = pair.first == selected + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) setColor(0xFF10B981.toInt()) + else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + } + + chip.setOnClickListener { + selectedCategoryAccount = acct + updateStyle(acct) + } + categoryContainer?.addView(chip) + categoryChips.add(acct to chip) + } + selectedCategoryAccount = if (accounts.size > 1 && accounts[0] == selectedSourceAccount) accounts[1] else (accounts.firstOrNull() ?: "") + categoryChips.forEach { pair -> + val active = pair.first == selectedCategoryAccount + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) setColor(0xFF10B981.toInt()) + else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + } + + // === 2. 绘制第二行 (资金账户) === + if (currentDirection == "expense") { + accountLabel?.text = "资金来源" + } else if (currentDirection == "income") { + accountLabel?.text = "存入账户" + } else { + accountLabel?.text = "转出账户" + } + + for (acct in accounts) { + val shortName = acct.split(":").lastOrNull() ?: acct + val chip = createChipView(shortName) + + fun updateStyle(selected: String) { + for (pair in accountChips) { + val active = pair.first == selected + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) { + setColor(if (currentDirection == "income") 0xFF10B981.toInt() else 0xFFE11D48.toInt()) + } else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + } + + chip.setOnClickListener { + selectedSourceAccount = acct + updateStyle(acct) + + // 若是转账,且转入转出账户冲突,自动移开转入账户 + if (currentDirection == "transfer" && selectedCategoryAccount == selectedSourceAccount) { + val nextAvail = categoryChips.find { it.first != selectedSourceAccount } + if (nextAvail != null) { + selectedCategoryAccount = nextAvail.first + for (p in categoryChips) { + val act = p.first == selectedCategoryAccount + p.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (act) setColor(0xFF10B981.toInt()) + else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + p.second.setTextColor(if (act) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + } + } + } + accountContainer?.addView(chip) + accountChips.add(acct to chip) + } + + selectedSourceAccount = accounts.firstOrNull() ?: "" + accountChips.forEach { pair -> + val active = pair.first == selectedSourceAccount + pair.second.background = GradientDrawable().apply { + cornerRadius = dp(6f).toFloat() + if (active) { + setColor(if (currentDirection == "income") 0xFF10B981.toInt() else 0xFFE11D48.toInt()) + } else { + setColor(0x4012131A.toInt()) + setStroke(dp(1f), 0x80222433.toInt()) + } + } + pair.second.setTextColor(if (active) 0xFFFFFFFF.toInt() else 0xFF9CA3AF.toInt()) + } + } + + /** 推送保存事件到 JS 层。 */ + private fun sendSaveEvent(newAmount: String, newPayee: String, newNarration: String, categoryAccount: String, sourceAccount: String) { + val reactContext = ReactContextHolder.context ?: return + try { + val map = WritableNativeMap().apply { + putString("draftId", draftId) + putString("amount", newAmount) + putString("merchant", newPayee) + putString("narration", newNarration) + putString("category", categoryAccount) + putString("account", sourceAccount) + putString("time", time) + putString("direction", currentDirection) + putString("packageName", packageName) + putBoolean("confirmed", true) + putBoolean("editRequested", false) + putBoolean("isManualEdit", true) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingConfirmed", map) + } catch (e: Exception) { + Log.e(TAG, "推送修改保存事件失败: ${e.message}") + } + } + + /** 推送打开应用事件到 JS 层,以便前台加载详细表单。 */ + private fun sendOpenAppEvent(newAmount: String, newPayee: String, newNarration: String, categoryAccount: String, sourceAccount: String) { + val reactContext = ReactContextHolder.context ?: return + try { + val map = WritableNativeMap().apply { + putString("draftId", draftId) + putString("amount", newAmount) + putString("merchant", newPayee) + putString("narration", newNarration) + putString("category", categoryAccount) + putString("account", sourceAccount) + putString("time", time) + putString("direction", currentDirection) + putString("packageName", packageName) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingOpenApp", map) + } catch (e: Exception) { + Log.e(TAG, "推送打开应用事件失败: ${e.message}") + } + } + + /** 推送取消/忽略事件到 JS 层。 */ + private fun sendCancelEvent() { + val reactContext = ReactContextHolder.context ?: return + try { + val map = WritableNativeMap().apply { + putString("draftId", draftId) + putBoolean("confirmed", false) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingConfirmed", map) + } catch (e: Exception) { + Log.e(TAG, "推送取消事件失败: ${e.message}") + } + } + + /** 关闭浮窗。 */ + fun dismiss() { + handler.removeCallbacksAndMessages(null) + try { + view?.let { windowManager.removeView(it) } + } catch (_: Exception) {} + view = null + } +} diff --git a/plugins/accessibility/android/FloatingHelper.kt b/plugins/accessibility/android/FloatingHelper.kt new file mode 100644 index 0000000..3cb2145 --- /dev/null +++ b/plugins/accessibility/android/FloatingHelper.kt @@ -0,0 +1,418 @@ +package com.beancount.mobile.accessibility + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView + +/** + * 记账助手悬浮球(边缘竖线胶囊面板)。 + * 贴合在屏幕边缘,采用高透、超轻量竖线指示器,点击后展开垂直对齐的功能菜单。 + */ +class FloatingHelper( + private val service: BillingAccessibilityService +) { + companion object { + private const val TAG = "FloatingHelper" + private var lastX = 0 + private var lastY = 400 + } + + private val windowManager = service.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private var containerView: LinearLayout? = null + private var bubbleView: View? = null + private var menuView: LinearLayout? = null + private var isExpanded = false + + private val params = WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + PixelFormat.TRANSLUCENT + ).apply { + gravity = Gravity.TOP or Gravity.START + x = lastX + y = lastY + } + + @SuppressLint("ClickableViewAccessibility") + fun show() { + if (containerView != null) return + + try { + val context = service + + val density = service.resources.displayMetrics.density + fun dp(value: Float) = (value * density).toInt() + + // 1. 创建整体包裹容器 (水平排列,当贴在左侧时,菜单向右展开) + containerView = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_OUTSIDE) { + if (isExpanded) { + collapse() + } + } + false + } + } + + // 2. 创建高透明度悬浮球/边缘竖线 (仅 3dp 宽的极简胶囊,搭配 24dp 的宽触控区) + val bubbleLayout = FrameLayout(context).apply { + layoutParams = LinearLayout.LayoutParams(dp(24f), dp(60f)) + } + + val indicatorView = View(context).apply { + background = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = dp(1.5f).toFloat() // 高度圆润 + setColor(0xB05E6AD2.toInt()) // 70% 高透靛蓝色,无边框 + } + layoutParams = FrameLayout.LayoutParams(dp(3f), dp(44f)).apply { + gravity = Gravity.CENTER + } + } + bubbleLayout.addView(indicatorView) + bubbleView = bubbleLayout + + // 3. 创建展开菜单 (垂直布局,高紧凑度设计) + 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 + } + + menuView = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER_HORIZONTAL + background = menuBg + setPadding(dp(4f), dp(4f), dp(4f), dp(4f)) + visibility = View.GONE + layoutParams = LinearLayout.LayoutParams( + dp(96f), // ultra-compact width: 96dp + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + leftMargin = dp(4f) // spacing with indicator line + } + } + + // 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) + + // Button 1: "识别账单" + val btnOcr = TextView(context).apply { + text = "识别账单" + setTextColor(0xFFFFFFFF.toInt()) + 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 + } + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + dp(32f) + ).apply { + bottomMargin = dp(5f) + } + setCompoundDrawablesWithIntrinsicBounds(ocrIcon, null, null, null) + compoundDrawablePadding = dp(4f) + setOnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + view.background = GradientDrawable().apply { + cornerRadius = dp(7f).toFloat() + setColor(0x405E6AD2.toInt()) + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + view.background = GradientDrawable().apply { + cornerRadius = dp(7f).toFloat() + setColor(0x1F5E6AD2.toInt()) + } + if (event.action == MotionEvent.ACTION_UP) { + view.performClick() + } + } + } + true + } + setOnClickListener { + service.triggerManualExtraction() + } + } + + // Button 2: "记住此页" + val btnRemember = TextView(context).apply { + text = "记住此页" + setTextColor(0xFFE5E7EB.toInt()) + 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 + } + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + dp(32f) + ) + setCompoundDrawablesWithIntrinsicBounds(pinIcon, null, null, null) + compoundDrawablePadding = dp(4f) + setOnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + view.background = GradientDrawable().apply { + cornerRadius = dp(7f).toFloat() + setColor(0x30FFFFFF.toInt()) + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + view.background = GradientDrawable().apply { + cornerRadius = dp(7f).toFloat() + setColor(0x15FFFFFF.toInt()) + } + if (event.action == MotionEvent.ACTION_UP) { + view.performClick() + } + } + } + true + } + setOnClickListener { + try { + service.rememberCurrentPage() + FloatingTip(service, "📌 已将当前页面加入识别白名单!", FloatingTip.TipPosition.TOP, 2500L).show() + } catch (e: Exception) { + FloatingTip(service, "记录失败: ${e.message}", FloatingTip.TipPosition.TOP, 2500L).show() + } + collapse() + } + } + + menuView?.addView(btnOcr) + menuView?.addView(btnRemember) + + containerView?.addView(bubbleView) + containerView?.addView(menuView) + + // 4. 设置悬浮条拖动事件 (点击即展开,拖动则调整位置) + var initialX = 0 + var initialY = 0 + var initialTouchX = 0f + var initialTouchY = 0f + var isMoving = false + + bubbleView?.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + initialX = params.x + initialY = params.y + initialTouchX = event.rawX + initialTouchY = event.rawY + isMoving = false + true + } + MotionEvent.ACTION_MOVE -> { + val dx = (event.rawX - initialTouchX).toInt() + val dy = (event.rawY - initialTouchY).toInt() + if (Math.abs(dx) > 10 || Math.abs(dy) > 10) { + isMoving = true + } + params.x = initialX + dx + params.y = initialY + dy + containerView?.let { windowManager.updateViewLayout(it, params) } + + lastX = params.x + lastY = params.y + true + } + MotionEvent.ACTION_UP -> { + if (!isMoving) { + toggleMenu() + } else { + // 拖动抬起时自动吸附到屏幕边缘 + if (params.x < service.resources.displayMetrics.widthPixels / 2) { + params.x = 0 + } else { + params.x = service.resources.displayMetrics.widthPixels - dp(24f) + } + containerView?.let { windowManager.updateViewLayout(it, params) } + + lastX = params.x + lastY = params.y + } + true + } + else -> false + } + } + + windowManager.addView(containerView, params) + Log.i(TAG, "记账助手悬浮窗显示成功") + } catch (e: Exception) { + Log.e(TAG, "记账助手悬浮窗创建失败: ${e.message}", e) + } + } + + private fun toggleMenu() { + if (isExpanded) { + collapse() + } else { + expand() + } + } + + private fun expand() { + bubbleView?.visibility = View.GONE + menuView?.visibility = View.VISIBLE + isExpanded = true + } + + fun collapse() { + menuView?.visibility = View.GONE + bubbleView?.visibility = View.VISIBLE + isExpanded = false + } + + fun hideTemporarily() { + containerView?.visibility = View.GONE + } + + fun showTemporarily() { + containerView?.visibility = View.VISIBLE + } + + fun dismiss() { + try { + containerView?.let { windowManager.removeView(it) } + } catch (_: Exception) {} + containerView = null + bubbleView = null + menuView = null + isExpanded = false + } +} + +/** + * 扫码/OCR 矢量图标 drawable。 + */ +class ScanIconDrawable( + private val color: Int, + private val strokeWidthPx: Float, + private val laserColor: Int, + private val sizePx: Int +) : Drawable() { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = strokeWidthPx + strokeCap = Paint.Cap.ROUND + } + + override fun draw(canvas: Canvas) { + val w = bounds.width().toFloat() + val h = bounds.height().toFloat() + paint.color = color + paint.style = Paint.Style.STROKE + + // 绘制 4 个角的扫描框 + val len = w * 0.25f + val pad = strokeWidthPx + + // 左上 + canvas.drawLine(pad, pad, pad + len, pad, paint) + canvas.drawLine(pad, pad, pad, pad + len, paint) + + // 右上 + canvas.drawLine(w - pad, pad, w - pad - len, pad, paint) + canvas.drawLine(w - pad, pad, w - pad, pad + len, paint) + + // 左下 + canvas.drawLine(pad, h - pad, pad + len, h - pad, paint) + canvas.drawLine(pad, h - pad, pad, h - pad - len, paint) + + // 右下 + canvas.drawLine(w - pad, h - pad, w - pad - len, h - pad, paint) + canvas.drawLine(w - pad, h - pad, w - pad, h - pad - len, paint) + + // 绘制扫描红线 (激光) + paint.style = Paint.Style.FILL + paint.color = laserColor + val laserY = h / 2f + canvas.drawRect(pad * 2f, laserY - strokeWidthPx / 2f, w - pad * 2f, laserY + strokeWidthPx / 2f, paint) + } + + override fun getIntrinsicWidth() = sizePx + override fun getIntrinsicHeight() = sizePx + + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {} + override fun getOpacity() = PixelFormat.TRANSLUCENT +} + +/** + * 图钉/记住当前页 矢量图标 drawable。 + */ +class PinIconDrawable( + private val color: Int, + private val strokeWidthPx: Float, + private val sizePx: Int +) : Drawable() { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = strokeWidthPx + strokeCap = Paint.Cap.ROUND + } + + override fun draw(canvas: Canvas) { + val w = bounds.width().toFloat() + val h = bounds.height().toFloat() + paint.color = color + val cx = w / 2f + + // 图钉头部 (帽) + paint.style = Paint.Style.FILL + val hatW = w * 0.35f + val hatH = h * 0.12f + canvas.drawRoundRect(RectF(cx - hatW, hatH, cx + hatW, hatH * 2.2f), strokeWidthPx, strokeWidthPx, paint) + + // 图钉身体 (中) + val bodyW = w * 0.22f + canvas.drawRect(cx - bodyW, hatH * 2.2f, cx + bodyW, h * 0.58f, paint) + + // 针尖 (底) + paint.style = Paint.Style.STROKE + canvas.drawLine(cx, h * 0.58f, cx, h - strokeWidthPx, paint) + } + + override fun getIntrinsicWidth() = sizePx + override fun getIntrinsicHeight() = sizePx + + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {} + override fun getOpacity() = PixelFormat.TRANSLUCENT +} diff --git a/plugins/accessibility/android/FloatingTip.kt b/plugins/accessibility/android/FloatingTip.kt new file mode 100644 index 0000000..9640820 --- /dev/null +++ b/plugins/accessibility/android/FloatingTip.kt @@ -0,0 +1,128 @@ +package com.beancount.mobile.accessibility + +import android.content.Context +import android.graphics.PixelFormat +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 浮窗账单提示」)。 + * + * 参考 AutoAccounting 的 FloatingTip + RepeatToast: + * - 轻量浮窗(非全屏),滑入动画,自动消失 + * - 三种布局:顶部 / 左侧 / 右侧 + * - 倒计时进度环 + * - 重复账单提示(RepeatToast) + * + * 比 FloatingBillView 更轻:仅展示提示,不交互。 + * 需 SYSTEM_ALERT_WINDOW 权限(由 Config Plugin 注册)。 + */ +class FloatingTip( + private val context: Context, + private val message: String, + private val position: TipPosition = TipPosition.TOP, + private val durationMs: Long = 3000L, +) { + companion object { + private const val TAG = "FloatingTip" + private const val ANIM_DURATION = 300L + } + + enum class TipPosition { TOP, LEFT, RIGHT } + + private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private var view: View? = null + private val handler = Handler(Looper.getMainLooper()) + + /** 显示浮窗提示。 */ + fun show() { + try { + val layoutParams = WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT, + ).apply { + gravity = when (position) { + TipPosition.TOP -> Gravity.TOP or Gravity.CENTER_HORIZONTAL + TipPosition.LEFT -> Gravity.LEFT or Gravity.CENTER_VERTICAL + TipPosition.RIGHT -> Gravity.RIGHT or Gravity.CENTER_VERTICAL + } + y = if (position == TipPosition.TOP) 100 else 0 + x = if (position != TipPosition.TOP) 50 else 0 + } + + val container = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + setBackgroundColor(0xF0333333.toInt()) + 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) + } + 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}") + } + } + + /** 关闭浮窗。 */ + fun dismiss() { + handler.removeCallbacksAndMessages(null) + try { view?.let { windowManager.removeView(it) } } catch (_: Exception) {} + view = null + } +} + +/** + * 重复账单提示(plan.md「3.8」+ AutoAccounting RepeatToast)。 + * 当检测到重复账单时,轻量提示用户(不弹浮窗,用系统 Toast 风格)。 + */ +class RepeatToast(private val context: Context, private val message: String) { + fun show() { + val tip = FloatingTip(context, "⚠ $message", FloatingTip.TipPosition.TOP, 2000L) + tip.show() + Log.d("RepeatToast", "重复提示: $message") + } +} diff --git a/plugins/accessibility/android/OcrTileService.kt b/plugins/accessibility/android/OcrTileService.kt new file mode 100644 index 0000000..70dd8bc --- /dev/null +++ b/plugins/accessibility/android/OcrTileService.kt @@ -0,0 +1,62 @@ +package com.beancount.mobile.accessibility + +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import android.util.Log +import android.content.Intent +import android.app.PendingIntent + +/** + * 快速设置磁贴(plan.md「3.11 快速设置磁贴」)。 + * + * 参考 AutoAccounting 的 OcrTileService: + * - 用户下拉快速设置,点击「OCR 记账」磁贴触发一次手动 OCR + * - Android 14+ 用 PendingIntent + startActivityAndCollapse + * + * 触发后调用 BillingAccessibilityService.triggerManualOcr()。 + */ +class OcrTileService : TileService() { + + companion object { + private const val TAG = "OcrTileService" + } + + override fun onStartListening() { + super.onStartListening() + qsTile?.let { tile -> + tile.state = Tile.STATE_ACTIVE + tile.label = "OCR 记账" + tile.updateTile() + } + Log.d(TAG, "磁贴开始监听") + } + + override fun onClick() { + super.onClick() + Log.i(TAG, "磁贴被点击,触发手动 OCR") + triggerManualOcr() + } + + /** 触发手动 OCR(通过 BillingAccessibilityService)。 */ + private fun triggerManualOcr() { + val service = BillingAccessibilityService.instance + if (service != null) { + service.triggerManualOcr() + return + } + // 服务未运行,尝试启动(Android 14+ 用 collapse) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val pi = PendingIntent.getActivity( + this, 0, + Intent().apply { + setClassName(packageName, "com.beancount.mobile.MainActivity") + action = "com.beancount.mobile.TRIGGER_OCR" + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }, + PendingIntent.FLAG_IMMUTABLE, + ) + startActivityAndCollapse(pi) + } + } +} diff --git a/plugins/accessibility/android/res/xml/accessibility_service_config.xml b/plugins/accessibility/android/res/xml/accessibility_service_config.xml new file mode 100644 index 0000000..3a506d0 --- /dev/null +++ b/plugins/accessibility/android/res/xml/accessibility_service_config.xml @@ -0,0 +1,15 @@ + + + diff --git a/plugins/accessibility/app.plugin.js b/plugins/accessibility/app.plugin.js index 2cdde3a..221fbbb 100644 --- a/plugins/accessibility/app.plugin.js +++ b/plugins/accessibility/app.plugin.js @@ -62,44 +62,16 @@ function withAccessibilityService(config) { }, ]); - // 2. 注入 ReactContextHolder 赋值 + 注册 AccessibilityBridgePackage 到 MainApplication + // 2. 注册 AccessibilityBridgePackage 到 MainApplication config = withMainApplication(config, (modConfig) => { let content = modConfig.modResults.contents; - // 2a. 注入 import(ReactContextHolder + AccessibilityBridgePackage) - if (!content.includes(`import ${PACKAGE}.ReactContextHolder`)) { + // 2a. 注入 import(AccessibilityBridgePackage) + if (!content.includes(`import ${PACKAGE}.AccessibilityBridgePackage`)) { content = content.replace( /^(package\s+[\w.]+;?\s*)$/m, - `$1\nimport ${PACKAGE}.ReactContextHolder\nimport ${PACKAGE}.AccessibilityBridgePackage`, + `$1\nimport ${PACKAGE}.AccessibilityBridgePackage`, ); - } else if (!content.includes(`import ${PACKAGE}.AccessibilityBridgePackage`)) { - content = content.replace( - `import ${PACKAGE}.ReactContextHolder`, - `import ${PACKAGE}.ReactContextHolder\nimport ${PACKAGE}.AccessibilityBridgePackage`, - ); - } - - // 2b. 在 onCreate 方法中注入 ReactContextHolder.context = this - if (!content.includes('ReactContextHolder.context')) { - // 尝试在 super.onCreate() 后注入 - if (/super\.onCreate\(\)/.test(content)) { - content = content.replace( - /(super\.onCreate\(\))/, - `$1\n // 由 Config Plugin 注入:让原生服务能访问 RN 上下文\n ReactContextHolder.context = this`, - ); - } else if (/onCreate/.test(content)) { - // 有 onCreate 但没有 super.onCreate()(不太可能) - content = content.replace( - /(onCreate[^{]*\{)/, - `$1\n ReactContextHolder.context = this`, - ); - } else { - // 没有 onCreate — 在类体内注入一个 - content = content.replace( - /(class\s+MainApplication\s*[^{]*\{)/, - `$1\n override fun onCreate() {\n super.onCreate()\n ReactContextHolder.context = this\n }`, - ); - } } // 2c. 在 getPackages() 的 .apply {} 块里注入 add(AccessibilityBridgePackage()) diff --git a/plugins/notification-listener/android/BillingNotificationListenerService.kt b/plugins/notification-listener/android/BillingNotificationListenerService.kt new file mode 100644 index 0000000..5497c49 --- /dev/null +++ b/plugins/notification-listener/android/BillingNotificationListenerService.kt @@ -0,0 +1,96 @@ +package com.beancount.mobile.notification + +import android.app.Notification +import android.content.ComponentName +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.util.Log +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.beancount.mobile.accessibility.ReactContextHolder + +/** + * 通知监听服务(plan.md「4.1 通知监听服务」+「决策 4 Config Plugin」)。 + * + * 参考 AutoAccounting 的 NotificationListenerService: + * - 提取支付 App 通知的 title/text + * - 白名单过滤(仅支付类 App) + * - 关键词黑白名单(JS 层 keywordFilter 进一步过滤) + * - MD5 去重(JS 层 NotificationChannel 处理,避免原生持有状态) + * - onListenerDisconnected 时 requestRebind 自动重连 + * + * 通过 DeviceEventEmitter 把通知事件推送到 JS 层 NotificationChannel。 + */ +class BillingNotificationListenerService : NotificationListenerService() { + + companion object { + private const val TAG = "BillingNotification" + + /** 支付 App 白名单(与 JS 层 DEFAULT_PAYMENT_PACKAGES 一致)。 */ + private val PAYMENT_PACKAGES = setOf( + "com.eg.android.AlipayGphone", + "com.tencent.mm", + "com.unionpay", + "com.cmbchina", + "com.icbc", + "com.chinamworld.main", + "com.ccbrcb", + "com.bankcomm.Bankcomm", + ) + } + + override fun onNotificationPosted(sbn: StatusBarNotification?) { + super.onNotificationPosted(sbn) + runCatching { + val packageName = sbn?.packageName?.toString() ?: return + // 白名单过滤 + if (!PAYMENT_PACKAGES.contains(packageName)) return + + val notification = sbn.notification + val extras = notification.extras + val title = extras?.getCharSequence(Notification.EXTRA_TITLE)?.toString() ?: "" + val text = (extras?.getCharSequence(Notification.EXTRA_BIG_TEXT) + ?: extras?.getCharSequence(Notification.EXTRA_TEXT))?.toString() ?: "" + + if (title.isBlank() && text.isBlank()) return + + Log.d(TAG, "收到支付通知: pkg=$packageName, title=$title") + // 推送到 JS 层 + sendNotificationEvent(packageName, title, text) + }.onFailure { + Log.e(TAG, "通知处理异常: ${it.message}", it) + } + } + + /** + * 监听断开时自动重连(参考 AutoAccounting requestRebind)。 + * Android Doze / App Standby 可能断开通知监听。 + */ + override fun onListenerDisconnected() { + super.onListenerDisconnected() + Log.w(TAG, "通知监听断开,尝试重连") + requestRebind(ComponentName(this, BillingNotificationListenerService::class.java)) + } + + /** 把通知事件推送到 JS 层 NotificationChannel.handleNotification。 */ + private fun sendNotificationEvent(packageName: String, title: String, text: String) { + val reactContext = ReactContextHolder.context ?: run { + Log.w(TAG, "RN 上下文未就绪,丢弃通知") + return + } + try { + val map = WritableNativeMap().apply { + putString("packageName", packageName) + putString("title", title) + putString("text", text) + putDouble("timestamp", System.currentTimeMillis().toDouble()) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingNotification", map) + } catch (e: Exception) { + Log.e(TAG, "推送通知事件失败: ${e.message}") + } + } +} diff --git a/plugins/ppocr/README.md b/plugins/ppocr/README.md index ad649cb..f8e8d37 100644 --- a/plugins/ppocr/README.md +++ b/plugins/ppocr/README.md @@ -14,8 +14,8 @@ plugins/ppocr/ │ └── OcrPackage.kt # RN Package 注册(注入到 MainApplication.getPackages) └── assets/ # ONNX 模型 + 字典(需自行下载放置) ├── ppocrv5_det.onnx # 文本检测模型 - ├── ppocrv5_rec.onnx # 文本识别模型 - └── ppocr_keys_v1.txt # CJK 字典(CTC 解码用) + ├── ppocrv5_rec.onnx # 文本识别模型(多语言,输出 18385 维) + └── ppocrv5_dict.txt # PP-OCRv5 多语言字典(18383 字符,CTC 解码用) ``` ## 模型获取(一键下载) @@ -33,12 +33,16 @@ curl -L -o ppocrv5_det.onnx https://huggingface.co/ilaylow/PP_OCRv5_mobile_onnx/ # rec 模型(16.6 MB) curl -L -o ppocrv5_rec.onnx https://huggingface.co/ilaylow/PP_OCRv5_mobile_onnx/resolve/main/ppocrv5_rec.onnx -# CJK 字典(26 KB,PaddleOCR 标准字典) -curl -L -o ppocr_keys_v1.txt https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/release/2.6/ppocr/utils/ppocr_keys_v1.txt +# PP-OCRv5 多语言字典(74 KB,必须与上面的 rec 模型配套) +curl -L -o ppocrv5_dict.txt https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/dict/ppocrv5_dict.txt ``` 或用 HuggingFace CLI(首次下载原生模型再转 ONNX 的方式,参见历史 git log)。 +> ⚠️ **字典必须与 rec 模型配套**:ppocrv5_rec.onnx 输出 18385 维(= 18383 字符 + blank + 特殊位), +> 必须使用 `ppocrv5_dict.txt`(18383 行)。若错用旧版 `ppocr_keys_v1.txt`(仅 6623 行), +> CTC 解码会把真实字符的高索引全部丢弃,只输出形如 `'消'青'露'仰'` 的单引号穿插单字符乱码。 + > 来源说明:[ilaylow/PP_OCRv5_mobile_onnx](https://huggingface.co/ilaylow/PP_OCRv5_mobile_onnx) 是社区维护的 PP-OCRv5 mobile ONNX 镜像,基于官方 [PaddlePaddle/PP-OCRv5_mobile_det](https://huggingface.co/PaddlePaddle/PP-OCRv5_mobile_det) 与 [_rec](https://huggingface.co/PaddlePaddle/PP-OCRv5_mobile_rec) 转换而来。 ## 性能配置(参考 AutoAccounting OcrProcessor.kt) @@ -75,4 +79,6 @@ JS 层通过 `src/services/ocrBridge.ts` 的 `NativeOcrBridge` 调用,桥接 - `android/OcrPackage.kt`:✅ RN Package 注册 - `assets/`:需自行下载放置(见上「模型获取」),版权/体积原因不入仓库 -真机构建步骤:放置模型文件 → `npx expo prebuild --platform android` → `npx expo run:android`。 +真机构建步骤:放置模型文件 → `npx expo prebuild --platform android`(Config Plugin 会把 Kotlin 源码与 `assets/` 下的模型/字典复制进 `android/`)→ `npx expo run:android`。 + +> 若之前已 prebuild 过且更换过字典/模型文件,务必重新执行 `npx expo prebuild --clean`,否则 `android/app/src/main/assets/` 下可能残留旧字典(如 `ppocr_keys_v1.txt`),导致新代码找不到配套字典。 diff --git a/plugins/ppocr/android/OcrModule.kt b/plugins/ppocr/android/OcrModule.kt new file mode 100644 index 0000000..661fd33 --- /dev/null +++ b/plugins/ppocr/android/OcrModule.kt @@ -0,0 +1,659 @@ +package com.beancount.mobile.ppocr + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 +import android.util.Log +import ai.onnxruntime.OnnxTensor +import ai.onnxruntime.OrtEnvironment +import ai.onnxruntime.OrtSession +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeArray +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.module.annotations.ReactModule +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import java.io.BufferedReader +import java.io.InputStreamReader +import java.nio.FloatBuffer +import java.util.concurrent.locks.ReentrantLock +import kotlin.math.max +import kotlin.math.min + +/** + * PP-OCRv5 (ONNX Runtime) React Native Bridge(plan.md「3.4 Layer 2」+「决策 4 Config Plugin」)。 + * + * 引擎:ONNX Runtime(跨平台、微软官方、Windows 友好),替代 NCNN 路线。 + * 模型:ppocrv5_det.onnx + ppocrv5_rec.onnx(从 ilaylow/PP_OCRv5_mobile_onnx 下载)。 + * 字典:ppocrv5_dict.txt(PP-OCRv5 多语言字典,18383 字符;rec 模型 18385 维输出 = 字典 + blank + 特殊位)。 + * + * 流水线: + * 1. det(文本检测):bitmap → DB 后处理得到文本框 + * 2. rec(文本识别):每个框 crop → resize 到 48px 高 → CTC 解码 + * + * 性能优化(参考 AutoAccounting OcrProcessor.kt): + * - 短边压缩到 720px(像素量比 1440p 减少约 75%) + * - CPU 执行(兼容性最稳,部分设备 GPU 会崩溃) + * - det 最大边限制 960(PaddleOCR 默认 limit_max_side_len) + * + * JS 层通过 NativeModules.PpOcr.recognizeText(base64) 调用。 + */ +const val OCR_MODULE_NAME = "PpOcr" + +@ReactModule(name = OCR_MODULE_NAME) +class OcrModule(private val context: ReactApplicationContext) : + ReactContextBaseJavaModule(context) { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val lock = ReentrantLock() + + private var ortEnv: OrtEnvironment? = null + private var detSession: OrtSession? = null + private var recSession: OrtSession? = null + private var dictionary: List = emptyList() + @Volatile private var initialized = false + @Volatile private var initFailed = false + + override fun getName(): String = OCR_MODULE_NAME + + override fun initialize() { + super.initialize() + // 异步加载模型,避免阻塞 RN 桥初始化 + scope.launch { initEngine() } + } + + /** 从 assets 加载 det/rec ONNX 模型与字典。 */ + private fun initEngine() { + lock.lock() + try { + if (initialized || initFailed) return + val env = OrtEnvironment.getEnvironment() + val opts = OrtSession.SessionOptions().apply { + // CPU 线程数:2 是兼容性/性能的稳妥折中(高端机可调高) + setInterOpNumThreads(2) + setIntraOpNumThreads(2) + // 移动端关闭内存优化里的图优化级别过高(部分模型会崩) + 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() + + detSession = det + recSession = rec + ortEnv = env + dictionary = dict + initialized = true + Log.i(OCR_MODULE_NAME, "PP-OCRv5 ONNX 模型加载成功(det+rec, dict=${dict.size})") + } 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") + } finally { + lock.unlock() + } + } + + /** rec 推理用的 OrtEnvironment(复用 ortEnv 单例)。 */ + private val recEnv: OrtEnvironment? get() = ortEnv + + /** + * 加载 ppocrv5_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 { + val words = mutableListOf() + context.assets.open(ASSET_DICT).use { stream -> + BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).useLines { lines -> + lines.forEach { line -> + // PaddleOCR 字典每行一个字符(去掉行尾换行) + words.add(line.trimEnd('\r', '\n')) + } + } + } + return words + } + + /** + * 识别图片文本。 + * @param imageBase64 base64 编码的图片(JPEG/PNG) + * @return 识别出的纯文本(所有行用 \n 连接) + */ + @ReactMethod + fun recognizeText(imageBase64: String, promise: Promise) { + scope.launch { + var bitmap: Bitmap? = null + var scaled: Bitmap? = null + try { + ensureReady() + bitmap = decodeBase64(imageBase64) + if (bitmap == null) { + promise.reject("DECODE_FAILED", "base64 解码失败") + return@launch + } + scaled = scaleDownForOcr(bitmap, OCR_MAX_SHORT_EDGE) + val blocks = runInference(scaled) + val text = blocks.joinToString("\n") { it.text } + promise.resolve(text) + } catch (e: Exception) { + Log.e(OCR_MODULE_NAME, "recognizeText 异常: ${e.message}", e) + promise.reject("OCR_ERROR", e.message) + } finally { + bitmap?.recycle() + if (scaled !== bitmap) { + scaled?.recycle() + } + } + } + } + + /** + * 识别并返回带坐标的文本块(用于复杂版面)。 + * @return JSON 数组字符串:[{text, x, y, width, height, confidence}] + */ + @ReactMethod + fun recognizeTextBlocks(imageBase64: String, promise: Promise) { + scope.launch { + var bitmap: Bitmap? = null + var scaled: Bitmap? = null + try { + ensureReady() + bitmap = decodeBase64(imageBase64) + if (bitmap == null) { + promise.reject("DECODE_FAILED", "base64 解码失败") + return@launch + } + scaled = scaleDownForOcr(bitmap, OCR_MAX_SHORT_EDGE) + val blocks = runInference(scaled) + // 序列化为 RN WritableArray + val arr = WritableNativeArray() + for (b in blocks) { + val map: WritableMap = WritableNativeMap() + map.putString("text", b.text) + map.putDouble("x", b.x.toDouble()) + map.putDouble("y", b.y.toDouble()) + map.putDouble("width", b.width.toDouble()) + map.putDouble("height", b.height.toDouble()) + map.putDouble("confidence", b.confidence.toDouble()) + arr.pushMap(map) + } + promise.resolve(arr) + } catch (e: Exception) { + Log.e(OCR_MODULE_NAME, "recognizeTextBlocks 异常: ${e.message}", e) + promise.reject("OCR_ERROR", e.message) + } finally { + bitmap?.recycle() + if (scaled !== bitmap) { + scaled?.recycle() + } + } + } + } + + /** 引擎是否已就绪(模型加载完成)。 */ + @ReactMethod + fun isReady(promise: Promise) { + promise.resolve(initialized) + } + + // ============== 推理流水线 ============== + + private fun ensureReady() { + if (!initialized && !initFailed) initEngine() + if (!initialized) throw IllegalStateException("OCR 引擎未就绪(模型未加载,${if (initFailed) "初始化失败" else "加载中"})") + } + + /** 完整推理:det 检测文本框 → 对每个框 rec 识别 → 返回带坐标的文本块。 */ + private fun runInference(bitmap: Bitmap): List { + Log.i(OCR_MODULE_NAME, "runInference 开始: bitmap 尺寸 = ${bitmap.width}x${bitmap.height}") + val det = detSession ?: run { + Log.e(OCR_MODULE_NAME, "detSession 为空,放弃推理") + return emptyList() + } + val rec = recSession ?: run { + Log.e(OCR_MODULE_NAME, "recSession 为空,放弃推理") + return emptyList() + } + + var resized: Bitmap? = null + var detInputTensor: OnnxTensor? = null + var detOutputs: OrtSession.Result? = null + val results = mutableListOf() + + try { + // ---- 1. 文本检测(DB)---- + resized = resizeForDet(bitmap, DET_LIMIT_MAX_SIDE) + Log.i(OCR_MODULE_NAME, "det 图像缩放后尺寸 = ${resized.width}x${resized.height}") + val ratioX = bitmap.width.toFloat() / resized.width + val ratioY = bitmap.height.toFloat() / resized.height + + val detInput = preprocessDet(resized) + detInputTensor = OnnxTensor.createTensor(recEnv, FloatBuffer.wrap(detInput.data), longArrayOf(1L, 3L, detInput.h.toLong(), detInput.w.toLong())) + val detInputs = mapOf("x" to detInputTensor) + detOutputs = det.run(detInputs) + @Suppress("UNCHECKED_CAST") + val detProb = (detOutputs[0].value as Array>>)[0][0] // [H,W] + Log.i(OCR_MODULE_NAME, "det 推理完成,概率图尺寸 = ${detProb.size}x${detProb[0].size}") + + // DB 后处理:threshold → 轮廓 → 最小外接矩形 + val boxes = dbPostprocess(detProb, detInput.h, detInput.w, ratioX, ratioY) + Log.i(OCR_MODULE_NAME, "dbPostprocess 后处理完成,检测到文本框数量 = ${boxes.size}") + + if (boxes.isEmpty()) return emptyList() + + // ---- 2. 文本识别(CRNN+CTC)---- + for ((idx, box) in boxes.withIndex()) { + var crop: Bitmap? = null + var recInputTensor: OnnxTensor? = null + var recOutputs: OrtSession.Result? = null + try { + crop = cropBox(bitmap, box) + if (crop == null) { + Log.w(OCR_MODULE_NAME, "裁剪文本框失败 (index = $idx)") + continue + } + val recInput = preprocessRec(crop) + recInputTensor = OnnxTensor.createTensor(recEnv, FloatBuffer.wrap(recInput.data), longArrayOf(1L, 3L, REC_IMAGE_HEIGHT.toLong(), recInput.w.toLong())) + val recInputs = mapOf("x" to recInputTensor) + recOutputs = rec.run(recInputs) + // 输出 shape: [1, T, numClasses] + @Suppress("UNCHECKED_CAST") + val logits = (recOutputs[0].value as Array>)[0] + + val (text, conf) = ctcGreedyDecode(logits) + val xs = box.map { it[0] } + val ys = box.map { it[1] } + val minX = (xs.minOrNull() ?: 0f).toInt() + val minY = (ys.minOrNull() ?: 0f).toInt() + val maxX = (xs.maxOrNull() ?: 0f).toInt() + val maxY = (ys.maxOrNull() ?: 0f).toInt() + val w = maxX - minX + val h = maxY - minY + Log.i(OCR_MODULE_NAME, "文本框 $idx 识别结果 = '$text', 坐标 = ($minX, $minY, $w, $h), 置信度 = $conf") + if (text.isNotEmpty()) { + results.add(OcrBlock(text, minX.toFloat(), minY.toFloat(), w.toFloat(), h.toFloat(), conf)) + } + } finally { + crop?.recycle() + recInputTensor?.close() + recOutputs?.close() + } + } + } finally { + if (resized !== bitmap) { + resized?.recycle() + } + detInputTensor?.close() + detOutputs?.close() + } + + return results + } + + // ============== 前处理 ============== + + /** det 前处理:resize → BCHW → normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])。 */ + private fun preprocessDet(bmp: Bitmap): TensorData { + val w = bmp.width + val h = bmp.height + val pixels = IntArray(w * h) + bmp.getPixels(pixels, 0, w, 0, 0, w, h) + val data = FloatArray(3 * w * h) + // BCHW 顺序 + val means = floatArrayOf(0.485f, 0.456f, 0.406f) + val stds = floatArrayOf(0.229f, 0.224f, 0.225f) + for (c in 0..2) { + val mean = means[c] + val std = stds[c] + for (y in 0 until h) { + for (x in 0 until w) { + val px = pixels[y * w + x] + // 提取 R/G/B(c=0→R, 1→G, 2→B) + val channelVal = (px shr (16 - 8 * c)) and 0xFF + data[c * w * h + y * w + x] = (channelVal / 255.0f - mean) / std + } + } + } + return TensorData(data, w, h) + } + + /** rec 前处理:crop → resize 到 48 高(保持宽高比)→ pad 到整除 4 → normalize。 */ + private fun preprocessRec(bmp: Bitmap): TensorData { + var w = bmp.width + val h = bmp.height + // resize 到高度 48,宽度等比缩放 + var resizedW = (w.toFloat() / h * REC_IMAGE_HEIGHT).toInt() + // 宽度上限,避免单行过长爆显存 + resizedW = min(resizedW, REC_MAX_WIDTH) + resizedW = max(resizedW, 1) + val resized = if (resizedW == w && h == REC_IMAGE_HEIGHT) bmp + else Bitmap.createScaledBitmap(bmp, resizedW, REC_IMAGE_HEIGHT, true) + w = resized.width + val pixels = IntArray(w * REC_IMAGE_HEIGHT) + resized.getPixels(pixels, 0, w, 0, 0, w, REC_IMAGE_HEIGHT) + val data = FloatArray(3 * w * REC_IMAGE_HEIGHT) + val means = floatArrayOf(0.5f, 0.5f, 0.5f) + val stds = floatArrayOf(0.5f, 0.5f, 0.5f) + for (c in 0..2) { + val mean = means[c] + val std = stds[c] + for (y in 0 until REC_IMAGE_HEIGHT) { + for (x in 0 until w) { + val px = pixels[y * w + x] + val channelVal = (px shr (16 - 8 * c)) and 0xFF + data[c * w * REC_IMAGE_HEIGHT + y * w + x] = (channelVal / 255.0f - mean) / std + } + } + } + if (resized !== bmp) resized.recycle() + return TensorData(data, w, REC_IMAGE_HEIGHT) + } + + // ============== DB 后处理(简化版) ============== + // 参考 PaddleOCR db_postprocess:sigmoid → threshold → 连通域 → 最小外接矩形 + // 这里用轻量实现:逐像素阈值化后用投影法估框,对常见单/多行账单足够。 + + /** + * DB 后处理:sigmoid + 阈值 0.3 → 二值图 → 连通域外接矩形。 + * 简化版:用水平投影切行 + 垂直投影切列,得到矩形框(对账单类版面够用)。 + */ + private fun dbPostprocess(prob: Array, h: Int, w: Int, ratioX: Float, ratioY: Float): List> { + // 自动检测是否需要 Sigmoid + var minVal = Float.MAX_VALUE + var maxVal = Float.MIN_VALUE + for (y in 0 until h) { + for (x in 0 until w) { + val v = prob[y][x] + if (v < minVal) minVal = v + if (v > maxVal) maxVal = v + } + } + val needSigmoid = minVal < -0.05f || maxVal > 1.05f + Log.i(OCR_MODULE_NAME, "DB prob min=$minVal, max=$maxVal, needSigmoid=$needSigmoid") + + // 过滤状态栏(前8%)和导航栏(后8%),避免其上的干扰字符(如电池、时间、返回键)影响识别或导致行粘连 + val startY = (h * 0.08).toInt() + val endY = (h * 0.92).toInt() + + val binMask = Array(h) { IntArray(w) } + var activeCountTotal = 0 + for (y in 0 until h) { + for (x in 0 until w) { + if (y < startY || y > endY) { + binMask[y][x] = 0 + continue + } + val raw = prob[y][x] + val sig = if (needSigmoid) { + 1.0f / (1.0f + Math.exp(-raw.toDouble()).toFloat()) + } else { + raw + } + val isActive = if (sig > DET_THRESH) 1 else 0 + binMask[y][x] = isActive + if (isActive == 1) activeCountTotal++ + } + } + Log.i(OCR_MODULE_NAME, "二值化完成: 活跃像素 = $activeCountTotal / ${w * h}") + + // 清理垂直干扰线(如滚动条、背景边框线):如果某列在文本有效区域内的活跃像素超过该区域高度的 30%,视为干扰列,整列清零 + val maxColActive = ((endY - startY) * 0.3).toInt() + var clearedColsCount = 0 + for (x in 0 until w) { + var colActive = 0 + for (y in startY..endY) { + if (binMask[y][x] == 1) colActive++ + } + if (colActive > maxColActive) { + clearedColsCount++ + for (y in 0 until h) { + binMask[y][x] = 0 + } + } + } + Log.i(OCR_MODULE_NAME, "垂直线噪清理完成: 清理了 $clearedColsCount / $w 列") + + // 水平投影:按行找文本行 + val rowHits = IntArray(h) + for (y in 0 until h) { + var sum = 0 + for (x in 0 until w) sum += binMask[y][x] + rowHits[y] = sum + } + val minRowWidth = max(1, w / 20) // 一行至少要有这么多像素才算文本 + val rowRanges = mutableListOf() + var inLine = false + var lineStart = 0 + for (y in 0 until h) { + val isText = rowHits[y] >= minRowWidth + if (isText && !inLine) { inLine = true; lineStart = y } + else if (!isText && inLine) { + rowRanges.add(intArrayOf(lineStart, y - 1)) + inLine = false + } + } + if (inLine) rowRanges.add(intArrayOf(lineStart, h - 1)) + Log.i(OCR_MODULE_NAME, "dbPostprocess 水平分割完成,找到行Ranges数 = ${rowRanges.size}") + + val boxes = mutableListOf>() + // 对每行做垂直投影切列(账单每行通常是连续一段或多段) + for ((y0, y1) in rowRanges.map { it[0] to it[1] }) { + val colHits = IntArray(w) + for (x in 0 until w) { + var sum = 0 + for (y in y0..y1) sum += binMask[y][x] + colHits[x] = sum + } + val minColHeight = max(1, (y1 - y0 + 1) / 12) + var inSeg = false + var segStart = 0 + var segs = mutableListOf() + for (x in 0 until w) { + val isText = colHits[x] >= minColHeight + if (isText && !inSeg) { inSeg = true; segStart = x } + else if (!isText && inSeg) { + // 合并间隔很近的段 + if (segs.isNotEmpty() && segStart - segs.last()[1] < DET_MERGE_GAP) { + segs.last()[1] = x - 1 + } else { + segs.add(intArrayOf(segStart, x - 1)) + } + inSeg = false + } + } + if (inSeg) { + if (segs.isNotEmpty() && (w - 1) - segs.last()[1] < DET_MERGE_GAP) { + segs.last()[1] = w - 1 + } else { + segs.add(intArrayOf(segStart, w - 1)) + } + } + for ((x0, x1) in segs.map { it[0] to it[1] }) { + // 过滤过小的框 + val boxW = x1 - x0 + 1 + val boxH = y1 - y0 + 1 + if (boxW < 4 || boxH < 2) continue + // 映射回原图坐标(4 个角点) + val fx0 = x0 * ratioX + val fx1 = x1 * ratioX + val fy0 = y0 * ratioY + val fy1 = y1 * ratioY + boxes.add(listOf( + floatArrayOf(fx0, fy0), + floatArrayOf(fx1, fy0), + floatArrayOf(fx1, fy1), + floatArrayOf(fx0, fy1), + )) + } + } + return boxes + } + + // ============== CTC 解码 ============== + + /** + * CTC greedy decode:每个时间步取 argmax,去 blank 去重复。返回 (text, avgConfidence)。 + * + * PaddleOCR 约定:logits 的 index 0 固定是 blank,字符从 index 1 起, + * dictionary[i] 对应模型输出 index i+1。因此 dictIdx = argmaxIdx - 1。 + * 已用 onnxruntime 实证:argmax 序列中 0 占多数(即 blank),真实字符索引 + * (如 90→'支')按 idx-1 映射到 dictionary 即可正确还原中文。 + */ + private fun ctcGreedyDecode(logits: Array): Pair { + if (logits.isEmpty()) return "" to 0f + val numClasses = logits[0].size + val blankIdx = 0 // PaddleOCR CTC:blank 固定在 index 0 + + val sb = StringBuilder() + var lastIdx = -1 + var confSum = 0.0f + var confCount = 0 + for (t in logits.indices) { + var maxIdx = 0 + var maxVal = logits[t][0] + for (i in 1 until numClasses) { + if (logits[t][i] > maxVal) { maxVal = logits[t][i]; maxIdx = i } + } + // softmax 概率(用于置信度统计) + var expSum = 0.0 + for (i in 0 until numClasses) expSum += Math.exp(logits[t][i].toDouble()) + val prob = Math.exp(maxVal.toDouble()) / expSum + + if (maxIdx != blankIdx && maxIdx != lastIdx) { + val dictIdx = maxIdx - 1 // index 1..N → dictionary[0..N-1] + if (dictIdx in 0 until dictionary.size) { + sb.append(dictionary[dictIdx]) + confSum += prob.toFloat() + confCount++ + } + } + lastIdx = maxIdx + } + val avgConf = if (confCount > 0) confSum / confCount else 0f + return sb.toString() to avgConf + } + + // ============== Bitmap 工具 ============== + + private fun cropBox(bmp: Bitmap, box: List): Bitmap? { + val xs = box.map { it[0] } + val ys = box.map { it[1] } + val paddingX = 4 + val paddingY = 2 + val minX = max(0, (xs.minOrNull() ?: 0f).toInt() - paddingX) + val minY = max(0, (ys.minOrNull() ?: 0f).toInt() - paddingY) + val maxX = min(bmp.width, ((xs.maxOrNull() ?: 0f) + 1).toInt() + paddingX) + val maxY = min(bmp.height, ((ys.maxOrNull() ?: 0f) + 1).toInt() + paddingY) + val w = maxX - minX + val h = maxY - minY + if (w < 2 || h < 2) return null + return Bitmap.createBitmap(bmp, minX, minY, w, h) + } + + private fun resizeForDet(bmp: Bitmap, maxSide: Int): Bitmap { + val ratio = maxSide.toFloat() / max(bmp.width, bmp.height) + if (ratio >= 1f) return bmp + val newW = (bmp.width * ratio).toInt() + val newH = (bmp.height * ratio).toInt() + // 确保尺寸是 32 的倍数(det 模型下采样要求) + val alignedW = (newW / 32) * 32 + val alignedH = (newH / 32) * 32 + if (alignedW < 32 || alignedH < 32) return bmp + return Bitmap.createScaledBitmap(bmp, alignedW, alignedH, true) + } + + // ============== 公共工具(复用) ============== + + /** 解码 base64 图片为 Bitmap。 */ + private fun decodeBase64(base64: String): Bitmap? { + return try { + // 去除 data:image/...;base64, 前缀 + val data = if (base64.contains(",")) base64.substringAfter(",") else base64 + val bytes = Base64.decode(data, Base64.DEFAULT) + BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + } catch (e: Exception) { + Log.e(OCR_MODULE_NAME, "base64 解码失败: ${e.message}") + null + } + } + + /** + * 短边压缩到 maxShortEdge(参考 AutoAccounting scaleDownForOcr)。 + * 像素量比 1440p 减少约 75%,识别速度大幅提升。 + */ + private fun scaleDownForOcr(bitmap: Bitmap, maxShortEdge: Int): Bitmap { + val width = bitmap.width + val height = bitmap.height + val shortEdge = minOf(width, height) + if (shortEdge <= maxShortEdge) return bitmap + val scale = maxShortEdge.toFloat() / shortEdge + val newWidth = (width * scale).toInt() + val newHeight = (height * scale).toInt() + return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + } + + override fun onCatalystInstanceDestroy() { + super.onCatalystInstanceDestroy() + release() + } + + override fun invalidate() { + super.invalidate() + release() + } + + private fun release() { + lock.lock() + try { + detSession?.close() + recSession?.close() + // OrtEnvironment 是单例,不主动 close(进程级) + detSession = null + recSession = null + ortEnv = null + initialized = false + } catch (_: Exception) { + } finally { + lock.unlock() + } + } + + /** 识别结果块。 */ + private data class OcrBlock(val text: String, val x: Float, val y: Float, val width: Float, val height: Float, val confidence: Float) + /** 预处理后的张量数据 + 宽高。 */ + private data class TensorData(val data: FloatArray, val w: Int, val h: Int) + + companion object { + /** OCR 最大短边(参考 AutoAccounting OCR_MAX_SHORT_EDGE)。 */ + private const val OCR_MAX_SHORT_EDGE = 720 + /** det resize 最大边(PaddleOCR limit_max_side_len 默认值)。 */ + private const val DET_LIMIT_MAX_SIDE = 960 + /** DB 二值化阈值。 */ + private const val DET_THRESH = 0.3f + /** 投影法合并相邻文本段的间隔(像素)。 */ + private const val DET_MERGE_GAP = 10 + /** rec 固定图像高度。 */ + private const val REC_IMAGE_HEIGHT = 48 + /** rec 单行最大宽度。 */ + private const val REC_MAX_WIDTH = 320 + /** assets 中的模型/字典文件名。 */ + private const val ASSET_DET_MODEL = "ppocrv5_det.onnx" + private const val ASSET_REC_MODEL = "ppocrv5_rec.onnx" + // PP-OCRv5 多语言识别模型的配套字典(18383 字符 + 运行时 1 blank = 18385 维输出)。 + // 注意:必须与 rec 模型配套,错用旧版 ppocr_keys_v1.txt(6623)会导致 CTC 解码乱码。 + private const val ASSET_DICT = "ppocrv5_dict.txt" + } +} diff --git a/plugins/ppocr/android/OcrPackage.kt b/plugins/ppocr/android/OcrPackage.kt new file mode 100644 index 0000000..b6f80c5 --- /dev/null +++ b/plugins/ppocr/android/OcrPackage.kt @@ -0,0 +1,24 @@ +package com.beancount.mobile.ppocr + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager + +/** + * 注册 OcrModule 到 React Native 的 Package(plan.md「3.2 Config Plugin」)。 + * + * 由 app.plugin.js 的 withMainApplication 注入到 MainApplication.getPackages() 列表。 + * RN 在启动时遍历所有 Package,调用 createNativeModules 注册原生模块。 + */ +class OcrPackage : ReactPackage { + override fun createNativeModules(rc: ReactApplicationContext): List { + return listOf(OcrModule(rc)) + } + + override fun createViewManagers(rc: ReactApplicationContext): List>> { + return emptyList() + } +} diff --git a/plugins/ppocr/app.plugin.js b/plugins/ppocr/app.plugin.js index c5cb5b9..004684b 100644 --- a/plugins/ppocr/app.plugin.js +++ b/plugins/ppocr/app.plugin.js @@ -103,11 +103,10 @@ function withPpOcr(config) { config = withAppBuildGradle(config, (modConfig) => { let gradle = modConfig.modResults.contents; if (!gradle.includes('onnxruntime')) { - // 在 dependencies { ... } 块末尾追加 + // 在 dependencies { 开头处插入,避免嵌套花括号的正则匹配错误 gradle = gradle.replace( - /dependencies\s*{([\s\S]*?)^\s*}/m, - (m, inner) => - `dependencies {${inner}\n // PP-OCRv5 ONNX Runtime(由 Config Plugin 注入)\n implementation 'com.microsoft.onnxruntime:onnxruntime-android:1.20.0'\n}`, + /(dependencies\s*\{)/, + `$1\n // PP-OCRv5 ONNX Runtime(由 Config Plugin 注入)\n implementation 'com.microsoft.onnxruntime:onnxruntime-android:1.20.0'` ); } modConfig.modResults.contents = gradle; diff --git a/plugins/ppocr/assets/ppocr_keys_v1.txt b/plugins/ppocr/assets/ppocr_keys_v1.txt deleted file mode 100644 index 84b885d..0000000 --- a/plugins/ppocr/assets/ppocr_keys_v1.txt +++ /dev/null @@ -1,6623 +0,0 @@ -' -疗 -绚 -诚 -娇 -溜 -题 -贿 -者 -廖 -更 -纳 -加 -奉 -公 -一 -就 -汴 -计 -与 -路 -房 -原 -妇 -2 -0 -8 -- -7 -其 -> -: -] -, -, -骑 -刈 -全 -消 -昏 -傈 -安 -久 -钟 -嗅 -不 -影 -处 -驽 -蜿 -资 -关 -椤 -地 -瘸 -专 -问 -忖 -票 -嫉 -炎 -韵 -要 -月 -田 -节 -陂 -鄙 -捌 -备 -拳 -伺 -眼 -网 -盎 -大 -傍 -心 -东 -愉 -汇 -蹿 -科 -每 -业 -里 -航 -晏 -字 -平 -录 -先 -1 -3 -彤 -鲶 -产 -稍 -督 -腴 -有 -象 -岳 -注 -绍 -在 -泺 -文 -定 -核 -名 -水 -过 -理 -让 -偷 -率 -等 -这 -发 -” -为 -含 -肥 -酉 -相 -鄱 -七 -编 -猥 -锛 -日 -镀 -蒂 -掰 -倒 -辆 -栾 -栗 -综 -涩 -州 -雌 -滑 -馀 -了 -机 -块 -司 -宰 -甙 -兴 -矽 -抚 -保 -用 -沧 -秩 -如 -收 -息 -滥 -页 -疑 -埠 -! -! -姥 -异 -橹 -钇 -向 -下 -跄 -的 -椴 -沫 -国 -绥 -獠 -报 -开 -民 -蜇 -何 -分 -凇 -长 -讥 -藏 -掏 -施 -羽 -中 -讲 -派 -嘟 -人 -提 -浼 -间 -世 -而 -古 -多 -倪 -唇 -饯 -控 -庚 -首 -赛 -蜓 -味 -断 -制 -觉 -技 -替 -艰 -溢 -潮 -夕 -钺 -外 -摘 -枋 -动 -双 -单 -啮 -户 -枇 -确 -锦 -曜 -杜 -或 -能 -效 -霜 -盒 -然 -侗 -电 -晁 -放 -步 -鹃 -新 -杖 -蜂 -吒 -濂 -瞬 -评 -总 -隍 -对 -独 -合 -也 -是 -府 -青 -天 -诲 -墙 -组 -滴 -级 -邀 -帘 -示 -已 -时 -骸 -仄 -泅 -和 -遨 -店 -雇 -疫 -持 -巍 -踮 -境 -只 -亨 -目 -鉴 -崤 -闲 -体 -泄 -杂 -作 -般 -轰 -化 -解 -迂 -诿 -蛭 -璀 -腾 -告 -版 -服 -省 -师 -小 -规 -程 -线 -海 -办 -引 -二 -桧 -牌 -砺 -洄 -裴 -修 -图 -痫 -胡 -许 -犊 -事 -郛 -基 -柴 -呼 -食 -研 -奶 -律 -蛋 -因 -葆 -察 -戏 -褒 -戒 -再 -李 -骁 -工 -貂 -油 -鹅 -章 -啄 -休 -场 -给 -睡 -纷 -豆 -器 -捎 -说 -敏 -学 -会 -浒 -设 -诊 -格 -廓 -查 -来 -霓 -室 -溆 -¢ -诡 -寥 -焕 -舜 -柒 -狐 -回 -戟 -砾 -厄 -实 -翩 -尿 -五 -入 -径 -惭 -喹 -股 -宇 -篝 -| -; -美 -期 -云 -九 -祺 -扮 -靠 -锝 -槌 -系 -企 -酰 -阊 -暂 -蚕 -忻 -豁 -本 -羹 -执 -条 -钦 -H -獒 -限 -进 -季 -楦 -于 -芘 -玖 -铋 -茯 -未 -答 -粘 -括 -样 -精 -欠 -矢 -甥 -帷 -嵩 -扣 -令 -仔 -风 -皈 -行 -支 -部 -蓉 -刮 -站 -蜡 -救 -钊 -汗 -松 -嫌 -成 -可 -. -鹤 -院 -从 -交 -政 -怕 -活 -调 -球 -局 -验 -髌 -第 -韫 -谗 -串 -到 -圆 -年 -米 -/ -* -友 -忿 -检 -区 -看 -自 -敢 -刃 -个 -兹 -弄 -流 -留 -同 -没 -齿 -星 -聆 -轼 -湖 -什 -三 -建 -蛔 -儿 -椋 -汕 -震 -颧 -鲤 -跟 -力 -情 -璺 -铨 -陪 -务 -指 -族 -训 -滦 -鄣 -濮 -扒 -商 -箱 -十 -召 -慷 -辗 -所 -莞 -管 -护 -臭 -横 -硒 -嗓 -接 -侦 -六 -露 -党 -馋 -驾 -剖 -高 -侬 -妪 -幂 -猗 -绺 -骐 -央 -酐 -孝 -筝 -课 -徇 -缰 -门 -男 -西 -项 -句 -谙 -瞒 -秃 -篇 -教 -碲 -罚 -声 -呐 -景 -前 -富 -嘴 -鳌 -稀 -免 -朋 -啬 -睐 -去 -赈 -鱼 -住 -肩 -愕 -速 -旁 -波 -厅 -健 -茼 -厥 -鲟 -谅 -投 -攸 -炔 -数 -方 -击 -呋 -谈 -绩 -别 -愫 -僚 -躬 -鹧 -胪 -炳 -招 -喇 -膨 -泵 -蹦 -毛 -结 -5 -4 -谱 -识 -陕 -粽 -婚 -拟 -构 -且 -搜 -任 -潘 -比 -郢 -妨 -醪 -陀 -桔 -碘 -扎 -选 -哈 -骷 -楷 -亿 -明 -缆 -脯 -监 -睫 -逻 -婵 -共 -赴 -淝 -凡 -惦 -及 -达 -揖 -谩 -澹 -减 -焰 -蛹 -番 -祁 -柏 -员 -禄 -怡 -峤 -龙 -白 -叽 -生 -闯 -起 -细 -装 -谕 -竟 -聚 -钙 -上 -导 -渊 -按 -艾 -辘 -挡 -耒 -盹 -饪 -臀 -记 -邮 -蕙 -受 -各 -医 -搂 -普 -滇 -朗 -茸 -带 -翻 -酚 -( -光 -堤 -墟 -蔷 -万 -幻 -〓 -瑙 -辈 -昧 -盏 -亘 -蛀 -吉 -铰 -请 -子 -假 -闻 -税 -井 -诩 -哨 -嫂 -好 -面 -琐 -校 -馊 -鬣 -缂 -营 -访 -炖 -占 -农 -缀 -否 -经 -钚 -棵 -趟 -张 -亟 -吏 -茶 -谨 -捻 -论 -迸 -堂 -玉 -信 -吧 -瞠 -乡 -姬 -寺 -咬 -溏 -苄 -皿 -意 -赉 -宝 -尔 -钰 -艺 -特 -唳 -踉 -都 -荣 -倚 -登 -荐 -丧 -奇 -涵 -批 -炭 -近 -符 -傩 -感 -道 -着 -菊 -虹 -仲 -众 -懈 -濯 -颞 -眺 -南 -释 -北 -缝 -标 -既 -茗 -整 -撼 -迤 -贲 -挎 -耱 -拒 -某 -妍 -卫 -哇 -英 -矶 -藩 -治 -他 -元 -领 -膜 -遮 -穗 -蛾 -飞 -荒 -棺 -劫 -么 -市 -火 -温 -拈 -棚 -洼 -转 -果 -奕 -卸 -迪 -伸 -泳 -斗 -邡 -侄 -涨 -屯 -萋 -胭 -氡 -崮 -枞 -惧 -冒 -彩 -斜 -手 -豚 -随 -旭 -淑 -妞 -形 -菌 -吲 -沱 -争 -驯 -歹 -挟 -兆 -柱 -传 -至 -包 -内 -响 -临 -红 -功 -弩 -衡 -寂 -禁 -老 -棍 -耆 -渍 -织 -害 -氵 -渑 -布 -载 -靥 -嗬 -虽 -苹 -咨 -娄 -库 -雉 -榜 -帜 -嘲 -套 -瑚 -亲 -簸 -欧 -边 -6 -腿 -旮 -抛 -吹 -瞳 -得 -镓 -梗 -厨 -继 -漾 -愣 -憨 -士 -策 -窑 -抑 -躯 -襟 -脏 -参 -贸 -言 -干 -绸 -鳄 -穷 -藜 -音 -折 -详 -) -举 -悍 -甸 -癌 -黎 -谴 -死 -罩 -迁 -寒 -驷 -袖 -媒 -蒋 -掘 -模 -纠 -恣 -观 -祖 -蛆 -碍 -位 -稿 -主 -澧 -跌 -筏 -京 -锏 -帝 -贴 -证 -糠 -才 -黄 -鲸 -略 -炯 -饱 -四 -出 -园 -犀 -牧 -容 -汉 -杆 -浈 -汰 -瑷 -造 -虫 -瘩 -怪 -驴 -济 -应 -花 -沣 -谔 -夙 -旅 -价 -矿 -以 -考 -s -u -呦 -晒 -巡 -茅 -准 -肟 -瓴 -詹 -仟 -褂 -译 -桌 -混 -宁 -怦 -郑 -抿 -些 -余 -鄂 -饴 -攒 -珑 -群 -阖 -岔 -琨 -藓 -预 -环 -洮 -岌 -宀 -杲 -瀵 -最 -常 -囡 -周 -踊 -女 -鼓 -袭 -喉 -简 -范 -薯 -遐 -疏 -粱 -黜 -禧 -法 -箔 -斤 -遥 -汝 -奥 -直 -贞 -撑 -置 -绱 -集 -她 -馅 -逗 -钧 -橱 -魉 -[ -恙 -躁 -唤 -9 -旺 -膘 -待 -脾 -惫 -购 -吗 -依 -盲 -度 -瘿 -蠖 -俾 -之 -镗 -拇 -鲵 -厝 -簧 -续 -款 -展 -啃 -表 -剔 -品 -钻 -腭 -损 -清 -锶 -统 -涌 -寸 -滨 -贪 -链 -吠 -冈 -伎 -迥 -咏 -吁 -览 -防 -迅 -失 -汾 -阔 -逵 -绀 -蔑 -列 -川 -凭 -努 -熨 -揪 -利 -俱 -绉 -抢 -鸨 -我 -即 -责 -膦 -易 -毓 -鹊 -刹 -玷 -岿 -空 -嘞 -绊 -排 -术 -估 -锷 -违 -们 -苟 -铜 -播 -肘 -件 -烫 -审 -鲂 -广 -像 -铌 -惰 -铟 -巳 -胍 -鲍 -康 -憧 -色 -恢 -想 -拷 -尤 -疳 -知 -S -Y -F -D -A -峄 -裕 -帮 -握 -搔 -氐 -氘 -难 -墒 -沮 -雨 -叁 -缥 -悴 -藐 -湫 -娟 -苑 -稠 -颛 -簇 -后 -阕 -闭 -蕤 -缚 -怎 -佞 -码 -嘤 -蔡 -痊 -舱 -螯 -帕 -赫 -昵 -升 -烬 -岫 -、 -疵 -蜻 -髁 -蕨 -隶 -烛 -械 -丑 -盂 -梁 -强 -鲛 -由 -拘 -揉 -劭 -龟 -撤 -钩 -呕 -孛 -费 -妻 -漂 -求 -阑 -崖 -秤 -甘 -通 -深 -补 -赃 -坎 -床 -啪 -承 -吼 -量 -暇 -钼 -烨 -阂 -擎 -脱 -逮 -称 -P -神 -属 -矗 -华 -届 -狍 -葑 -汹 -育 -患 -窒 -蛰 -佼 -静 -槎 -运 -鳗 -庆 -逝 -曼 -疱 -克 -代 -官 -此 -麸 -耧 -蚌 -晟 -例 -础 -榛 -副 -测 -唰 -缢 -迹 -灬 -霁 -身 -岁 -赭 -扛 -又 -菡 -乜 -雾 -板 -读 -陷 -徉 -贯 -郁 -虑 -变 -钓 -菜 -圾 -现 -琢 -式 -乐 -维 -渔 -浜 -左 -吾 -脑 -钡 -警 -T -啵 -拴 -偌 -漱 -湿 -硕 -止 -骼 -魄 -积 -燥 -联 -踢 -玛 -则 -窿 -见 -振 -畿 -送 -班 -钽 -您 -赵 -刨 -印 -讨 -踝 -籍 -谡 -舌 -崧 -汽 -蔽 -沪 -酥 -绒 -怖 -财 -帖 -肱 -私 -莎 -勋 -羔 -霸 -励 -哼 -帐 -将 -帅 -渠 -纪 -婴 -娩 -岭 -厘 -滕 -吻 -伤 -坝 -冠 -戊 -隆 -瘁 -介 -涧 -物 -黍 -并 -姗 -奢 -蹑 -掣 -垸 -锴 -命 -箍 -捉 -病 -辖 -琰 -眭 -迩 -艘 -绌 -繁 -寅 -若 -毋 -思 -诉 -类 -诈 -燮 -轲 -酮 -狂 -重 -反 -职 -筱 -县 -委 -磕 -绣 -奖 -晋 -濉 -志 -徽 -肠 -呈 -獐 -坻 -口 -片 -碰 -几 -村 -柿 -劳 -料 -获 -亩 -惕 -晕 -厌 -号 -罢 -池 -正 -鏖 -煨 -家 -棕 -复 -尝 -懋 -蜥 -锅 -岛 -扰 -队 -坠 -瘾 -钬 -@ -卧 -疣 -镇 -譬 -冰 -彷 -频 -黯 -据 -垄 -采 -八 -缪 -瘫 -型 -熹 -砰 -楠 -襁 -箐 -但 -嘶 -绳 -啤 -拍 -盥 -穆 -傲 -洗 -盯 -塘 -怔 -筛 -丿 -台 -恒 -喂 -葛 -永 -¥ -烟 -酒 -桦 -书 -砂 -蚝 -缉 -态 -瀚 -袄 -圳 -轻 -蛛 -超 -榧 -遛 -姒 -奘 -铮 -右 -荽 -望 -偻 -卡 -丶 -氰 -附 -做 -革 -索 -戚 -坨 -桷 -唁 -垅 -榻 -岐 -偎 -坛 -莨 -山 -殊 -微 -骇 -陈 -爨 -推 -嗝 -驹 -澡 -藁 -呤 -卤 -嘻 -糅 -逛 -侵 -郓 -酌 -德 -摇 -※ -鬃 -被 -慨 -殡 -羸 -昌 -泡 -戛 -鞋 -河 -宪 -沿 -玲 -鲨 -翅 -哽 -源 -铅 -语 -照 -邯 -址 -荃 -佬 -顺 -鸳 -町 -霭 -睾 -瓢 -夸 -椁 -晓 -酿 -痈 -咔 -侏 -券 -噎 -湍 -签 -嚷 -离 -午 -尚 -社 -锤 -背 -孟 -使 -浪 -缦 -潍 -鞅 -军 -姹 -驶 -笑 -鳟 -鲁 -》 -孽 -钜 -绿 -洱 -礴 -焯 -椰 -颖 -囔 -乌 -孔 -巴 -互 -性 -椽 -哞 -聘 -昨 -早 -暮 -胶 -炀 -隧 -低 -彗 -昝 -铁 -呓 -氽 -藉 -喔 -癖 -瑗 -姨 -权 -胱 -韦 -堑 -蜜 -酋 -楝 -砝 -毁 -靓 -歙 -锲 -究 -屋 -喳 -骨 -辨 -碑 -武 -鸠 -宫 -辜 -烊 -适 -坡 -殃 -培 -佩 -供 -走 -蜈 -迟 -翼 -况 -姣 -凛 -浔 -吃 -飘 -债 -犟 -金 -促 -苛 -崇 -坂 -莳 -畔 -绂 -兵 -蠕 -斋 -根 -砍 -亢 -欢 -恬 -崔 -剁 -餐 -榫 -快 -扶 -‖ -濒 -缠 -鳜 -当 -彭 -驭 -浦 -篮 -昀 -锆 -秸 -钳 -弋 -娣 -瞑 -夷 -龛 -苫 -拱 -致 -% -嵊 -障 -隐 -弑 -初 -娓 -抉 -汩 -累 -蓖 -" -唬 -助 -苓 -昙 -押 -毙 -破 -城 -郧 -逢 -嚏 -獭 -瞻 -溱 -婿 -赊 -跨 -恼 -璧 -萃 -姻 -貉 -灵 -炉 -密 -氛 -陶 -砸 -谬 -衔 -点 -琛 -沛 -枳 -层 -岱 -诺 -脍 -榈 -埂 -征 -冷 -裁 -打 -蹴 -素 -瘘 -逞 -蛐 -聊 -激 -腱 -萘 -踵 -飒 -蓟 -吆 -取 -咙 -簋 -涓 -矩 -曝 -挺 -揣 -座 -你 -史 -舵 -焱 -尘 -苏 -笈 -脚 -溉 -榨 -诵 -樊 -邓 -焊 -义 -庶 -儋 -蟋 -蒲 -赦 -呷 -杞 -诠 -豪 -还 -试 -颓 -茉 -太 -除 -紫 -逃 -痴 -草 -充 -鳕 -珉 -祗 -墨 -渭 -烩 -蘸 -慕 -璇 -镶 -穴 -嵘 -恶 -骂 -险 -绋 -幕 -碉 -肺 -戳 -刘 -潞 -秣 -纾 -潜 -銮 -洛 -须 -罘 -销 -瘪 -汞 -兮 -屉 -r -林 -厕 -质 -探 -划 -狸 -殚 -善 -煊 -烹 -〒 -锈 -逯 -宸 -辍 -泱 -柚 -袍 -远 -蹋 -嶙 -绝 -峥 -娥 -缍 -雀 -徵 -认 -镱 -谷 -= -贩 -勉 -撩 -鄯 -斐 -洋 -非 -祚 -泾 -诒 -饿 -撬 -威 -晷 -搭 -芍 -锥 -笺 -蓦 -候 -琊 -档 -礁 -沼 -卵 -荠 -忑 -朝 -凹 -瑞 -头 -仪 -弧 -孵 -畏 -铆 -突 -衲 -车 -浩 -气 -茂 -悖 -厢 -枕 -酝 -戴 -湾 -邹 -飚 -攘 -锂 -写 -宵 -翁 -岷 -无 -喜 -丈 -挑 -嗟 -绛 -殉 -议 -槽 -具 -醇 -淞 -笃 -郴 -阅 -饼 -底 -壕 -砚 -弈 -询 -缕 -庹 -翟 -零 -筷 -暨 -舟 -闺 -甯 -撞 -麂 -茌 -蔼 -很 -珲 -捕 -棠 -角 -阉 -媛 -娲 -诽 -剿 -尉 -爵 -睬 -韩 -诰 -匣 -危 -糍 -镯 -立 -浏 -阳 -少 -盆 -舔 -擘 -匪 -申 -尬 -铣 -旯 -抖 -赘 -瓯 -居 -ˇ -哮 -游 -锭 -茏 -歌 -坏 -甚 -秒 -舞 -沙 -仗 -劲 -潺 -阿 -燧 -郭 -嗖 -霏 -忠 -材 -奂 -耐 -跺 -砀 -输 -岖 -媳 -氟 -极 -摆 -灿 -今 -扔 -腻 -枝 -奎 -药 -熄 -吨 -话 -q -额 -慑 -嘌 -协 -喀 -壳 -埭 -视 -著 -於 -愧 -陲 -翌 -峁 -颅 -佛 -腹 -聋 -侯 -咎 -叟 -秀 -颇 -存 -较 -罪 -哄 -岗 -扫 -栏 -钾 -羌 -己 -璨 -枭 -霉 -煌 -涸 -衿 -键 -镝 -益 -岢 -奏 -连 -夯 -睿 -冥 -均 -糖 -狞 -蹊 -稻 -爸 -刿 -胥 -煜 -丽 -肿 -璃 -掸 -跚 -灾 -垂 -樾 -濑 -乎 -莲 -窄 -犹 -撮 -战 -馄 -软 -络 -显 -鸢 -胸 -宾 -妲 -恕 -埔 -蝌 -份 -遇 -巧 -瞟 -粒 -恰 -剥 -桡 -博 -讯 -凯 -堇 -阶 -滤 -卖 -斌 -骚 -彬 -兑 -磺 -樱 -舷 -两 -娱 -福 -仃 -差 -找 -桁 -÷ -净 -把 -阴 -污 -戬 -雷 -碓 -蕲 -楚 -罡 -焖 -抽 -妫 -咒 -仑 -闱 -尽 -邑 -菁 -爱 -贷 -沥 -鞑 -牡 -嗉 -崴 -骤 -塌 -嗦 -订 -拮 -滓 -捡 -锻 -次 -坪 -杩 -臃 -箬 -融 -珂 -鹗 -宗 -枚 -降 -鸬 -妯 -阄 -堰 -盐 -毅 -必 -杨 -崃 -俺 -甬 -状 -莘 -货 -耸 -菱 -腼 -铸 -唏 -痤 -孚 -澳 -懒 -溅 -翘 -疙 -杷 -淼 -缙 -骰 -喊 -悉 -砻 -坷 -艇 -赁 -界 -谤 -纣 -宴 -晃 -茹 -归 -饭 -梢 -铡 -街 -抄 -肼 -鬟 -苯 -颂 -撷 -戈 -炒 -咆 -茭 -瘙 -负 -仰 -客 -琉 -铢 -封 -卑 -珥 -椿 -镧 -窨 -鬲 -寿 -御 -袤 -铃 -萎 -砖 -餮 -脒 -裳 -肪 -孕 -嫣 -馗 -嵇 -恳 -氯 -江 -石 -褶 -冢 -祸 -阻 -狈 -羞 -银 -靳 -透 -咳 -叼 -敷 -芷 -啥 -它 -瓤 -兰 -痘 -懊 -逑 -肌 -往 -捺 -坊 -甩 -呻 -〃 -沦 -忘 -膻 -祟 -菅 -剧 -崆 -智 -坯 -臧 -霍 -墅 -攻 -眯 -倘 -拢 -骠 -铐 -庭 -岙 -瓠 -′ -缺 -泥 -迢 -捶 -? -? -郏 -喙 -掷 -沌 -纯 -秘 -种 -听 -绘 -固 -螨 -团 -香 -盗 -妒 -埚 -蓝 -拖 -旱 -荞 -铀 -血 -遏 -汲 -辰 -叩 -拽 -幅 -硬 -惶 -桀 -漠 -措 -泼 -唑 -齐 -肾 -念 -酱 -虚 -屁 -耶 -旗 -砦 -闵 -婉 -馆 -拭 -绅 -韧 -忏 -窝 -醋 -葺 -顾 -辞 -倜 -堆 -辋 -逆 -玟 -贱 -疾 -董 -惘 -倌 -锕 -淘 -嘀 -莽 -俭 -笏 -绑 -鲷 -杈 -择 -蟀 -粥 -嗯 -驰 -逾 -案 -谪 -褓 -胫 -哩 -昕 -颚 -鲢 -绠 -躺 -鹄 -崂 -儒 -俨 -丝 -尕 -泌 -啊 -萸 -彰 -幺 -吟 -骄 -苣 -弦 -脊 -瑰 -〈 -诛 -镁 -析 -闪 -剪 -侧 -哟 -框 -螃 -守 -嬗 -燕 -狭 -铈 -缮 -概 -迳 -痧 -鲲 -俯 -售 -笼 -痣 -扉 -挖 -满 -咋 -援 -邱 -扇 -歪 -便 -玑 -绦 -峡 -蛇 -叨 -〖 -泽 -胃 -斓 -喋 -怂 -坟 -猪 -该 -蚬 -炕 -弥 -赞 -棣 -晔 -娠 -挲 -狡 -创 -疖 -铕 -镭 -稷 -挫 -弭 -啾 -翔 -粉 -履 -苘 -哦 -楼 -秕 -铂 -土 -锣 -瘟 -挣 -栉 -习 -享 -桢 -袅 -磨 -桂 -谦 -延 -坚 -蔚 -噗 -署 -谟 -猬 -钎 -恐 -嬉 -雒 -倦 -衅 -亏 -璩 -睹 -刻 -殿 -王 -算 -雕 -麻 -丘 -柯 -骆 -丸 -塍 -谚 -添 -鲈 -垓 -桎 -蚯 -芥 -予 -飕 -镦 -谌 -窗 -醚 -菀 -亮 -搪 -莺 -蒿 -羁 -足 -J -真 -轶 -悬 -衷 -靛 -翊 -掩 -哒 -炅 -掐 -冼 -妮 -l -谐 -稚 -荆 -擒 -犯 -陵 -虏 -浓 -崽 -刍 -陌 -傻 -孜 -千 -靖 -演 -矜 -钕 -煽 -杰 -酗 -渗 -伞 -栋 -俗 -泫 -戍 -罕 -沾 -疽 -灏 -煦 -芬 -磴 -叱 -阱 -榉 -湃 -蜀 -叉 -醒 -彪 -租 -郡 -篷 -屎 -良 -垢 -隗 -弱 -陨 -峪 -砷 -掴 -颁 -胎 -雯 -绵 -贬 -沐 -撵 -隘 -篙 -暖 -曹 -陡 -栓 -填 -臼 -彦 -瓶 -琪 -潼 -哪 -鸡 -摩 -啦 -俟 -锋 -域 -耻 -蔫 -疯 -纹 -撇 -毒 -绶 -痛 -酯 -忍 -爪 -赳 -歆 -嘹 -辕 -烈 -册 -朴 -钱 -吮 -毯 -癜 -娃 -谀 -邵 -厮 -炽 -璞 -邃 -丐 -追 -词 -瓒 -忆 -轧 -芫 -谯 -喷 -弟 -半 -冕 -裙 -掖 -墉 -绮 -寝 -苔 -势 -顷 -褥 -切 -衮 -君 -佳 -嫒 -蚩 -霞 -佚 -洙 -逊 -镖 -暹 -唛 -& -殒 -顶 -碗 -獗 -轭 -铺 -蛊 -废 -恹 -汨 -崩 -珍 -那 -杵 -曲 -纺 -夏 -薰 -傀 -闳 -淬 -姘 -舀 -拧 -卷 -楂 -恍 -讪 -厩 -寮 -篪 -赓 -乘 -灭 -盅 -鞣 -沟 -慎 -挂 -饺 -鼾 -杳 -树 -缨 -丛 -絮 -娌 -臻 -嗳 -篡 -侩 -述 -衰 -矛 -圈 -蚜 -匕 -筹 -匿 -濞 -晨 -叶 -骋 -郝 -挚 -蚴 -滞 -增 -侍 -描 -瓣 -吖 -嫦 -蟒 -匾 -圣 -赌 -毡 -癞 -恺 -百 -曳 -需 -篓 -肮 -庖 -帏 -卿 -驿 -遗 -蹬 -鬓 -骡 -歉 -芎 -胳 -屐 -禽 -烦 -晌 -寄 -媾 -狄 -翡 -苒 -船 -廉 -终 -痞 -殇 -々 -畦 -饶 -改 -拆 -悻 -萄 -£ -瓿 -乃 -訾 -桅 -匮 -溧 -拥 -纱 -铍 -骗 -蕃 -龋 -缬 -父 -佐 -疚 -栎 -醍 -掳 -蓄 -x -惆 -颜 -鲆 -榆 -〔 -猎 -敌 -暴 -谥 -鲫 -贾 -罗 -玻 -缄 -扦 -芪 -癣 -落 -徒 -臾 -恿 -猩 -托 -邴 -肄 -牵 -春 -陛 -耀 -刊 -拓 -蓓 -邳 -堕 -寇 -枉 -淌 -啡 -湄 -兽 -酷 -萼 -碚 -濠 -萤 -夹 -旬 -戮 -梭 -琥 -椭 -昔 -勺 -蜊 -绐 -晚 -孺 -僵 -宣 -摄 -冽 -旨 -萌 -忙 -蚤 -眉 -噼 -蟑 -付 -契 -瓜 -悼 -颡 -壁 -曾 -窕 -颢 -澎 -仿 -俑 -浑 -嵌 -浣 -乍 -碌 -褪 -乱 -蔟 -隙 -玩 -剐 -葫 -箫 -纲 -围 -伐 -决 -伙 -漩 -瑟 -刑 -肓 -镳 -缓 -蹭 -氨 -皓 -典 -畲 -坍 -铑 -檐 -塑 -洞 -倬 -储 -胴 -淳 -戾 -吐 -灼 -惺 -妙 -毕 -珐 -缈 -虱 -盖 -羰 -鸿 -磅 -谓 -髅 -娴 -苴 -唷 -蚣 -霹 -抨 -贤 -唠 -犬 -誓 -逍 -庠 -逼 -麓 -籼 -釉 -呜 -碧 -秧 -氩 -摔 -霄 -穸 -纨 -辟 -妈 -映 -完 -牛 -缴 -嗷 -炊 -恩 -荔 -茆 -掉 -紊 -慌 -莓 -羟 -阙 -萁 -磐 -另 -蕹 -辱 -鳐 -湮 -吡 -吩 -唐 -睦 -垠 -舒 -圜 -冗 -瞿 -溺 -芾 -囱 -匠 -僳 -汐 -菩 -饬 -漓 -黑 -霰 -浸 -濡 -窥 -毂 -蒡 -兢 -驻 -鹉 -芮 -诙 -迫 -雳 -厂 -忐 -臆 -猴 -鸣 -蚪 -栈 -箕 -羡 -渐 -莆 -捍 -眈 -哓 -趴 -蹼 -埕 -嚣 -骛 -宏 -淄 -斑 -噜 -严 -瑛 -垃 -椎 -诱 -压 -庾 -绞 -焘 -廿 -抡 -迄 -棘 -夫 -纬 -锹 -眨 -瞌 -侠 -脐 -竞 -瀑 -孳 -骧 -遁 -姜 -颦 -荪 -滚 -萦 -伪 -逸 -粳 -爬 -锁 -矣 -役 -趣 -洒 -颔 -诏 -逐 -奸 -甭 -惠 -攀 -蹄 -泛 -尼 -拼 -阮 -鹰 -亚 -颈 -惑 -勒 -〉 -际 -肛 -爷 -刚 -钨 -丰 -养 -冶 -鲽 -辉 -蔻 -画 -覆 -皴 -妊 -麦 -返 -醉 -皂 -擀 -〗 -酶 -凑 -粹 -悟 -诀 -硖 -港 -卜 -z -杀 -涕 -± -舍 -铠 -抵 -弛 -段 -敝 -镐 -奠 -拂 -轴 -跛 -袱 -e -t -沉 -菇 -俎 -薪 -峦 -秭 -蟹 -历 -盟 -菠 -寡 -液 -肢 -喻 -染 -裱 -悱 -抱 -氙 -赤 -捅 -猛 -跑 -氮 -谣 -仁 -尺 -辊 -窍 -烙 -衍 -架 -擦 -倏 -璐 -瑁 -币 -楞 -胖 -夔 -趸 -邛 -惴 -饕 -虔 -蝎 -§ -哉 -贝 -宽 -辫 -炮 -扩 -饲 -籽 -魏 -菟 -锰 -伍 -猝 -末 -琳 -哚 -蛎 -邂 -呀 -姿 -鄞 -却 -歧 -仙 -恸 -椐 -森 -牒 -寤 -袒 -婆 -虢 -雅 -钉 -朵 -贼 -欲 -苞 -寰 -故 -龚 -坭 -嘘 -咫 -礼 -硷 -兀 -睢 -汶 -’ -铲 -烧 -绕 -诃 -浃 -钿 -哺 -柜 -讼 -颊 -璁 -腔 -洽 -咐 -脲 -簌 -筠 -镣 -玮 -鞠 -谁 -兼 -姆 -挥 -梯 -蝴 -谘 -漕 -刷 -躏 -宦 -弼 -b -垌 -劈 -麟 -莉 -揭 -笙 -渎 -仕 -嗤 -仓 -配 -怏 -抬 -错 -泯 -镊 -孰 -猿 -邪 -仍 -秋 -鼬 -壹 -歇 -吵 -炼 -< -尧 -射 -柬 -廷 -胧 -霾 -凳 -隋 -肚 -浮 -梦 -祥 -株 -堵 -退 -L -鹫 -跎 -凶 -毽 -荟 -炫 -栩 -玳 -甜 -沂 -鹿 -顽 -伯 -爹 -赔 -蛴 -徐 -匡 -欣 -狰 -缸 -雹 -蟆 -疤 -默 -沤 -啜 -痂 -衣 -禅 -w -i -h -辽 -葳 -黝 -钗 -停 -沽 -棒 -馨 -颌 -肉 -吴 -硫 -悯 -劾 -娈 -马 -啧 -吊 -悌 -镑 -峭 -帆 -瀣 -涉 -咸 -疸 -滋 -泣 -翦 -拙 -癸 -钥 -蜒 -+ -尾 -庄 -凝 -泉 -婢 -渴 -谊 -乞 -陆 -锉 -糊 -鸦 -淮 -I -B -N -晦 -弗 -乔 -庥 -葡 -尻 -席 -橡 -傣 -渣 -拿 -惩 -麋 -斛 -缃 -矮 -蛏 -岘 -鸽 -姐 -膏 -催 -奔 -镒 -喱 -蠡 -摧 -钯 -胤 -柠 -拐 -璋 -鸥 -卢 -荡 -倾 -^ -_ -珀 -逄 -萧 -塾 -掇 -贮 -笆 -聂 -圃 -冲 -嵬 -M -滔 -笕 -值 -炙 -偶 -蜱 -搐 -梆 -汪 -蔬 -腑 -鸯 -蹇 -敞 -绯 -仨 -祯 -谆 -梧 -糗 -鑫 -啸 -豺 -囹 -猾 -巢 -柄 -瀛 -筑 -踌 -沭 -暗 -苁 -鱿 -蹉 -脂 -蘖 -牢 -热 -木 -吸 -溃 -宠 -序 -泞 -偿 -拜 -檩 -厚 -朐 -毗 -螳 -吞 -媚 -朽 -担 -蝗 -橘 -畴 -祈 -糟 -盱 -隼 -郜 -惜 -珠 -裨 -铵 -焙 -琚 -唯 -咚 -噪 -骊 -丫 -滢 -勤 -棉 -呸 -咣 -淀 -隔 -蕾 -窈 -饨 -挨 -煅 -短 -匙 -粕 -镜 -赣 -撕 -墩 -酬 -馁 -豌 -颐 -抗 -酣 -氓 -佑 -搁 -哭 -递 -耷 -涡 -桃 -贻 -碣 -截 -瘦 -昭 -镌 -蔓 -氚 -甲 -猕 -蕴 -蓬 -散 -拾 -纛 -狼 -猷 -铎 -埋 -旖 -矾 -讳 -囊 -糜 -迈 -粟 -蚂 -紧 -鲳 -瘢 -栽 -稼 -羊 -锄 -斟 -睁 -桥 -瓮 -蹙 -祉 -醺 -鼻 -昱 -剃 -跳 -篱 -跷 -蒜 -翎 -宅 -晖 -嗑 -壑 -峻 -癫 -屏 -狠 -陋 -袜 -途 -憎 -祀 -莹 -滟 -佶 -溥 -臣 -约 -盛 -峰 -磁 -慵 -婪 -拦 -莅 -朕 -鹦 -粲 -裤 -哎 -疡 -嫖 -琵 -窟 -堪 -谛 -嘉 -儡 -鳝 -斩 -郾 -驸 -酊 -妄 -胜 -贺 -徙 -傅 -噌 -钢 -栅 -庇 -恋 -匝 -巯 -邈 -尸 -锚 -粗 -佟 -蛟 -薹 -纵 -蚊 -郅 -绢 -锐 -苗 -俞 -篆 -淆 -膀 -鲜 -煎 -诶 -秽 -寻 -涮 -刺 -怀 -噶 -巨 -褰 -魅 -灶 -灌 -桉 -藕 -谜 -舸 -薄 -搀 -恽 -借 -牯 -痉 -渥 -愿 -亓 -耘 -杠 -柩 -锔 -蚶 -钣 -珈 -喘 -蹒 -幽 -赐 -稗 -晤 -莱 -泔 -扯 -肯 -菪 -裆 -腩 -豉 -疆 -骜 -腐 -倭 -珏 -唔 -粮 -亡 -润 -慰 -伽 -橄 -玄 -誉 -醐 -胆 -龊 -粼 -塬 -陇 -彼 -削 -嗣 -绾 -芽 -妗 -垭 -瘴 -爽 -薏 -寨 -龈 -泠 -弹 -赢 -漪 -猫 -嘧 -涂 -恤 -圭 -茧 -烽 -屑 -痕 -巾 -赖 -荸 -凰 -腮 -畈 -亵 -蹲 -偃 -苇 -澜 -艮 -换 -骺 -烘 -苕 -梓 -颉 -肇 -哗 -悄 -氤 -涠 -葬 -屠 -鹭 -植 -竺 -佯 -诣 -鲇 -瘀 -鲅 -邦 -移 -滁 -冯 -耕 -癔 -戌 -茬 -沁 -巩 -悠 -湘 -洪 -痹 -锟 -循 -谋 -腕 -鳃 -钠 -捞 -焉 -迎 -碱 -伫 -急 -榷 -奈 -邝 -卯 -辄 -皲 -卟 -醛 -畹 -忧 -稳 -雄 -昼 -缩 -阈 -睑 -扌 -耗 -曦 -涅 -捏 -瞧 -邕 -淖 -漉 -铝 -耦 -禹 -湛 -喽 -莼 -琅 -诸 -苎 -纂 -硅 -始 -嗨 -傥 -燃 -臂 -赅 -嘈 -呆 -贵 -屹 -壮 -肋 -亍 -蚀 -卅 -豹 -腆 -邬 -迭 -浊 -} -童 -螂 -捐 -圩 -勐 -触 -寞 -汊 -壤 -荫 -膺 -渌 -芳 -懿 -遴 -螈 -泰 -蓼 -蛤 -茜 -舅 -枫 -朔 -膝 -眙 -避 -梅 -判 -鹜 -璜 -牍 -缅 -垫 -藻 -黔 -侥 -惚 -懂 -踩 -腰 -腈 -札 -丞 -唾 -慈 -顿 -摹 -荻 -琬 -~ -斧 -沈 -滂 -胁 -胀 -幄 -莜 -Z -匀 -鄄 -掌 -绰 -茎 -焚 -赋 -萱 -谑 -汁 -铒 -瞎 -夺 -蜗 -野 -娆 -冀 -弯 -篁 -懵 -灞 -隽 -芡 -脘 -俐 -辩 -芯 -掺 -喏 -膈 -蝈 -觐 -悚 -踹 -蔗 -熠 -鼠 -呵 -抓 -橼 -峨 -畜 -缔 -禾 -崭 -弃 -熊 -摒 -凸 -拗 -穹 -蒙 -抒 -祛 -劝 -闫 -扳 -阵 -醌 -踪 -喵 -侣 -搬 -仅 -荧 -赎 -蝾 -琦 -买 -婧 -瞄 -寓 -皎 -冻 -赝 -箩 -莫 -瞰 -郊 -笫 -姝 -筒 -枪 -遣 -煸 -袋 -舆 -痱 -涛 -母 -〇 -启 -践 -耙 -绲 -盘 -遂 -昊 -搞 -槿 -诬 -纰 -泓 -惨 -檬 -亻 -越 -C -o -憩 -熵 -祷 -钒 -暧 -塔 -阗 -胰 -咄 -娶 -魔 -琶 -钞 -邻 -扬 -杉 -殴 -咽 -弓 -〆 -髻 -】 -吭 -揽 -霆 -拄 -殖 -脆 -彻 -岩 -芝 -勃 -辣 -剌 -钝 -嘎 -甄 -佘 -皖 -伦 -授 -徕 -憔 -挪 -皇 -庞 -稔 -芜 -踏 -溴 -兖 -卒 -擢 -饥 -鳞 -煲 -‰ -账 -颗 -叻 -斯 -捧 -鳍 -琮 -讹 -蛙 -纽 -谭 -酸 -兔 -莒 -睇 -伟 -觑 -羲 -嗜 -宜 -褐 -旎 -辛 -卦 -诘 -筋 -鎏 -溪 -挛 -熔 -阜 -晰 -鳅 -丢 -奚 -灸 -呱 -献 -陉 -黛 -鸪 -甾 -萨 -疮 -拯 -洲 -疹 -辑 -叙 -恻 -谒 -允 -柔 -烂 -氏 -逅 -漆 -拎 -惋 -扈 -湟 -纭 -啕 -掬 -擞 -哥 -忽 -涤 -鸵 -靡 -郗 -瓷 -扁 -廊 -怨 -雏 -钮 -敦 -E -懦 -憋 -汀 -拚 -啉 -腌 -岸 -f -痼 -瞅 -尊 -咀 -眩 -飙 -忌 -仝 -迦 -熬 -毫 -胯 -篑 -茄 -腺 -凄 -舛 -碴 -锵 -诧 -羯 -後 -漏 -汤 -宓 -仞 -蚁 -壶 -谰 -皑 -铄 -棰 -罔 -辅 -晶 -苦 -牟 -闽 -\ -烃 -饮 -聿 -丙 -蛳 -朱 -煤 -涔 -鳖 -犁 -罐 -荼 -砒 -淦 -妤 -黏 -戎 -孑 -婕 -瑾 -戢 -钵 -枣 -捋 -砥 -衩 -狙 -桠 -稣 -阎 -肃 -梏 -诫 -孪 -昶 -婊 -衫 -嗔 -侃 -塞 -蜃 -樵 -峒 -貌 -屿 -欺 -缫 -阐 -栖 -诟 -珞 -荭 -吝 -萍 -嗽 -恂 -啻 -蜴 -磬 -峋 -俸 -豫 -谎 -徊 -镍 -韬 -魇 -晴 -U -囟 -猜 -蛮 -坐 -囿 -伴 -亭 -肝 -佗 -蝠 -妃 -胞 -滩 -榴 -氖 -垩 -苋 -砣 -扪 -馏 -姓 -轩 -厉 -夥 -侈 -禀 -垒 -岑 -赏 -钛 -辐 -痔 -披 -纸 -碳 -“ -坞 -蠓 -挤 -荥 -沅 -悔 -铧 -帼 -蒌 -蝇 -a -p -y -n -g -哀 -浆 -瑶 -凿 -桶 -馈 -皮 -奴 -苜 -佤 -伶 -晗 -铱 -炬 -优 -弊 -氢 -恃 -甫 -攥 -端 -锌 -灰 -稹 -炝 -曙 -邋 -亥 -眶 -碾 -拉 -萝 -绔 -捷 -浍 -腋 -姑 -菖 -凌 -涞 -麽 -锢 -桨 -潢 -绎 -镰 -殆 -锑 -渝 -铬 -困 -绽 -觎 -匈 -糙 -暑 -裹 -鸟 -盔 -肽 -迷 -綦 -『 -亳 -佝 -俘 -钴 -觇 -骥 -仆 -疝 -跪 -婶 -郯 -瀹 -唉 -脖 -踞 -针 -晾 -忒 -扼 -瞩 -叛 -椒 -疟 -嗡 -邗 -肆 -跆 -玫 -忡 -捣 -咧 -唆 -艄 -蘑 -潦 -笛 -阚 -沸 -泻 -掊 -菽 -贫 -斥 -髂 -孢 -镂 -赂 -麝 -鸾 -屡 -衬 -苷 -恪 -叠 -希 -粤 -爻 -喝 -茫 -惬 -郸 -绻 -庸 -撅 -碟 -宄 -妹 -膛 -叮 -饵 -崛 -嗲 -椅 -冤 -搅 -咕 -敛 -尹 -垦 -闷 -蝉 -霎 -勰 -败 -蓑 -泸 -肤 -鹌 -幌 -焦 -浠 -鞍 -刁 -舰 -乙 -竿 -裔 -。 -茵 -函 -伊 -兄 -丨 -娜 -匍 -謇 -莪 -宥 -似 -蝽 -翳 -酪 -翠 -粑 -薇 -祢 -骏 -赠 -叫 -Q -噤 -噻 -竖 -芗 -莠 -潭 -俊 -羿 -耜 -O -郫 -趁 -嗪 -囚 -蹶 -芒 -洁 -笋 -鹑 -敲 -硝 -啶 -堡 -渲 -揩 -』 -携 -宿 -遒 -颍 -扭 -棱 -割 -萜 -蔸 -葵 -琴 -捂 -饰 -衙 -耿 -掠 -募 -岂 -窖 -涟 -蔺 -瘤 -柞 -瞪 -怜 -匹 -距 -楔 -炜 -哆 -秦 -缎 -幼 -茁 -绪 -痨 -恨 -楸 -娅 -瓦 -桩 -雪 -嬴 -伏 -榔 -妥 -铿 -拌 -眠 -雍 -缇 -‘ -卓 -搓 -哌 -觞 -噩 -屈 -哧 -髓 -咦 -巅 -娑 -侑 -淫 -膳 -祝 -勾 -姊 -莴 -胄 -疃 -薛 -蜷 -胛 -巷 -芙 -芋 -熙 -闰 -勿 -窃 -狱 -剩 -钏 -幢 -陟 -铛 -慧 -靴 -耍 -k -浙 -浇 -飨 -惟 -绗 -祜 -澈 -啼 -咪 -磷 -摞 -诅 -郦 -抹 -跃 -壬 -吕 -肖 -琏 -颤 -尴 -剡 -抠 -凋 -赚 -泊 -津 -宕 -殷 -倔 -氲 -漫 -邺 -涎 -怠 -$ -垮 -荬 -遵 -俏 -叹 -噢 -饽 -蜘 -孙 -筵 -疼 -鞭 -羧 -牦 -箭 -潴 -c -眸 -祭 -髯 -啖 -坳 -愁 -芩 -驮 -倡 -巽 -穰 -沃 -胚 -怒 -凤 -槛 -剂 -趵 -嫁 -v -邢 -灯 -鄢 -桐 -睽 -檗 -锯 -槟 -婷 -嵋 -圻 -诗 -蕈 -颠 -遭 -痢 -芸 -怯 -馥 -竭 -锗 -徜 -恭 -遍 -籁 -剑 -嘱 -苡 -龄 -僧 -桑 -潸 -弘 -澶 -楹 -悲 -讫 -愤 -腥 -悸 -谍 -椹 -呢 -桓 -葭 -攫 -阀 -翰 -躲 -敖 -柑 -郎 -笨 -橇 -呃 -魁 -燎 -脓 -葩 -磋 -垛 -玺 -狮 -沓 -砜 -蕊 -锺 -罹 -蕉 -翱 -虐 -闾 -巫 -旦 -茱 -嬷 -枯 -鹏 -贡 -芹 -汛 -矫 -绁 -拣 -禺 -佃 -讣 -舫 -惯 -乳 -趋 -疲 -挽 -岚 -虾 -衾 -蠹 -蹂 -飓 -氦 -铖 -孩 -稞 -瑜 -壅 -掀 -勘 -妓 -畅 -髋 -W -庐 -牲 -蓿 -榕 -练 -垣 -唱 -邸 -菲 -昆 -婺 -穿 -绡 -麒 -蚱 -掂 -愚 -泷 -涪 -漳 -妩 -娉 -榄 -讷 -觅 -旧 -藤 -煮 -呛 -柳 -腓 -叭 -庵 -烷 -阡 -罂 -蜕 -擂 -猖 -咿 -媲 -脉 -【 -沏 -貅 -黠 -熏 -哲 -烁 -坦 -酵 -兜 -× -潇 -撒 -剽 -珩 -圹 -乾 -摸 -樟 -帽 -嗒 -襄 -魂 -轿 -憬 -锡 -〕 -喃 -皆 -咖 -隅 -脸 -残 -泮 -袂 -鹂 -珊 -囤 -捆 -咤 -误 -徨 -闹 -淙 -芊 -淋 -怆 -囗 -拨 -梳 -渤 -R -G -绨 -蚓 -婀 -幡 -狩 -麾 -谢 -唢 -裸 -旌 -伉 -纶 -裂 -驳 -砼 -咛 -澄 -樨 -蹈 -宙 -澍 -倍 -貔 -操 -勇 -蟠 -摈 -砧 -虬 -够 -缁 -悦 -藿 -撸 -艹 -摁 -淹 -豇 -虎 -榭 -ˉ -吱 -d -° -喧 -荀 -踱 -侮 -奋 -偕 -饷 -犍 -惮 -坑 -璎 -徘 -宛 -妆 -袈 -倩 -窦 -昂 -荏 -乖 -K -怅 -撰 -鳙 -牙 -袁 -酞 -X -痿 -琼 -闸 -雁 -趾 -荚 -虻 -涝 -《 -杏 -韭 -偈 -烤 -绫 -鞘 -卉 -症 -遢 -蓥 -诋 -杭 -荨 -匆 -竣 -簪 -辙 -敕 -虞 -丹 -缭 -咩 -黟 -m -淤 -瑕 -咂 -铉 -硼 -茨 -嶂 -痒 -畸 -敬 -涿 -粪 -窘 -熟 -叔 -嫔 -盾 -忱 -裘 -憾 -梵 -赡 -珙 -咯 -娘 -庙 -溯 -胺 -葱 -痪 -摊 -荷 -卞 -乒 -髦 -寐 -铭 -坩 -胗 -枷 -爆 -溟 -嚼 -羚 -砬 -轨 -惊 -挠 -罄 -竽 -菏 -氧 -浅 -楣 -盼 -枢 -炸 -阆 -杯 -谏 -噬 -淇 -渺 -俪 -秆 -墓 -泪 -跻 -砌 -痰 -垡 -渡 -耽 -釜 -讶 -鳎 -煞 -呗 -韶 -舶 -绷 -鹳 -缜 -旷 -铊 -皱 -龌 -檀 -霖 -奄 -槐 -艳 -蝶 -旋 -哝 -赶 -骞 -蚧 -腊 -盈 -丁 -` -蜚 -矸 -蝙 -睨 -嚓 -僻 -鬼 -醴 -夜 -彝 -磊 -笔 -拔 -栀 -糕 -厦 -邰 -纫 -逭 -纤 -眦 -膊 -馍 -躇 -烯 -蘼 -冬 -诤 -暄 -骶 -哑 -瘠 -」 -臊 -丕 -愈 -咱 -螺 -擅 -跋 -搏 -硪 -谄 -笠 -淡 -嘿 -骅 -谧 -鼎 -皋 -姚 -歼 -蠢 -驼 -耳 -胬 -挝 -涯 -狗 -蒽 -孓 -犷 -凉 -芦 -箴 -铤 -孤 -嘛 -坤 -V -茴 -朦 -挞 -尖 -橙 -诞 -搴 -碇 -洵 -浚 -帚 -蜍 -漯 -柘 -嚎 -讽 -芭 -荤 -咻 -祠 -秉 -跖 -埃 -吓 -糯 -眷 -馒 -惹 -娼 -鲑 -嫩 -讴 -轮 -瞥 -靶 -褚 -乏 -缤 -宋 -帧 -删 -驱 -碎 -扑 -俩 -俄 -偏 -涣 -竹 -噱 -皙 -佰 -渚 -唧 -斡 -# -镉 -刀 -崎 -筐 -佣 -夭 -贰 -肴 -峙 -哔 -艿 -匐 -牺 -镛 -缘 -仡 -嫡 -劣 -枸 -堀 -梨 -簿 -鸭 -蒸 -亦 -稽 -浴 -{ -衢 -束 -槲 -j -阁 -揍 -疥 -棋 -潋 -聪 -窜 -乓 -睛 -插 -冉 -阪 -苍 -搽 -「 -蟾 -螟 -幸 -仇 -樽 -撂 -慢 -跤 -幔 -俚 -淅 -覃 -觊 -溶 -妖 -帛 -侨 -曰 -妾 -泗 -· -: -瀘 -風 -Ë -( -) -∶ -紅 -紗 -瑭 -雲 -頭 -鶏 -財 -許 -• -¥ -樂 -焗 -麗 -— -; -滙 -東 -榮 -繪 -興 -… -門 -業 -π -楊 -國 -顧 -é -盤 -寳 -Λ -龍 -鳳 -島 -誌 -緣 -結 -銭 -萬 -勝 -祎 -璟 -優 -歡 -臨 -時 -購 -= -★ -藍 -昇 -鐵 -觀 -勅 -農 -聲 -畫 -兿 -術 -發 -劉 -記 -專 -耑 -園 -書 -壴 -種 -Ο -● -褀 -號 -銀 -匯 -敟 -锘 -葉 -橪 -廣 -進 -蒄 -鑽 -阝 -祙 -貢 -鍋 -豊 -夬 -喆 -團 -閣 -開 -燁 -賓 -館 -酡 -沔 -順 -+ -硚 -劵 -饸 -陽 -車 -湓 -復 -萊 -氣 -軒 -華 -堃 -迮 -纟 -戶 -馬 -學 -裡 -電 -嶽 -獨 -マ -シ -サ -ジ -燘 -袪 -環 -❤ -臺 -灣 -専 -賣 -孖 -聖 -攝 -線 -▪ -α -傢 -俬 -夢 -達 -莊 -喬 -貝 -薩 -劍 -羅 -壓 -棛 -饦 -尃 -璈 -囍 -醫 -G -I -A -# -N -鷄 -髙 -嬰 -啓 -約 -隹 -潔 -賴 -藝 -~ -寶 -籣 -麺 -  -嶺 -√ -義 -網 -峩 -長 -∧ -魚 -機 -構 -② -鳯 -偉 -L -B -㙟 -畵 -鴿 -' -詩 -溝 -嚞 -屌 -藔 -佧 -玥 -蘭 -織 -1 -3 -9 -0 -7 -點 -砭 -鴨 -鋪 -銘 -廳 -弍 -‧ -創 -湯 -坶 -℃ -卩 -骝 -& -烜 -荘 -當 -潤 -扞 -係 -懷 -碶 -钅 -蚨 -讠 -☆ -叢 -爲 -埗 -涫 -塗 -→ -楽 -現 -鯨 -愛 -瑪 -鈺 -忄 -悶 -藥 -飾 -樓 -視 -孬 -ㆍ -燚 -苪 -師 -① -丼 -锽 -│ -韓 -標 -è -兒 -閏 -匋 -張 -漢 -Ü -髪 -會 -閑 -檔 -習 -裝 -の -峯 -菘 -輝 -И -雞 -釣 -億 -浐 -K -O -R -8 -H -E -P -T -W -D -S -C -M -F -姌 -饹 -» -晞 -廰 -ä -嵯 -鷹 -負 -飲 -絲 -冚 -楗 -澤 -綫 -區 -❋ -← -質 -靑 -揚 -③ -滬 -統 -産 -協 -﹑ -乸 -畐 -經 -運 -際 -洺 -岽 -為 -粵 -諾 -崋 -豐 -碁 -ɔ -V -2 -6 -齋 -誠 -訂 -´ -勑 -雙 -陳 -無 -í -泩 -媄 -夌 -刂 -i -c -t -o -r -a -嘢 -耄 -燴 -暃 -壽 -媽 -靈 -抻 -體 -唻 -É -冮 -甹 -鎮 -錦 -ʌ -蜛 -蠄 -尓 -駕 -戀 -飬 -逹 -倫 -貴 -極 -Я -Й -寬 -磚 -嶪 -郎 -職 -| -間 -n -d -剎 -伈 -課 -飛 -橋 -瘊 -№ -譜 -骓 -圗 -滘 -縣 -粿 -咅 -養 -濤 -彳 -® -% -Ⅱ -啰 -㴪 -見 -矞 -薬 -糁 -邨 -鲮 -顔 -罱 -З -選 -話 -贏 -氪 -俵 -競 -瑩 -繡 -枱 -β -綉 -á -獅 -爾 -™ -麵 -戋 -淩 -徳 -個 -劇 -場 -務 -簡 -寵 -h -實 -膠 -轱 -圖 -築 -嘣 -樹 -㸃 -營 -耵 -孫 -饃 -鄺 -飯 -麯 -遠 -輸 -坫 -孃 -乚 -閃 -鏢 -㎡ -題 -廠 -關 -↑ -爺 -將 -軍 -連 -篦 -覌 -參 -箸 -- -窠 -棽 -寕 -夀 -爰 -歐 -呙 -閥 -頡 -熱 -雎 -垟 -裟 -凬 -勁 -帑 -馕 -夆 -疌 -枼 -馮 -貨 -蒤 -樸 -彧 -旸 -靜 -龢 -暢 -㐱 -鳥 -珺 -鏡 -灡 -爭 -堷 -廚 -Ó -騰 -診 -┅ -蘇 -褔 -凱 -頂 -豕 -亞 -帥 -嘬 -⊥ -仺 -桖 -複 -饣 -絡 -穂 -顏 -棟 -納 -▏ -濟 -親 -設 -計 -攵 -埌 -烺 -ò -頤 -燦 -蓮 -撻 -節 -講 -濱 -濃 -娽 -洳 -朿 -燈 -鈴 -護 -膚 -铔 -過 -補 -Z -U -5 -4 -坋 -闿 -䖝 -餘 -缐 -铞 -貿 -铪 -桼 -趙 -鍊 -[ -㐂 -垚 -菓 -揸 -捲 -鐘 -滏 -𣇉 -爍 -輪 -燜 -鴻 -鮮 -動 -鹞 -鷗 -丄 -慶 -鉌 -翥 -飮 -腸 -⇋ -漁 -覺 -來 -熘 -昴 -翏 -鲱 -圧 -鄉 -萭 -頔 -爐 -嫚 -г -貭 -類 -聯 -幛 -輕 -訓 -鑒 -夋 -锨 -芃 -珣 -䝉 -扙 -嵐 -銷 -處 -ㄱ -語 -誘 -苝 -歸 -儀 -燒 -楿 -內 -粢 -葒 -奧 -麥 -礻 -滿 -蠔 -穵 -瞭 -態 -鱬 -榞 -硂 -鄭 -黃 -煙 -祐 -奓 -逺 -* -瑄 -獲 -聞 -薦 -讀 -這 -樣 -決 -問 -啟 -們 -執 -説 -轉 -單 -隨 -唘 -帶 -倉 -庫 -還 -贈 -尙 -皺 -■ -餅 -產 -○ -∈ -報 -狀 -楓 -賠 -琯 -嗮 -禮 -` -傳 -> -≤ -嗞 -Φ -≥ -換 -咭 -∣ -↓ -曬 -ε -応 -寫 -″ -終 -様 -純 -費 -療 -聨 -凍 -壐 -郵 -ü -黒 -∫ -製 -塊 -調 -軽 -確 -撃 -級 -馴 -Ⅲ -涇 -繹 -數 -碼 -證 -狒 -処 -劑 -< -晧 -賀 -衆 -] -櫥 -兩 -陰 -絶 -對 -鯉 -憶 -◎ -p -e -Y -蕒 -煖 -頓 -測 -試 -鼽 -僑 -碩 -妝 -帯 -≈ -鐡 -舖 -權 -喫 -倆 -ˋ -該 -悅 -ā -俫 -. -f -s -b -m -k -g -u -j -貼 -淨 -濕 -針 -適 -備 -l -/ -給 -謢 -強 -觸 -衛 -與 -⊙ -$ -緯 -變 -⑴ -⑵ -⑶ -㎏ -殺 -∩ -幚 -─ -價 -▲ -離 -ú -ó -飄 -烏 -関 -閟 -﹝ -﹞ -邏 -輯 -鍵 -驗 -訣 -導 -歷 -屆 -層 -▼ -儱 -錄 -熳 -ē -艦 -吋 -錶 -辧 -飼 -顯 -④ -禦 -販 -気 -対 -枰 -閩 -紀 -幹 -瞓 -貊 -淚 -△ -眞 -墊 -Ω -獻 -褲 -縫 -緑 -亜 -鉅 -餠 -{ -} -◆ -蘆 -薈 -█ -◇ -溫 -彈 -晳 -粧 -犸 -穩 -訊 -崬 -凖 -熥 -П -舊 -條 -紋 -圍 -Ⅳ -筆 -尷 -難 -雜 -錯 -綁 -識 -頰 -鎖 -艶 -□ -殁 -殼 -⑧ -├ -▕ -鵬 -ǐ -ō -ǒ -糝 -綱 -▎ -μ -盜 -饅 -醬 -籤 -蓋 -釀 -鹽 -據 -à -ɡ -辦 -◥ -彐 -┌ -婦 -獸 -鲩 -伱 -ī -蒟 -蒻 -齊 -袆 -腦 -寧 -凈 -妳 -煥 -詢 -偽 -謹 -啫 -鯽 -騷 -鱸 -損 -傷 -鎻 -髮 -買 -冏 -儥 -両 -﹢ -∞ -載 -喰 -z -羙 -悵 -燙 -曉 -員 -組 -徹 -艷 -痠 -鋼 -鼙 -縮 -細 -嚒 -爯 -≠ -維 -" -鱻 -壇 -厍 -帰 -浥 -犇 -薡 -軎 -² -應 -醜 -刪 -緻 -鶴 -賜 -噁 -軌 -尨 -镔 -鷺 -槗 -彌 -葚 -濛 -請 -溇 -緹 -賢 -訪 -獴 -瑅 -資 -縤 -陣 -蕟 -栢 -韻 -祼 -恁 -伢 -謝 -劃 -涑 -總 -衖 -踺 -砋 -凉 -籃 -駿 -苼 -瘋 -昽 -紡 -驊 -腎 -﹗ -響 -杋 -剛 -嚴 -禪 -歓 -槍 -傘 -檸 -檫 -炣 -勢 -鏜 -鎢 -銑 -尐 -減 -奪 -惡 -θ -僮 -婭 -臘 -ū -ì -殻 -鉄 -∑ -蛲 -焼 -緖 -續 -紹 -懮 \ No newline at end of file diff --git a/plugins/ppocr/assets/ppocrv5_dict.txt b/plugins/ppocr/assets/ppocrv5_dict.txt new file mode 100644 index 0000000..567898b --- /dev/null +++ b/plugins/ppocr/assets/ppocrv5_dict.txt @@ -0,0 +1,18383 @@ +  +一 +乙 +二 +十 +丁 +厂 +七 +卜 +八 +人 +入 +儿 +匕 +几 +九 +刁 +了 +刀 +力 +乃 +又 +三 +干 +于 +亏 +工 +土 +士 +才 +下 +寸 +大 +丈 +与 +万 +上 +小 +口 +山 +巾 +千 +乞 +川 +亿 +个 +夕 +久 +么 +勺 +凡 +丸 +及 +广 +亡 +门 +丫 +义 +之 +尸 +己 +已 +巳 +弓 +子 +卫 +也 +女 +刃 +飞 +习 +叉 +马 +乡 +丰 +王 +开 +井 +天 +夫 +元 +无 +云 +专 +丐 +扎 +艺 +木 +五 +支 +厅 +不 +犬 +太 +区 +历 +歹 +友 +尤 +匹 +车 +巨 +牙 +屯 +戈 +比 +互 +切 +瓦 +止 +少 +曰 +日 +中 +贝 +冈 +内 +水 +见 +午 +牛 +手 +气 +毛 +壬 +升 +夭 +长 +仁 +什 +片 +仆 +化 +仇 +币 +仍 +仅 +斤 +爪 +反 +介 +父 +从 +仑 +今 +凶 +分 +乏 +公 +仓 +月 +氏 +勿 +欠 +风 +丹 +匀 +乌 +勾 +凤 +六 +文 +亢 +方 +火 +为 +斗 +忆 +计 +订 +户 +认 +冗 +讥 +心 +尺 +引 +丑 +巴 +孔 +队 +办 +以 +允 +予 +邓 +劝 +双 +书 +幻 +玉 +刊 +未 +末 +示 +击 +打 +巧 +正 +扑 +卉 +扒 +功 +扔 +去 +甘 +世 +艾 +古 +节 +本 +术 +可 +丙 +左 +厉 +石 +右 +布 +夯 +戊 +龙 +平 +灭 +轧 +东 +卡 +北 +占 +凸 +卢 +业 +旧 +帅 +归 +旦 +目 +且 +叶 +甲 +申 +叮 +电 +号 +田 +由 +只 +叭 +史 +央 +兄 +叽 +叼 +叫 +叩 +叨 +另 +叹 +冉 +皿 +凹 +囚 +四 +生 +矢 +失 +乍 +禾 +丘 +付 +仗 +代 +仙 +们 +仪 +白 +仔 +他 +斥 +瓜 +乎 +丛 +令 +用 +甩 +印 +尔 +乐 +句 +匆 +册 +卯 +犯 +外 +处 +冬 +鸟 +务 +包 +饥 +主 +市 +立 +冯 +玄 +闪 +兰 +半 +汁 +汇 +头 +汉 +宁 +穴 +它 +讨 +写 +让 +礼 +训 +议 +必 +讯 +记 +永 +司 +尼 +民 +弗 +弘 +出 +辽 +奶 +奴 +召 +加 +皮 +边 +孕 +发 +圣 +对 +台 +矛 +纠 +母 +幼 +丝 +邦 +式 +迂 +刑 +戎 +动 +扛 +寺 +吉 +扣 +考 +托 +老 +巩 +圾 +执 +扩 +扫 +地 +场 +扬 +耳 +芋 +共 +芒 +亚 +芝 +朽 +朴 +机 +权 +过 +臣 +吏 +再 +协 +西 +压 +厌 +戌 +在 +百 +有 +存 +而 +页 +匠 +夸 +夺 +灰 +达 +列 +死 +成 +夹 +夷 +轨 +邪 +尧 +划 +迈 +毕 +至 +此 +贞 +师 +尘 +尖 +劣 +光 +当 +早 +吁 +吐 +吓 +虫 +曲 +团 +吕 +同 +吊 +吃 +因 +吸 +吗 +吆 +屿 +屹 +岁 +帆 +回 +岂 +则 +刚 +网 +肉 +年 +朱 +先 +丢 +廷 +舌 +竹 +迁 +乔 +迄 +伟 +传 +乒 +乓 +休 +伍 +伏 +优 +臼 +伐 +延 +仲 +件 +任 +伤 +价 +伦 +份 +华 +仰 +仿 +伙 +伪 +自 +伊 +血 +向 +似 +后 +行 +舟 +全 +会 +杀 +合 +兆 +企 +众 +爷 +伞 +创 +肌 +肋 +朵 +杂 +危 +旬 +旨 +旭 +负 +匈 +名 +各 +多 +争 +色 +壮 +冲 +妆 +冰 +庄 +庆 +亦 +刘 +齐 +交 +衣 +次 +产 +决 +亥 +充 +妄 +闭 +问 +闯 +羊 +并 +关 +米 +灯 +州 +汗 +污 +江 +汛 +池 +汝 +汤 +忙 +兴 +宇 +守 +宅 +字 +安 +讲 +讳 +军 +讶 +许 +讹 +论 +讼 +农 +讽 +设 +访 +诀 +寻 +那 +迅 +尽 +导 +异 +弛 +孙 +阵 +阳 +收 +阶 +阴 +防 +奸 +如 +妇 +妃 +好 +她 +妈 +戏 +羽 +观 +欢 +买 +红 +驮 +纤 +驯 +约 +级 +纪 +驰 +纫 +巡 +寿 +弄 +麦 +玖 +玛 +形 +进 +戒 +吞 +远 +违 +韧 +运 +扶 +抚 +坛 +技 +坏 +抠 +扰 +扼 +拒 +找 +批 +址 +扯 +走 +抄 +贡 +汞 +坝 +攻 +赤 +折 +抓 +扳 +抡 +扮 +抢 +孝 +坎 +均 +抑 +抛 +投 +坟 +坑 +抗 +坊 +抖 +护 +壳 +志 +块 +扭 +声 +把 +报 +拟 +却 +抒 +劫 +芙 +芜 +苇 +芽 +花 +芹 +芥 +芬 +苍 +芳 +严 +芦 +芯 +劳 +克 +芭 +苏 +杆 +杠 +杜 +材 +村 +杖 +杏 +杉 +巫 +极 +李 +杨 +求 +甫 +匣 +更 +束 +吾 +豆 +两 +酉 +丽 +医 +辰 +励 +否 +还 +尬 +歼 +来 +连 +轩 +步 +卤 +坚 +肖 +旱 +盯 +呈 +时 +吴 +助 +县 +里 +呆 +吱 +吠 +呕 +园 +旷 +围 +呀 +吨 +足 +邮 +男 +困 +吵 +串 +员 +呐 +听 +吟 +吩 +呛 +吻 +吹 +呜 +吭 +吧 +邑 +吼 +囤 +别 +吮 +岖 +岗 +帐 +财 +针 +钉 +牡 +告 +我 +乱 +利 +秃 +秀 +私 +每 +兵 +估 +体 +何 +佐 +佑 +但 +伸 +佃 +作 +伯 +伶 +佣 +低 +你 +住 +位 +伴 +身 +皂 +伺 +佛 +囱 +近 +彻 +役 +返 +余 +希 +坐 +谷 +妥 +含 +邻 +岔 +肝 +肛 +肚 +肘 +肠 +龟 +甸 +免 +狂 +犹 +狈 +角 +删 +条 +彤 +卵 +灸 +岛 +刨 +迎 +饭 +饮 +系 +言 +冻 +状 +亩 +况 +床 +库 +庇 +疗 +吝 +应 +这 +冷 +庐 +序 +辛 +弃 +冶 +忘 +闰 +闲 +间 +闷 +判 +兑 +灶 +灿 +灼 +弟 +汪 +沐 +沛 +汰 +沥 +沙 +汽 +沃 +沦 +汹 +泛 +沧 +没 +沟 +沪 +沈 +沉 +沁 +怀 +忧 +忱 +快 +完 +宋 +宏 +牢 +究 +穷 +灾 +良 +证 +启 +评 +补 +初 +社 +祀 +识 +诈 +诉 +罕 +诊 +词 +译 +君 +灵 +即 +层 +屁 +尿 +尾 +迟 +局 +改 +张 +忌 +际 +陆 +阿 +陈 +阻 +附 +坠 +妓 +妙 +妖 +姊 +妨 +妒 +努 +忍 +劲 +矣 +鸡 +纬 +驱 +纯 +纱 +纲 +纳 +驳 +纵 +纷 +纸 +纹 +纺 +驴 +纽 +奉 +玩 +环 +武 +青 +责 +现 +玫 +表 +规 +抹 +卦 +坷 +坯 +拓 +拢 +拔 +坪 +拣 +坦 +担 +坤 +押 +抽 +拐 +拖 +者 +拍 +顶 +拆 +拎 +拥 +抵 +拘 +势 +抱 +拄 +垃 +拉 +拦 +幸 +拌 +拧 +拂 +拙 +招 +坡 +披 +拨 +择 +抬 +拇 +拗 +其 +取 +茉 +苦 +昔 +苛 +若 +茂 +苹 +苗 +英 +苟 +苑 +苞 +范 +直 +茁 +茄 +茎 +苔 +茅 +枉 +林 +枝 +杯 +枢 +柜 +枚 +析 +板 +松 +枪 +枫 +构 +杭 +杰 +述 +枕 +丧 +或 +画 +卧 +事 +刺 +枣 +雨 +卖 +郁 +矾 +矿 +码 +厕 +奈 +奔 +奇 +奋 +态 +欧 +殴 +垄 +妻 +轰 +顷 +转 +斩 +轮 +软 +到 +非 +叔 +歧 +肯 +齿 +些 +卓 +虎 +虏 +肾 +贤 +尚 +旺 +具 +味 +果 +昆 +国 +哎 +咕 +昌 +呵 +畅 +明 +易 +咙 +昂 +迪 +典 +固 +忠 +呻 +咒 +咋 +咐 +呼 +鸣 +咏 +呢 +咄 +咖 +岸 +岩 +帖 +罗 +帜 +帕 +岭 +凯 +败 +账 +贩 +贬 +购 +贮 +图 +钓 +制 +知 +迭 +氛 +垂 +牧 +物 +乖 +刮 +秆 +和 +季 +委 +秉 +佳 +侍 +岳 +供 +使 +例 +侠 +侥 +版 +侄 +侦 +侣 +侧 +凭 +侨 +佩 +货 +侈 +依 +卑 +的 +迫 +质 +欣 +征 +往 +爬 +彼 +径 +所 +舍 +金 +刹 +命 +肴 +斧 +爸 +采 +觅 +受 +乳 +贪 +念 +贫 +忿 +肤 +肺 +肢 +肿 +胀 +朋 +股 +肮 +肪 +肥 +服 +胁 +周 +昏 +鱼 +兔 +狐 +忽 +狗 +狞 +备 +饰 +饱 +饲 +变 +京 +享 +庞 +店 +夜 +庙 +府 +底 +疟 +疙 +疚 +剂 +卒 +郊 +庚 +废 +净 +盲 +放 +刻 +育 +氓 +闸 +闹 +郑 +券 +卷 +单 +炬 +炒 +炊 +炕 +炎 +炉 +沫 +浅 +法 +泄 +沽 +河 +沾 +泪 +沮 +油 +泊 +沿 +泡 +注 +泣 +泞 +泻 +泌 +泳 +泥 +沸 +沼 +波 +泼 +泽 +治 +怔 +怯 +怖 +性 +怕 +怜 +怪 +怡 +学 +宝 +宗 +定 +宠 +宜 +审 +宙 +官 +空 +帘 +宛 +实 +试 +郎 +诗 +肩 +房 +诚 +衬 +衫 +视 +祈 +话 +诞 +诡 +询 +该 +详 +建 +肃 +录 +隶 +帚 +屉 +居 +届 +刷 +屈 +弧 +弥 +弦 +承 +孟 +陋 +陌 +孤 +陕 +降 +函 +限 +妹 +姑 +姐 +姓 +妮 +始 +姆 +迢 +驾 +叁 +参 +艰 +线 +练 +组 +绅 +细 +驶 +织 +驹 +终 +驻 +绊 +驼 +绍 +绎 +经 +贯 +契 +贰 +奏 +春 +帮 +玷 +珍 +玲 +玻 +毒 +型 +拭 +挂 +封 +持 +拷 +拱 +项 +垮 +挎 +城 +挟 +挠 +政 +赴 +赵 +挡 +拽 +哉 +挺 +括 +垢 +拴 +拾 +挑 +垛 +指 +垫 +挣 +挤 +拼 +挖 +按 +挥 +挪 +拯 +某 +甚 +荆 +茸 +革 +茬 +荐 +巷 +带 +草 +茧 +茵 +茶 +荒 +茫 +荡 +荣 +荤 +荧 +故 +胡 +荫 +荔 +南 +药 +标 +栈 +柑 +枯 +柄 +栋 +相 +查 +柏 +栅 +柳 +柱 +柿 +栏 +柠 +树 +勃 +要 +柬 +咸 +威 +歪 +研 +砖 +厘 +厚 +砌 +砂 +泵 +砚 +砍 +面 +耐 +耍 +牵 +鸥 +残 +殃 +轴 +轻 +鸦 +皆 +韭 +背 +战 +点 +虐 +临 +览 +竖 +省 +削 +尝 +昧 +盹 +是 +盼 +眨 +哇 +哄 +哑 +显 +冒 +映 +星 +昨 +咧 +昭 +畏 +趴 +胃 +贵 +界 +虹 +虾 +蚁 +思 +蚂 +虽 +品 +咽 +骂 +勋 +哗 +咱 +响 +哈 +哆 +咬 +咳 +咪 +哪 +哟 +炭 +峡 +罚 +贱 +贴 +贻 +骨 +幽 +钙 +钝 +钞 +钟 +钢 +钠 +钥 +钦 +钧 +钩 +钮 +卸 +缸 +拜 +看 +矩 +毡 +氢 +怎 +牲 +选 +适 +秒 +香 +种 +秋 +科 +重 +复 +竿 +段 +便 +俩 +贷 +顺 +修 +俏 +保 +促 +俄 +俐 +侮 +俭 +俗 +俘 +信 +皇 +泉 +鬼 +侵 +禹 +侯 +追 +俊 +盾 +待 +徊 +衍 +律 +很 +须 +叙 +剑 +逃 +食 +盆 +胚 +胧 +胆 +胜 +胞 +胖 +脉 +胎 +勉 +狭 +狮 +独 +狰 +狡 +狱 +狠 +贸 +怨 +急 +饵 +饶 +蚀 +饺 +饼 +峦 +弯 +将 +奖 +哀 +亭 +亮 +度 +迹 +庭 +疮 +疯 +疫 +疤 +咨 +姿 +亲 +音 +帝 +施 +闺 +闻 +闽 +阀 +阁 +差 +养 +美 +姜 +叛 +送 +类 +迷 +籽 +娄 +前 +首 +逆 +兹 +总 +炼 +炸 +烁 +炮 +炫 +烂 +剃 +洼 +洁 +洪 +洒 +柒 +浇 +浊 +洞 +测 +洗 +活 +派 +洽 +染 +洛 +浏 +济 +洋 +洲 +浑 +浓 +津 +恃 +恒 +恢 +恍 +恬 +恤 +恰 +恼 +恨 +举 +觉 +宣 +宦 +室 +宫 +宪 +突 +穿 +窃 +客 +诫 +冠 +诬 +语 +扁 +袄 +祖 +神 +祝 +祠 +误 +诱 +诲 +说 +诵 +垦 +退 +既 +屋 +昼 +屏 +屎 +费 +陡 +逊 +眉 +孩 +陨 +除 +险 +院 +娃 +姥 +姨 +姻 +娇 +姚 +娜 +怒 +架 +贺 +盈 +勇 +怠 +癸 +蚤 +柔 +垒 +绑 +绒 +结 +绕 +骄 +绘 +给 +绚 +骆 +络 +绝 +绞 +骇 +统 +耕 +耘 +耗 +耙 +艳 +泰 +秦 +珠 +班 +素 +匿 +蚕 +顽 +盏 +匪 +捞 +栽 +捕 +埂 +捂 +振 +载 +赶 +起 +盐 +捎 +捍 +捏 +埋 +捉 +捆 +捐 +损 +袁 +捌 +都 +哲 +逝 +捡 +挫 +换 +挽 +挚 +热 +恐 +捣 +壶 +捅 +埃 +挨 +耻 +耿 +耽 +聂 +恭 +莽 +莱 +莲 +莫 +莉 +荷 +获 +晋 +恶 +莹 +莺 +真 +框 +梆 +桂 +桔 +栖 +档 +桐 +株 +桥 +桦 +栓 +桃 +格 +桩 +校 +核 +样 +根 +索 +哥 +速 +逗 +栗 +贾 +酌 +配 +翅 +辱 +唇 +夏 +砸 +砰 +砾 +础 +破 +原 +套 +逐 +烈 +殊 +殉 +顾 +轿 +较 +顿 +毙 +致 +柴 +桌 +虑 +监 +紧 +党 +逞 +晒 +眠 +晓 +哮 +唠 +鸭 +晃 +哺 +晌 +剔 +晕 +蚌 +畔 +蚣 +蚊 +蚪 +蚓 +哨 +哩 +圃 +哭 +哦 +恩 +鸯 +唤 +唁 +哼 +唧 +啊 +唉 +唆 +罢 +峭 +峨 +峰 +圆 +峻 +贼 +贿 +赂 +赃 +钱 +钳 +钻 +钾 +铁 +铃 +铅 +缺 +氧 +氨 +特 +牺 +造 +乘 +敌 +秤 +租 +积 +秧 +秩 +称 +秘 +透 +笔 +笑 +笋 +债 +借 +值 +倚 +俺 +倾 +倒 +倘 +俱 +倡 +候 +赁 +俯 +倍 +倦 +健 +臭 +射 +躬 +息 +倔 +徒 +徐 +殷 +舰 +舱 +般 +航 +途 +拿 +耸 +爹 +舀 +爱 +豺 +豹 +颁 +颂 +翁 +胰 +脆 +脂 +胸 +胳 +脏 +脐 +胶 +脑 +脓 +逛 +狸 +狼 +卿 +逢 +鸵 +留 +鸳 +皱 +饿 +馁 +凌 +凄 +恋 +桨 +浆 +衰 +衷 +高 +郭 +席 +准 +座 +症 +病 +疾 +斋 +疹 +疼 +疲 +脊 +效 +离 +紊 +唐 +瓷 +资 +凉 +站 +剖 +竞 +部 +旁 +旅 +畜 +阅 +羞 +羔 +瓶 +拳 +粉 +料 +益 +兼 +烤 +烘 +烦 +烧 +烛 +烟 +烙 +递 +涛 +浙 +涝 +浦 +酒 +涉 +消 +涡 +浩 +海 +涂 +浴 +浮 +涣 +涤 +流 +润 +涧 +涕 +浪 +浸 +涨 +烫 +涩 +涌 +悖 +悟 +悄 +悍 +悔 +悯 +悦 +害 +宽 +家 +宵 +宴 +宾 +窍 +窄 +容 +宰 +案 +请 +朗 +诸 +诺 +读 +扇 +诽 +袜 +袖 +袍 +被 +祥 +课 +冥 +谁 +调 +冤 +谅 +谆 +谈 +谊 +剥 +恳 +展 +剧 +屑 +弱 +陵 +祟 +陶 +陷 +陪 +娱 +娟 +恕 +娥 +娘 +通 +能 +难 +预 +桑 +绢 +绣 +验 +继 +骏 +球 +琐 +理 +琉 +琅 +捧 +堵 +措 +描 +域 +捺 +掩 +捷 +排 +焉 +掉 +捶 +赦 +堆 +推 +埠 +掀 +授 +捻 +教 +掏 +掐 +掠 +掂 +培 +接 +掷 +控 +探 +据 +掘 +掺 +职 +基 +聆 +勘 +聊 +娶 +著 +菱 +勒 +黄 +菲 +萌 +萝 +菌 +萎 +菜 +萄 +菊 +菩 +萍 +菠 +萤 +营 +乾 +萧 +萨 +菇 +械 +彬 +梦 +婪 +梗 +梧 +梢 +梅 +检 +梳 +梯 +桶 +梭 +救 +曹 +副 +票 +酝 +酗 +厢 +戚 +硅 +硕 +奢 +盔 +爽 +聋 +袭 +盛 +匾 +雪 +辅 +辆 +颅 +虚 +彪 +雀 +堂 +常 +眶 +匙 +晨 +睁 +眯 +眼 +悬 +野 +啪 +啦 +曼 +晦 +晚 +啄 +啡 +距 +趾 +啃 +跃 +略 +蚯 +蛀 +蛇 +唬 +累 +鄂 +唱 +患 +啰 +唾 +唯 +啤 +啥 +啸 +崖 +崎 +崭 +逻 +崔 +帷 +崩 +崇 +崛 +婴 +圈 +铐 +铛 +铝 +铜 +铭 +铲 +银 +矫 +甜 +秸 +梨 +犁 +秽 +移 +笨 +笼 +笛 +笙 +符 +第 +敏 +做 +袋 +悠 +偿 +偶 +偎 +偷 +您 +售 +停 +偏 +躯 +兜 +假 +衅 +徘 +徙 +得 +衔 +盘 +舶 +船 +舵 +斜 +盒 +鸽 +敛 +悉 +欲 +彩 +领 +脚 +脖 +脯 +豚 +脸 +脱 +象 +够 +逸 +猜 +猪 +猎 +猫 +凰 +猖 +猛 +祭 +馅 +馆 +凑 +减 +毫 +烹 +庶 +麻 +庵 +痊 +痒 +痕 +廊 +康 +庸 +鹿 +盗 +章 +竟 +商 +族 +旋 +望 +率 +阎 +阐 +着 +羚 +盖 +眷 +粘 +粗 +粒 +断 +剪 +兽 +焊 +焕 +清 +添 +鸿 +淋 +涯 +淹 +渠 +渐 +淑 +淌 +混 +淮 +淆 +渊 +淫 +渔 +淘 +淳 +液 +淤 +淡 +淀 +深 +涮 +涵 +婆 +梁 +渗 +情 +惜 +惭 +悼 +惧 +惕 +惟 +惊 +惦 +悴 +惋 +惨 +惯 +寇 +寅 +寄 +寂 +宿 +窒 +窑 +密 +谋 +谍 +谎 +谐 +袱 +祷 +祸 +谓 +谚 +谜 +逮 +敢 +尉 +屠 +弹 +隋 +堕 +随 +蛋 +隅 +隆 +隐 +婚 +婶 +婉 +颇 +颈 +绩 +绪 +续 +骑 +绰 +绳 +维 +绵 +绷 +绸 +综 +绽 +绿 +缀 +巢 +琴 +琳 +琢 +琼 +斑 +替 +揍 +款 +堪 +塔 +搭 +堰 +揩 +越 +趁 +趋 +超 +揽 +堤 +提 +博 +揭 +喜 +彭 +揣 +插 +揪 +搜 +煮 +援 +搀 +裁 +搁 +搓 +搂 +搅 +壹 +握 +搔 +揉 +斯 +期 +欺 +联 +葫 +散 +惹 +葬 +募 +葛 +董 +葡 +敬 +葱 +蒋 +蒂 +落 +韩 +朝 +辜 +葵 +棒 +棱 +棋 +椰 +植 +森 +焚 +椅 +椒 +棵 +棍 +椎 +棉 +棚 +棕 +棺 +榔 +椭 +惠 +惑 +逼 +粟 +棘 +酣 +酥 +厨 +厦 +硬 +硝 +确 +硫 +雁 +殖 +裂 +雄 +颊 +雳 +暂 +雅 +翘 +辈 +悲 +紫 +凿 +辉 +敞 +棠 +赏 +掌 +晴 +睐 +暑 +最 +晰 +量 +鼎 +喷 +喳 +晶 +喇 +遇 +喊 +遏 +晾 +景 +畴 +践 +跋 +跌 +跑 +跛 +遗 +蛙 +蛛 +蜓 +蜒 +蛤 +喝 +鹃 +喂 +喘 +喉 +喻 +啼 +喧 +嵌 +幅 +帽 +赋 +赌 +赎 +赐 +赔 +黑 +铸 +铺 +链 +销 +锁 +锄 +锅 +锈 +锋 +锌 +锐 +甥 +掰 +短 +智 +氮 +毯 +氯 +鹅 +剩 +稍 +程 +稀 +税 +筐 +等 +筑 +策 +筛 +筒 +筏 +答 +筋 +筝 +傲 +傅 +牌 +堡 +集 +焦 +傍 +储 +皓 +皖 +粤 +奥 +街 +惩 +御 +循 +艇 +舒 +逾 +番 +释 +禽 +腊 +脾 +腋 +腔 +腕 +鲁 +猩 +猬 +猾 +猴 +惫 +然 +馈 +馋 +装 +蛮 +就 +敦 +斌 +痘 +痢 +痪 +痛 +童 +竣 +阔 +善 +翔 +羡 +普 +粪 +尊 +奠 +道 +遂 +曾 +焰 +港 +滞 +湖 +湘 +渣 +渤 +渺 +湿 +温 +渴 +溃 +溅 +滑 +湃 +渝 +湾 +渡 +游 +滋 +渲 +溉 +愤 +慌 +惰 +愕 +愣 +惶 +愧 +愉 +慨 +割 +寒 +富 +寓 +窜 +窝 +窖 +窗 +窘 +遍 +雇 +裕 +裤 +裙 +禅 +禄 +谢 +谣 +谤 +谦 +犀 +属 +屡 +强 +粥 +疏 +隔 +隙 +隘 +媒 +絮 +嫂 +媚 +婿 +登 +缅 +缆 +缉 +缎 +缓 +缔 +缕 +骗 +编 +骚 +缘 +瑟 +鹉 +瑞 +瑰 +瑙 +魂 +肆 +摄 +摸 +填 +搏 +塌 +鼓 +摆 +携 +搬 +摇 +搞 +塘 +摊 +聘 +斟 +蒜 +勤 +靴 +靶 +鹊 +蓝 +墓 +幕 +蓬 +蓄 +蒲 +蓉 +蒙 +蒸 +献 +椿 +禁 +楚 +楷 +榄 +想 +槐 +榆 +楼 +概 +赖 +酪 +酬 +感 +碍 +碘 +碑 +碎 +碰 +碗 +碌 +尴 +雷 +零 +雾 +雹 +辐 +辑 +输 +督 +频 +龄 +鉴 +睛 +睹 +睦 +瞄 +睫 +睡 +睬 +嗜 +鄙 +嗦 +愚 +暖 +盟 +歇 +暗 +暇 +照 +畸 +跨 +跷 +跳 +跺 +跪 +路 +跤 +跟 +遣 +蜈 +蜗 +蛾 +蜂 +蜕 +嗅 +嗡 +嗓 +署 +置 +罪 +罩 +蜀 +幌 +错 +锚 +锡 +锣 +锤 +锥 +锦 +键 +锯 +锰 +矮 +辞 +稚 +稠 +颓 +愁 +筹 +签 +简 +筷 +毁 +舅 +鼠 +催 +傻 +像 +躲 +魁 +衙 +微 +愈 +遥 +腻 +腰 +腥 +腮 +腹 +腺 +鹏 +腾 +腿 +鲍 +猿 +颖 +触 +解 +煞 +雏 +馍 +馏 +酱 +禀 +痹 +廓 +痴 +痰 +廉 +靖 +新 +韵 +意 +誊 +粮 +数 +煎 +塑 +慈 +煤 +煌 +满 +漠 +滇 +源 +滤 +滥 +滔 +溪 +溜 +漓 +滚 +溢 +溯 +滨 +溶 +溺 +粱 +滩 +慎 +誉 +塞 +寞 +窥 +窟 +寝 +谨 +褂 +裸 +福 +谬 +群 +殿 +辟 +障 +媳 +嫉 +嫌 +嫁 +叠 +缚 +缝 +缠 +缤 +剿 +静 +碧 +璃 +赘 +熬 +墙 +墟 +嘉 +摧 +赫 +截 +誓 +境 +摘 +摔 +撇 +聚 +慕 +暮 +摹 +蔓 +蔑 +蔡 +蔗 +蔽 +蔼 +熙 +蔚 +兢 +模 +槛 +榴 +榜 +榨 +榕 +歌 +遭 +酵 +酷 +酿 +酸 +碟 +碱 +碳 +磁 +愿 +需 +辖 +辗 +雌 +裳 +颗 +瞅 +墅 +嗽 +踊 +蜻 +蜡 +蝇 +蜘 +蝉 +嘛 +嘀 +赚 +锹 +锻 +镀 +舞 +舔 +稳 +熏 +箕 +算 +箩 +管 +箫 +舆 +僚 +僧 +鼻 +魄 +魅 +貌 +膜 +膊 +膀 +鲜 +疑 +孵 +馒 +裹 +敲 +豪 +膏 +遮 +腐 +瘩 +瘟 +瘦 +辣 +彰 +竭 +端 +旗 +精 +粹 +歉 +弊 +熄 +熔 +煽 +潇 +漆 +漱 +漂 +漫 +滴 +漾 +演 +漏 +慢 +慷 +寨 +赛 +寡 +察 +蜜 +寥 +谭 +肇 +褐 +褪 +谱 +隧 +嫩 +翠 +熊 +凳 +骡 +缩 +慧 +撵 +撕 +撒 +撩 +趣 +趟 +撑 +撮 +撬 +播 +擒 +墩 +撞 +撤 +增 +撰 +聪 +鞋 +鞍 +蕉 +蕊 +蔬 +蕴 +横 +槽 +樱 +橡 +樟 +橄 +敷 +豌 +飘 +醋 +醇 +醉 +磕 +磊 +磅 +碾 +震 +霄 +霉 +瞒 +题 +暴 +瞎 +嘻 +嘶 +嘲 +嘹 +影 +踢 +踏 +踩 +踪 +蝶 +蝴 +蝠 +蝎 +蝌 +蝗 +蝙 +嘿 +嘱 +幢 +墨 +镇 +镐 +镑 +靠 +稽 +稻 +黎 +稿 +稼 +箱 +篓 +箭 +篇 +僵 +躺 +僻 +德 +艘 +膝 +膛 +鲤 +鲫 +熟 +摩 +褒 +瘪 +瘤 +瘫 +凛 +颜 +毅 +糊 +遵 +憋 +潜 +澎 +潮 +潭 +鲨 +澳 +潘 +澈 +澜 +澄 +懂 +憔 +懊 +憎 +额 +翩 +褥 +谴 +鹤 +憨 +慰 +劈 +履 +豫 +缭 +撼 +擂 +操 +擅 +燕 +蕾 +薯 +薛 +薇 +擎 +薪 +薄 +颠 +翰 +噩 +橱 +橙 +橘 +整 +融 +瓢 +醒 +霍 +霎 +辙 +冀 +餐 +嘴 +踱 +蹄 +蹂 +蟆 +螃 +器 +噪 +鹦 +赠 +默 +黔 +镜 +赞 +穆 +篮 +篡 +篷 +篱 +儒 +邀 +衡 +膨 +雕 +鲸 +磨 +瘾 +瘸 +凝 +辨 +辩 +糙 +糖 +糕 +燃 +濒 +澡 +激 +懒 +憾 +懈 +窿 +壁 +避 +缰 +缴 +戴 +擦 +藉 +鞠 +藏 +藐 +檬 +檐 +檀 +礁 +磷 +霜 +霞 +瞭 +瞧 +瞬 +瞳 +瞩 +瞪 +曙 +蹋 +蹈 +螺 +蟋 +蟀 +嚎 +赡 +穗 +魏 +簧 +簇 +繁 +徽 +爵 +朦 +臊 +鳄 +癌 +辫 +赢 +糟 +糠 +燥 +懦 +豁 +臀 +臂 +翼 +骤 +藕 +鞭 +藤 +覆 +瞻 +蹦 +嚣 +镰 +翻 +鳍 +鹰 +瀑 +襟 +璧 +戳 +孽 +警 +蘑 +藻 +攀 +曝 +蹲 +蹭 +蹬 +巅 +簸 +簿 +蟹 +颤 +靡 +癣 +瓣 +羹 +鳖 +爆 +疆 +鬓 +壤 +馨 +耀 +躁 +蠕 +嚼 +嚷 +巍 +籍 +鳞 +魔 +糯 +灌 +譬 +蠢 +霸 +露 +霹 +躏 +黯 +髓 +赣 +囊 +镶 +瓤 +罐 +矗 +乂 +乜 +兀 +弋 +孑 +孓 +幺 +亓 +韦 +廿 +丏 +卅 +仄 +厄 +仃 +仉 +仂 +兮 +刈 +爻 +卞 +闩 +讣 +尹 +夬 +爿 +毋 +邗 +邛 +艽 +艿 +札 +叵 +匝 +丕 +匜 +劢 +卟 +叱 +叻 +仨 +仕 +仟 +仡 +仫 +仞 +卮 +氐 +犰 +刍 +邝 +邙 +汀 +讦 +讧 +讪 +讫 +尻 +阡 +尕 +弁 +驭 +匡 +耒 +玎 +玑 +邢 +圩 +圬 +圭 +扦 +圪 +圳 +圹 +扪 +圮 +圯 +芊 +芍 +芄 +芨 +芑 +芎 +芗 +亘 +厍 +夼 +戍 +尥 +乩 +旯 +曳 +岌 +屺 +凼 +囡 +钇 +缶 +氘 +氖 +牝 +伎 +伛 +伢 +佤 +仵 +伥 +伧 +伉 +伫 +囟 +汆 +刖 +夙 +旮 +刎 +犷 +犸 +舛 +凫 +邬 +饧 +汕 +汔 +汐 +汲 +汜 +汊 +忖 +忏 +讴 +讵 +祁 +讷 +聿 +艮 +厾 +阱 +阮 +阪 +丞 +妁 +牟 +纡 +纣 +纥 +纨 +玕 +玙 +抟 +抔 +圻 +坂 +坍 +坞 +抃 +抉 +㧐 +芫 +邯 +芸 +芾 +苈 +苣 +芷 +芮 +苋 +芼 +苌 +苁 +芩 +芪 +芡 +芟 +苄 +苎 +苡 +杌 +杓 +杞 +杈 +忑 +孛 +邴 +邳 +矶 +奁 +豕 +忒 +欤 +轫 +迓 +邶 +忐 +卣 +邺 +旰 +呋 +呒 +呓 +呔 +呖 +呃 +旸 +吡 +町 +虬 +呗 +吽 +吣 +吲 +帏 +岐 +岈 +岘 +岑 +岚 +兕 +囵 +囫 +钊 +钋 +钌 +迕 +氙 +氚 +牤 +佞 +邱 +攸 +佚 +佝 +佟 +佗 +伽 +彷 +佘 +佥 +孚 +豸 +坌 +肟 +邸 +奂 +劬 +狄 +狁 +鸠 +邹 +饨 +饩 +饪 +饫 +饬 +亨 +庑 +庋 +疔 +疖 +肓 +闱 +闳 +闵 +羌 +炀 +沣 +沅 +沔 +沤 +沌 +沏 +沚 +汩 +汨 +沂 +汾 +沨 +汴 +汶 +沆 +沩 +泐 +怃 +怄 +忡 +忤 +忾 +怅 +忻 +忪 +怆 +忭 +忸 +诂 +诃 +诅 +诋 +诌 +诏 +诒 +孜 +陇 +陀 +陂 +陉 +妍 +妩 +妪 +妣 +妊 +妗 +妫 +妞 +姒 +妤 +邵 +劭 +刭 +甬 +邰 +纭 +纰 +纴 +纶 +纾 +玮 +玡 +玭 +玠 +玢 +玥 +玦 +盂 +忝 +匦 +坩 +抨 +拤 +坫 +拈 +垆 +抻 +劼 +拃 +拊 +坼 +坻 +㧟 +坨 +坭 +抿 +坳 +耶 +苷 +苯 +苤 +茏 +苫 +苜 +苴 +苒 +苘 +茌 +苻 +苓 +茚 +茆 +茑 +茓 +茔 +茕 +茀 +苕 +枥 +枇 +杪 +杳 +枧 +杵 +枨 +枞 +枋 +杻 +杷 +杼 +矸 +砀 +刳 +奄 +瓯 +殁 +郏 +轭 +郅 +鸢 +盱 +昊 +昙 +杲 +昃 +咂 +呸 +昕 +昀 +旻 +昉 +炅 +咔 +畀 +虮 +咀 +呷 +黾 +呱 +呤 +咚 +咆 +咛 +呶 +呣 +呦 +咝 +岢 +岿 +岬 +岫 +帙 +岣 +峁 +刿 +迥 +岷 +剀 +帔 +峄 +沓 +囹 +罔 +钍 +钎 +钏 +钒 +钕 +钗 +邾 +迮 +牦 +竺 +迤 +佶 +佬 +佰 +侑 +侉 +臾 +岱 +侗 +侃 +侏 +侩 +佻 +佾 +侪 +佼 +佯 +侬 +帛 +阜 +侔 +徂 +刽 +郄 +怂 +籴 +瓮 +戗 +肼 +䏝 +肽 +肱 +肫 +剁 +迩 +郇 +狙 +狎 +狍 +狒 +咎 +炙 +枭 +饯 +饴 +冽 +冼 +庖 +疠 +疝 +疡 +兖 +妾 +劾 +炜 +𬉼 +炖 +炘 +炝 +炔 +泔 +沭 +泷 +泸 +泱 +泅 +泗 +泠 +泺 +泖 +泫 +泮 +沱 +泯 +泓 +泾 +怙 +怵 +怦 +怛 +怏 +怍 +㤘 +怩 +怫 +怿 +宕 +穹 +宓 +诓 +诔 +诖 +诘 +戾 +诙 +戽 +郓 +衩 +祆 +祎 +祉 +祇 +诛 +诜 +诟 +诠 +诣 +诤 +诧 +诨 +诩 +戕 +孢 +亟 +陔 +妲 +妯 +姗 +帑 +弩 +孥 +驽 +虱 +迦 +迨 +绀 +绁 +绂 +驷 +驸 +绉 +绌 +驿 +骀 +甾 +珏 +珐 +珂 +珑 +玳 +珀 +顸 +珉 +珈 +拮 +垭 +挝 +垣 +挞 +垤 +赳 +贲 +垱 +垌 +郝 +垧 +垓 +挦 +垠 +茜 +荚 +荑 +贳 +荜 +莒 +茼 +茴 +茱 +莛 +荞 +茯 +荏 +荇 +荃 +荟 +荀 +茗 +荠 +茭 +茨 +垩 +荥 +荦 +荨 +荩 +剋 +荪 +茹 +荬 +荮 +柰 +栉 +柯 +柘 +栊 +柩 +枰 +栌 +柙 +枵 +柚 +枳 +柞 +柝 +栀 +柢 +栎 +枸 +柈 +柁 +枷 +柽 +剌 +酊 +郦 +甭 +砗 +砘 +砒 +斫 +砭 +砜 +奎 +耷 +虺 +殂 +殇 +殄 +殆 +轱 +轲 +轳 +轶 +轸 +虿 +毖 +觇 +尜 +哐 +眄 +眍 +𠳐 +郢 +眇 +眊 +眈 +禺 +哂 +咴 +曷 +昴 +昱 +昵 +咦 +哓 +哔 +畎 +毗 +呲 +胄 +畋 +畈 +虼 +虻 +盅 +咣 +哕 +剐 +郧 +咻 +囿 +咿 +哌 +哙 +哚 +咯 +咩 +咤 +哝 +哏 +哞 +峙 +峣 +罘 +帧 +峒 +峤 +峋 +峥 +贶 +钚 +钛 +钡 +钣 +钤 +钨 +钫 +钯 +氡 +氟 +牯 +郜 +秕 +秭 +竽 +笈 +笃 +俦 +俨 +俅 +俪 +叟 +垡 +牮 +俣 +俚 +皈 +俑 +俟 +逅 +徇 +徉 +舢 +俞 +郗 +俎 +郤 +爰 +郛 +瓴 +胨 +胪 +胛 +胂 +胙 +胍 +胗 +胝 +朐 +胫 +鸨 +匍 +狨 +狯 +飑 +狩 +狲 +訇 +逄 +昝 +饷 +饸 +饹 +胤 +孪 +娈 +弈 +奕 +庥 +疬 +疣 +疥 +疭 +庠 +竑 +彦 +飒 +闼 +闾 +闿 +阂 +羑 +迸 +籼 +酋 +炳 +炻 +炽 +炯 +烀 +炷 +烃 +洱 +洹 +洧 +洌 +浃 +洇 +洄 +洙 +涎 +洎 +洫 +浍 +洮 +洵 +浒 +浔 +浕 +洳 +恸 +恓 +恹 +恫 +恺 +恻 +恂 +恪 +恽 +宥 +扃 +衲 +衽 +衿 +袂 +祛 +祜 +祓 +祚 +诮 +祗 +祢 +诰 +诳 +鸩 +昶 +郡 +咫 +弭 +牁 +胥 +陛 +陟 +娅 +姮 +娆 +姝 +姣 +姘 +姹 +怼 +羿 +炱 +矜 +绔 +骁 +骅 +绗 +绛 +骈 +耖 +挈 +珥 +珙 +顼 +珰 +珩 +珧 +珣 +珞 +琤 +珲 +敖 +恚 +埔 +埕 +埘 +埙 +埚 +挹 +耆 +耄 +埒 +捋 +贽 +垸 +捃 +盍 +荸 +莆 +莳 +莴 +莪 +莠 +莓 +莜 +莅 +荼 +莩 +荽 +莸 +荻 +莘 +莎 +莞 +莨 +渇 +鸪 +莼 +栲 +栳 +郴 +桓 +桡 +桎 +桢 +桤 +梃 +栝 +桕 +桁 +桧 +桅 +栟 +桉 +栩 +逑 +逋 +彧 +鬲 +豇 +酐 +逦 +厝 +孬 +砝 +砹 +砺 +砧 +砷 +砟 +砼 +砥 +砣 +剞 +砻 +轼 +轾 +辂 +鸫 +趸 +龀 +鸬 +虔 +逍 +眬 +唛 +晟 +眩 +眙 +哧 +哽 +唔 +晁 +晏 +鸮 +趵 +趿 +畛 +蚨 +蚜 +蚍 +蚋 +蚬 +蚝 +蚧 +唢 +圄 +唣 +唏 +盎 +唑 +崂 +崃 +罡 +罟 +峪 +觊 +赅 +钰 +钲 +钴 +钵 +钹 +钺 +钽 +钼 +钿 +铀 +铂 +铄 +铆 +铈 +铉 +铊 +铋 +铌 +铍 +䥽 +铎 +氩 +氤 +氦 +毪 +舐 +秣 +秫 +盉 +笄 +笕 +笊 +笏 +笆 +俸 +倩 +俵 +偌 +俳 +俶 +倬 +倏 +恁 +倭 +倪 +俾 +倜 +隼 +隽 +倌 +倥 +臬 +皋 +郫 +倨 +衄 +颀 +徕 +舫 +釜 +奚 +衾 +胯 +胱 +胴 +胭 +脍 +胼 +朕 +脒 +胺 +鸱 +玺 +鸲 +狷 +猁 +狳 +猃 +狺 +逖 +桀 +袅 +饽 +凇 +栾 +挛 +亳 +疳 +疴 +疸 +疽 +痈 +疱 +痂 +痉 +衮 +凋 +颃 +恣 +旆 +旄 +旃 +阃 +阄 +訚 +阆 +恙 +粑 +朔 +郸 +烜 +烨 +烩 +烊 +剡 +郯 +烬 +涑 +浯 +涞 +涟 +娑 +涅 +涠 +浞 +涓 +浥 +涔 +浜 +浠 +浣 +浚 +悚 +悭 +悝 +悒 +悌 +悛 +宸 +窈 +剜 +诹 +冢 +诼 +袒 +袢 +祯 +诿 +谀 +谂 +谄 +谇 +屐 +屙 +陬 +勐 +奘 +牂 +蚩 +陲 +姬 +娠 +娌 +娉 +娲 +娩 +娴 +娣 +娓 +婀 +畚 +逡 +绠 +骊 +绡 +骋 +绥 +绦 +绨 +骎 +邕 +鸶 +彗 +耜 +焘 +舂 +琏 +琇 +麸 +揶 +埴 +埯 +捯 +掳 +掴 +埸 +埵 +赧 +埤 +捭 +逵 +埝 +堋 +堍 +掬 +鸷 +掖 +捽 +掊 +堉 +掸 +捩 +掮 +悫 +埭 +埽 +掇 +掼 +聃 +菁 +萁 +菘 +堇 +萘 +萋 +菽 +菖 +萜 +萸 +萑 +棻 +菔 +菟 +萏 +萃 +菏 +菹 +菪 +菅 +菀 +萦 +菰 +菡 +梵 +梿 +梏 +觋 +桴 +桷 +梓 +棁 +桫 +棂 +啬 +郾 +匮 +敕 +豉 +鄄 +酞 +酚 +戛 +硎 +硭 +硒 +硖 +硗 +硐 +硇 +硌 +鸸 +瓠 +匏 +厩 +龚 +殒 +殓 +殍 +赉 +雩 +辄 +堑 +眭 +眦 +啧 +晡 +晤 +眺 +眵 +眸 +圊 +喏 +喵 +啉 +勖 +晞 +唵 +晗 +冕 +啭 +畦 +趺 +啮 +跄 +蚶 +蛄 +蛎 +蛆 +蚰 +蛊 +圉 +蚱 +蛉 +蛏 +蚴 +啁 +啕 +唿 +啐 +唼 +唷 +啖 +啵 +啶 +啷 +唳 +唰 +啜 +帻 +崚 +崦 +帼 +崮 +崤 +崆 +赇 +赈 +赊 +铑 +铒 +铗 +铙 +铟 +铠 +铡 +铢 +铣 +铤 +铧 +铨 +铩 +铪 +铫 +铬 +铮 +铯 +铰 +铱 +铳 +铵 +铷 +氪 +牾 +鸹 +秾 +逶 +笺 +筇 +笸 +笪 +笮 +笠 +笥 +笤 +笳 +笾 +笞 +偾 +偃 +偕 +偈 +傀 +偬 +偻 +皑 +皎 +鸻 +徜 +舸 +舻 +舴 +舷 +龛 +翎 +脬 +脘 +脲 +匐 +猗 +猡 +猞 +猝 +斛 +猕 +馗 +馃 +馄 +鸾 +孰 +庹 +庾 +痔 +痍 +疵 +翊 +旌 +旎 +袤 +阇 +阈 +阉 +阊 +阋 +阍 +阏 +羟 +粝 +粕 +敝 +焐 +烯 +焓 +烽 +焖 +烷 +焗 +渍 +渚 +淇 +淅 +淞 +渎 +涿 +淖 +挲 +淠 +涸 +渑 +淦 +淝 +淬 +涪 +淙 +涫 +渌 +淄 +惬 +悻 +悱 +惝 +惘 +悸 +惆 +惚 +惇 +惮 +窕 +谌 +谏 +扈 +皲 +谑 +裆 +袷 +裉 +谒 +谔 +谕 +谖 +谗 +谙 +谛 +谝 +逯 +郿 +隈 +粜 +隍 +隗 +婧 +婊 +婕 +娼 +婢 +婵 +胬 +袈 +翌 +恿 +欸 +绫 +骐 +绮 +绯 +绱 +骒 +绲 +骓 +绶 +绺 +绻 +绾 +骖 +缁 +耠 +琫 +琵 +琶 +琪 +瑛 +琦 +琥 +琨 +靓 +琰 +琮 +琯 +琬 +琛 +琚 +辇 +鼋 +揳 +堞 +搽 +揸 +揠 +堙 +趄 +揖 +颉 +塄 +揿 +耋 +揄 +蛩 +蛰 +塆 +摒 +揆 +掾 +聒 +葑 +葚 +靰 +靸 +葳 +葺 +葸 +萼 +葆 +葩 +葶 +蒌 +萱 +戟 +葭 +楮 +棼 +椟 +棹 +椤 +棰 +赍 +椋 +椁 +椪 +棣 +椐 +鹁 +覃 +酤 +酢 +酡 +鹂 +厥 +殚 +殛 +雯 +雱 +辊 +辋 +椠 +辍 +辎 +斐 +睄 +睑 +睇 +睃 +戢 +喋 +嗒 +喃 +喱 +喹 +晷 +喈 +跖 +跗 +跞 +跚 +跎 +跏 +跆 +蛱 +蛲 +蛭 +蛳 +蛐 +蛔 +蛞 +蛴 +蛟 +蛘 +喁 +喟 +啾 +嗖 +喑 +嗟 +喽 +嗞 +喀 +喔 +喙 +嵘 +嵖 +崴 +遄 +詈 +嵎 +崽 +嵬 +嵛 +嵯 +嵝 +嵫 +幄 +嵋 +赕 +铻 +铼 +铿 +锃 +锂 +锆 +锇 +锉 +锏 +锑 +锒 +锔 +锕 +掣 +矬 +氰 +毳 +毽 +犊 +犄 +犋 +鹄 +犍 +嵇 +黍 +稃 +稂 +筚 +筵 +筌 +傣 +傈 +舄 +牍 +傥 +傧 +遑 +傩 +遁 +徨 +媭 +畲 +弑 +颌 +翕 +釉 +鹆 +舜 +貂 +腈 +腌 +腓 +腆 +腴 +腑 +腚 +腱 +鱿 +鲀 +鲂 +颍 +猢 +猹 +猥 +飓 +觞 +觚 +猱 +颎 +飧 +馇 +馊 +亵 +脔 +裒 +痣 +痨 +痦 +痞 +痤 +痫 +痧 +赓 +竦 +瓿 +啻 +颏 +鹇 +阑 +阒 +阕 +粞 +遒 +孳 +焯 +焜 +焙 +焱 +鹈 +湛 +渫 +湮 +湎 +湜 +渭 +湍 +湫 +溲 +湟 +溆 +湲 +湔 +湉 +渥 +湄 +滁 +愠 +惺 +愦 +惴 +愀 +愎 +愔 +喾 +寐 +谟 +扉 +裢 +裎 +裥 +祾 +祺 +谠 +幂 +谡 +谥 +谧 +遐 +孱 +弼 +巽 +骘 +媪 +媛 +婷 +巯 +翚 +皴 +婺 +骛 +缂 +缃 +缄 +彘 +缇 +缈 +缌 +缑 +缒 +缗 +飨 +耢 +瑚 +瑁 +瑜 +瑗 +瑄 +瑕 +遨 +骜 +韫 +髡 +塬 +鄢 +趔 +趑 +摅 +摁 +蜇 +搋 +搪 +搐 +搛 +搠 +摈 +彀 +毂 +搦 +搡 +蓁 +戡 +蓍 +鄞 +靳 +蓐 +蓦 +鹋 +蒽 +蓓 +蓖 +蓊 +蒯 +蓟 +蓑 +蒿 +蒺 +蓠 +蒟 +蒡 +蒹 +蒴 +蒗 +蓥 +颐 +楔 +楠 +楂 +楝 +楫 +楸 +椴 +槌 +楯 +皙 +榈 +槎 +榉 +楦 +楣 +楹 +椽 +裘 +剽 +甄 +酮 +酰 +酯 +酩 +蜃 +碛 +碓 +硼 +碉 +碚 +碇 +碜 +鹌 +辏 +龃 +龅 +訾 +粲 +虞 +睚 +嗪 +韪 +嗷 +嗉 +睨 +睢 +雎 +睥 +嘟 +嗑 +嗫 +嗬 +嗔 +嗝 +戥 +嗄 +煦 +暄 +遢 +暌 +跬 +跶 +跸 +跐 +跣 +跹 +跻 +蛸 +蜊 +蜍 +蜉 +蜣 +畹 +蛹 +嗣 +嗯 +嗥 +嗲 +嗳 +嗌 +嗍 +嗨 +嗐 +嗤 +嗵 +罨 +嵊 +嵩 +嵴 +骰 +锗 +锛 +锜 +锝 +锞 +锟 +锢 +锨 +锩 +锭 +锱 +雉 +氲 +犏 +歃 +稞 +稗 +稔 +筠 +筢 +筮 +筲 +筱 +牒 +煲 +敫 +徭 +愆 +艄 +觎 +毹 +貊 +貅 +貉 +颔 +腠 +腩 +腼 +腭 +腧 +塍 +媵 +詹 +鲅 +鲆 +鲇 +鲈 +稣 +鲋 +鲐 +肄 +鹐 +飕 +觥 +遛 +馐 +鹑 +亶 +瘃 +痱 +痼 +痿 +瘐 +瘁 +瘆 +麂 +裔 +歆 +旒 +雍 +阖 +阗 +阙 +羧 +豢 +粳 +猷 +煳 +煜 +煨 +煅 +煊 +煸 +煺 +滟 +溱 +溘 +漭 +滢 +溥 +溧 +溽 +裟 +溻 +溷 +滗 +滫 +溴 +滏 +滃 +滦 +溏 +滂 +滓 +溟 +滪 +愫 +慑 +慊 +鲎 +骞 +窦 +窠 +窣 +裱 +褚 +裨 +裾 +裰 +禊 +谩 +谪 +媾 +嫫 +媲 +嫒 +嫔 +媸 +缙 +缜 +缛 +辔 +骝 +缟 +缡 +缢 +缣 +骟 +耥 +璈 +瑶 +瑭 +獒 +觏 +慝 +嫠 +韬 +叆 +髦 +摽 +墁 +撂 +摞 +撄 +翥 +踅 +摭 +墉 +墒 +榖 +綦 +蔫 +蔷 +靺 +靼 +鞅 +靿 +甍 +蔸 +蔟 +蔺 +戬 +蕖 +蔻 +蓿 +斡 +鹕 +蓼 +榛 +榧 +榻 +榫 +榭 +槔 +榱 +槁 +槟 +槠 +榷 +僰 +酽 +酶 +酹 +厮 +碡 +碴 +碣 +碲 +磋 +臧 +豨 +殡 +霆 +霁 +辕 +蜚 +裴 +翡 +龇 +龈 +睿 +䁖 +睽 +嘞 +嘈 +嘌 +嘁 +嘎 +暧 +暝 +踌 +踉 +蜞 +蜥 +蜮 +蝈 +蜴 +蜱 +蜩 +蜷 +蜿 +螂 +蜢 +嘘 +嘡 +鹗 +嘣 +嘤 +嘚 +嗾 +嘧 +罴 +罱 +幔 +嶂 +幛 +赙 +罂 +骷 +骶 +鹘 +锲 +锴 +锶 +锷 +锸 +锵 +镁 +镂 +犒 +箐 +箦 +箧 +箍 +箸 +箬 +箅 +箪 +箔 +箜 +箢 +箓 +毓 +僖 +儆 +僳 +僭 +劁 +僮 +魃 +魆 +睾 +艋 +鄱 +膈 +膑 +鲑 +鲔 +鲚 +鲛 +鲟 +獐 +觫 +雒 +夤 +馑 +銮 +塾 +麽 +瘌 +瘊 +瘘 +瘙 +廖 +韶 +旖 +膂 +阚 +鄯 +鲞 +粿 +粼 +粽 +糁 +槊 +鹚 +熘 +熥 +潢 +漕 +滹 +漯 +漶 +潋 +潴 +漪 +漉 +漳 +漩 +澉 +潍 +慵 +搴 +窨 +寤 +綮 +谮 +褡 +褙 +褓 +褛 +褊 +谯 +谰 +谲 +暨 +屣 +鹛 +嫣 +嫱 +嫖 +嫦 +嫚 +嫘 +嫡 +鼐 +翟 +瞀 +鹜 +骠 +缥 +缦 +缧 +缨 +骢 +缪 +缫 +耦 +耧 +瑾 +璜 +璀 +璎 +璁 +璋 +璇 +奭 +髯 +髫 +撷 +撅 +赭 +撸 +鋆 +撙 +撺 +墀 +聩 +觐 +鞑 +蕙 +鞒 +蕈 +蕨 +蕤 +蕞 +蕺 +瞢 +蕃 +蕲 +赜 +槿 +樯 +槭 +樗 +樘 +樊 +槲 +醌 +醅 +靥 +魇 +餍 +磔 +磙 +霈 +辘 +龉 +龊 +觑 +瞌 +瞋 +瞑 +嘭 +噎 +噶 +颙 +暹 +噘 +踔 +踝 +踟 +踒 +踬 +踮 +踯 +踺 +踞 +蝽 +蝾 +蝻 +蝰 +蝮 +螋 +蝓 +蝣 +蝼 +噗 +嘬 +颚 +噍 +噢 +噙 +噜 +噌 +噔 +颛 +幞 +幡 +嶙 +嶝 +骺 +骼 +骸 +镊 +镉 +镌 +镍 +镏 +镒 +镓 +镔 +稷 +箴 +篑 +篁 +篌 +篆 +牖 +儋 +徵 +磐 +虢 +鹞 +膘 +滕 +鲠 +鲡 +鲢 +鲣 +鲥 +鲧 +鲩 +獗 +獠 +觯 +馓 +馔 +麾 +廛 +瘛 +瘼 +瘢 +瘠 +齑 +羯 +羰 +𥻗 +遴 +糌 +糍 +糅 +熜 +熵 +熠 +澍 +澌 +潸 +潦 +潲 +鋈 +潟 +潼 +潺 +憬 +憧 +寮 +窳 +谳 +褴 +褟 +褫 +谵 +熨 +屦 +嬉 +勰 +戮 +蝥 +缬 +缮 +缯 +骣 +畿 +耩 +耨 +耪 +璞 +璟 +靛 +璠 +璘 +聱 +螯 +髻 +髭 +髹 +擀 +熹 +甏 +擞 +縠 +磬 +颞 +蕻 +鞘 +颟 +薤 +薨 +檠 +薏 +薮 +薜 +薅 +樾 +橛 +橇 +樵 +檎 +橹 +樽 +樨 +橼 +墼 +橐 +翮 +醛 +醐 +醍 +醚 +磲 +赝 +飙 +殪 +霖 +霏 +霓 +錾 +辚 +臻 +遽 +氅 +瞟 +瞠 +瞰 +嚄 +嚆 +噤 +暾 +蹀 +踹 +踵 +踽 +蹉 +蹁 +螨 +蟒 +螈 +螅 +螭 +螠 +螟 +噱 +噬 +噫 +噻 +噼 +罹 +圜 +䦃 +镖 +镗 +镘 +镚 +镛 +镝 +镞 +镠 +氇 +氆 +憩 +穑 +篝 +篥 +篦 +篪 +篙 +盥 +劓 +翱 +魉 +魈 +徼 +歙 +膳 +膦 +膙 +鲮 +鲱 +鲲 +鲳 +鲴 +鲵 +鲷 +鲻 +獴 +獭 +獬 +邂 +鹧 +廨 +赟 +瘰 +廪 +瘿 +瘵 +瘴 +癃 +瘳 +斓 +麇 +麈 +嬴 +壅 +羲 +糗 +瞥 +甑 +燎 +燠 +燔 +燧 +濑 +濉 +潞 +澧 +澹 +澥 +澶 +濂 +褰 +寰 +窸 +褶 +禧 +嬖 +犟 +隰 +嬗 +颡 +缱 +缲 +缳 +璨 +璩 +璐 +璪 +螫 +擤 +壕 +觳 +罄 +擢 +薹 +鞡 +鞬 +薷 +薰 +藓 +藁 +檄 +檩 +懋 +醢 +翳 +礅 +磴 +鹩 +龋 +龌 +豳 +壑 +黻 +嚏 +嚅 +蹑 +蹒 +蹊 +蟥 +螬 +螵 +疃 +螳 +蟑 +嚓 +羁 +罽 +罾 +嶷 +黜 +黝 +髁 +髀 +镡 +镢 +镣 +镦 +镧 +镩 +镪 +镫 +罅 +黏 +簌 +篾 +篼 +簖 +簋 +鼢 +黛 +儡 +鹪 +鼾 +皤 +魍 +龠 +繇 +貘 +邈 +貔 +臌 +膻 +臆 +臃 +鲼 +鲽 +鳀 +鳃 +鳅 +鳇 +鳊 +螽 +燮 +鹫 +襄 +糜 +縻 +膺 +癍 +麋 +懑 +濡 +濮 +濞 +濠 +濯 +蹇 +謇 +邃 +襁 +檗 +擘 +孺 +隳 +嬷 +蟊 +鹬 +鍪 +鏊 +鳌 +鬈 +鬃 +瞽 +鞯 +鞨 +鞫 +鞧 +鞣 +藜 +藠 +藩 +醪 +蹙 +礓 +燹 +餮 +瞿 +曛 +颢 +曜 +躇 +蹚 +鹭 +蟛 +蟪 +蟠 +蟮 +鹮 +黠 +黟 +髅 +髂 +镬 +镭 +镯 +馥 +簟 +簪 +鼬 +雠 +艟 +鳎 +鳏 +鳐 +癞 +癔 +癜 +癖 +糨 +蹩 +鎏 +懵 +彝 +邋 +鬏 +攉 +攒 +鞲 +鞴 +藿 +蘧 +蘅 +麓 +醮 +醯 +酃 +霪 +霭 +霨 +黼 +嚯 +蹰 +蹶 +蹽 +蹼 +蹴 +蹾 +蹿 +蠖 +蠓 +蟾 +蠊 +黢 +髋 +髌 +镲 +籀 +籁 +齁 +魑 +艨 +鳓 +鳔 +鳕 +鳗 +鳙 +麒 +鏖 +羸 +㸆 +瀚 +瀣 +瀛 +襦 +谶 +襞 +骥 +缵 +瓒 +攘 +蘩 +蘖 +醴 +霰 +酆 +矍 +曦 +躅 +鼍 +巉 +黩 +黥 +黪 +镳 +镴 +黧 +纂 +璺 +鼯 +臜 +鳜 +鳝 +鳟 +獾 +孀 +骧 +瓘 +鼙 +醺 +礴 +颦 +曩 +鳢 +癫 +麝 +夔 +爝 +灏 +禳 +鐾 +羼 +蠡 +耱 +懿 +蘸 +鹳 +霾 +氍 +饕 +躐 +髑 +镵 +穰 +饔 +鬻 +鬟 +趱 +攫 +攥 +颧 +躜 +鼹 +癯 +麟 +蠲 +蠹 +躞 +衢 +鑫 +灞 +襻 +纛 +鬣 +攮 +囔 +馕 +戆 +爨 +齉 +亍 +尢 +彳 +卬 +殳 +𠙶 +毌 +邘 +戋 +圢 +氕 +伋 +仝 +冮 +氿 +汈 +氾 +忉 +宄 +讱 +扞 +圲 +圫 +芏 +芃 +朳 +朸 +𨙸 +邨 +吒 +吖 +屼 +屾 +辿 +钆 +仳 +伣 +伈 +癿 +甪 +邠 +犴 +冱 +邡 +闫 +汋 +䜣 +讻 +孖 +纩 +玒 +玓 +玘 +玚 +刬 +坜 +坉 +扽 +坋 +扺 +㧑 +毐 +芰 +芣 +苊 +苉 +芘 +芴 +芠 +芤 +杕 +杙 +杄 +杧 +杩 +尪 +尨 +轪 +坒 +芈 +旴 +旵 +呙 +㕮 +岍 +岠 +岜 +呇 +冏 +觃 +岙 +伾 +㑇 +伭 +佖 +伲 +佁 +飏 +狃 +闶 +汧 +汫 +𣲘 +𣲗 +沄 +沘 +汭 +㳇 +沇 +忮 +忳 +忺 +祃 +诇 +邲 +诎 +诐 +屃 +岊 +阽 +䢺 +阼 +妧 +妘 +𨚕 +纮 +驲 +纻 +纼 +玤 +玞 +玱 +玟 +邽 +邿 +坥 +坰 +坬 +坽 +弆 +耵 +䢼 +𦭜 +茋 +苧 +苾 +苠 +枅 +㭎 +枘 +枍 +矼 +矻 +匼 +旿 +昇 +昄 +昒 +昈 +咉 +咇 +咍 +岵 +岽 +岨 +岞 +峂 +㟃 +囷 +钐 +钔 +钖 +牥 +佴 +垈 +侁 +侹 +佸 +佺 +隹 +㑊 +侂 +佽 +侘 +郈 +舠 +郐 +郃 +攽 +肭 +肸 +肷 +狉 +狝 +饳 +忞 +於 +炌 +炆 +泙 +沺 +泂 +泜 +泃 +泇 +怊 +峃 +穸 +祋 +祊 +鸤 +弢 +弨 +陑 +陎 +卺 +乸 +妭 +姈 +迳 +叕 +驵 +䌹 +驺 +绋 +绐 +砉 +耔 +㛃 +玶 +珇 +珅 +珋 +玹 +珌 +玿 +韨 +垚 +垯 +垙 +垲 +埏 +垍 +耇 +垎 +垴 +垟 +垞 +挓 +垵 +垏 +拶 +荖 +荁 +荙 +荛 +茈 +茽 +荄 +茺 +荓 +茳 +𦰡 +茛 +荭 +㭕 +柷 +柃 +柊 +枹 +栐 +柖 +郚 +剅 +䴓 +迺 +厖 +砆 +砑 +砄 +耏 +奓 +䶮 +轵 +轷 +轹 +轺 +昺 +昽 +盷 +咡 +咺 +昳 +昣 +哒 +昤 +昫 +昡 +咥 +昪 +虷 +虸 +哃 +峘 +耑 +峛 +峗 +峧 +帡 +钘 +钜 +钪 +钬 +钭 +矧 +秬 +俫 +舁 +俜 +俙 +俍 +垕 +衎 +舣 +弇 +侴 +鸧 +䏡 +胠 +𦙶 +胈 +胩 +胣 +朏 +飐 +訄 +饻 +庤 +疢 +炣 +炟 +㶲 +洭 +洘 +洓 +洿 +㳚 +泚 +浈 +浉 +洸 +洑 +洢 +洈 +洚 +洺 +洨 +浐 +㳘 +洴 +洣 +恔 +宬 +窀 +扂 +袆 +祏 +祐 +祕 +叚 +陧 +陞 +娀 +姞 +姱 +姤 +姶 +姽 +枲 +绖 +骃 +彖 +骉 +恝 +珪 +珛 +珹 +琊 +玼 +珖 +珽 +珦 +珫 +珒 +珢 +珕 +珝 +埗 +垾 +垺 +埆 +垿 +埌 +埇 +莰 +茝 +鄀 +莶 +莝 +䓖 +莙 +栻 +桠 +桄 +梠 +栴 +梴 +栒 +酎 +酏 +砵 +砠 +砫 +砬 +硁 +恧 +翃 +郪 +𨐈 +辀 +辁 +剕 +赀 +哢 +晅 +晊 +唝 +哳 +哱 +冔 +晔 +晐 +晖 +畖 +蚄 +蚆 +帱 +崁 +峿 +崄 +帨 +崀 +赆 +钷 +眚 +甡 +笫 +倻 +倴 +脩 +倮 +倕 +倞 +倓 +倧 +衃 +虒 +舭 +舯 +舥 +瓞 +鬯 +鸰 +脎 +朓 +胲 +虓 +鱽 +狴 +峱 +狻 +眢 +勍 +痄 +疰 +痃 +竘 +羖 +羓 +桊 +敉 +烠 +烔 +烶 +烻 +涍 +浡 +浭 +浬 +涄 +涢 +涐 +浰 +浟 +浛 +浼 +浲 +涘 +悈 +悃 +悢 +宧 +窅 +窊 +窎 +扅 +扆 +袪 +袗 +袯 +祧 +隺 +堲 +疍 +𨺙 +陴 +烝 +砮 +㛚 +哿 +翀 +翂 +剟 +绤 +骍 +䂮 +琎 +珸 +珵 +琄 +琈 +琀 +珺 +掭 +堎 +堐 +埼 +掎 +埫 +堌 +晢 +掞 +埪 +壸 +㙍 +聍 +菝 +萚 +菥 +莿 +䓫 +勚 +䓬 +萆 +菂 +菍 +菼 +萣 +䓨 +菉 +䓛 +梼 +梽 +桲 +梾 +桯 +梣 +梌 +桹 +敔 +厣 +硔 +硙 +硚 +硊 +硍 +勔 +䴕 +龁 +逴 +唪 +啫 +翈 +㫰 +晙 +畤 +趼 +跂 +蛃 +蚲 +蚺 +啴 +䎃 +崧 +崟 +崞 +崒 +崌 +崡 +铏 +铕 +铖 +铘 +铚 +铞 +铥 +铴 +牻 +牿 +稆 +笱 +笯 +偰 +偡 +鸺 +偭 +偲 +偁 +㿠 +鄅 +偓 +徛 +衒 +舳 +舲 +鸼 +悆 +鄃 +瓻 +䝙 +脶 +脞 +脟 +䏲 +鱾 +猇 +猊 +猄 +觖 +𠅤 +庱 +庼 +庳 +痓 +䴔 +竫 +堃 +阌 +羝 +羕 +焆 +烺 +焌 +淏 +淟 +淜 +淴 +淯 +湴 +涴 +㥄 +惛 +惔 +悰 +惙 +寁 +逭 +袼 +裈 +祲 +谞 +艴 +弸 +弶 +隃 +婞 +娵 +婼 +媖 +婳 +婍 +婌 +婫 +婤 +婘 +婠 +绹 +骕 +絜 +珷 +琲 +琡 +琟 +琔 +琭 +堾 +堼 +揕 +㙘 +堧 +喆 +堨 +塅 +堠 +絷 +𡎚 +葜 +惎 +萳 +葙 +靬 +葴 +蒇 +蒈 +鄚 +蒉 +蓇 +萩 +蒐 +葰 +葎 +鄑 +蒎 +葖 +蒄 +萹 +棤 +棽 +棫 +椓 +椑 +鹀 +椆 +棓 +棬 +棪 +椀 +楗 +甦 +酦 +觌 +奡 +皕 +硪 +欹 +詟 +辌 +棐 +龂 +黹 +牚 +睎 +晫 +晪 +晱 +𧿹 +蛑 +畯 +斝 +喤 +崶 +嵁 +崾 +嵅 +崿 +嵚 +翙 +圌 +圐 +赑 +淼 +赒 +铹 +铽 +𨱇 +锊 +锍 +锎 +锓 +犇 +颋 +稌 +筀 +筘 +筜 +筥 +筅 +傃 +傉 +翛 +傒 +傕 +舾 +畬 +脿 +腘 +䐃 +腙 +腒 +鲃 +猰 +猯 +㺄 +馉 +鄗 +廋 +廆 +鄌 +粢 +遆 +旐 +焞 +欻 +𣸣 +溚 +溁 +湝 +渰 +湓 +㴔 +渟 +溠 +渼 +溇 +湣 +湑 +溞 +愐 +愃 +敩 +甯 +棨 +扊 +裣 +祼 +婻 +媆 +媞 +㛹 +媓 +媂 +媄 +毵 +矞 +缊 +缐 +骙 +瑃 +瑓 +瑅 +瑆 +䴖 +瑖 +瑝 +瑔 +瑀 +𤧛 +瑳 +瑂 +嶅 +瑑 +遘 +髢 +塥 +堽 +赪 +摛 +塝 +搒 +搌 +蒱 +蒨 +蓏 +蔀 +蓢 +蓂 +蒻 +蓣 +椹 +楪 +榃 +榅 +楒 +楞 +楩 +榇 +椸 +楙 +歅 +碃 +碏 +碈 +䃅 +硿 +鄠 +辒 +龆 +觜 +䣘 +暕 +鹍 +㬊 +暅 +跱 +蜐 +蜎 +嵲 +赗 +骱 +锖 +锘 +锳 +锧 +锪 +锫 +锬 +稑 +稙 +䅟 +筻 +筼 +筶 +筦 +筤 +傺 +鹎 +僇 +艅 +艉 +谼 +貆 +腽 +腨 +腯 +鲉 +鲊 +鲌 +䲟 +鲏 +雊 +猺 +飔 +觟 +𦝼 +馌 +裛 +廒 +瘀 +瘅 +鄘 +鹒 +鄜 +麀 +鄣 +阘 +煁 +煃 +煴 +煋 +煟 +煓 +滠 +溍 +溹 +滆 +滉 +溦 +溵 +漷 +滧 +滘 +滍 +愭 +慥 +慆 +塱 +裼 +禋 +禔 +禘 +禒 +谫 +鹔 +愍 +嫄 +媱 +戤 +戣 +缞 +耤 +瑧 +瑨 +瑱 +瑷 +瑢 +斠 +摏 +墕 +墈 +墐 +墘 +摴 +銎 +𡐓 +墚 +撖 +靽 +鞁 +蔌 +蔈 +蓰 +蔹 +蔊 +嘏 +榰 +榑 +槚 +𣗋 +槜 +榍 +疐 +酺 +酾 +酲 +酴 +碶 +䃎 +碨 +𥔲 +碹 +碥 +劂 +䴗 +夥 +瞍 +鹖 +㬎 +跽 +蜾 +幖 +嶍 +圙 +𨱏 +锺 +锼 +锽 +锾 +锿 +镃 +镄 +镅 +馝 +鹙 +箨 +箖 +劄 +僬 +僦 +僔 +僎 +槃 +㙦 +鲒 +鲕 +鲖 +鲗 +鲘 +鲙 +𩽾 +夐 +獍 +飗 +凘 +廑 +廙 +瘗 +瘥 +瘕 +鲝 +鄫 +熇 +漹 +漖 +潆 +漤 +潩 +漼 +漴 +㽏 +漈 +漋 +漻 +慬 +窬 +窭 +㮾 +褕 +禛 +禚 +隩 +嫕 +嫭 +嫜 +嫪 +㻬 +麹 +璆 +漦 +叇 +墣 +墦 +墡 +劐 +薁 +蕰 +蔃 +鼒 +槱 +鹝 +磏 +磉 +殣 +慭 +霅 +暵 +暲 +暶 +踦 +踣 +䗖 +蝘 +蝲 +蝤 +噇 +噂 +噀 +罶 +嶲 +嶓 +㠇 +嶟 +嶒 +镆 +镈 +镋 +镎 +镕 +稹 +儇 +皞 +皛 +䴘 +艎 +艏 +鹟 +𩾃 +鲦 +鲪 +鲬 +橥 +觭 +鹠 +鹡 +糇 +糈 +翦 +鹢 +鹣 +熛 +潖 +潵 +㵐 +澂 +澛 +瑬 +潽 +潾 +潏 +憭 +憕 +戭 +褯 +禤 +嫽 +遹 +璥 +璲 +璒 +憙 +擐 +鄹 +薳 +鞔 +黇 +蕗 +薢 +蕹 +橞 +橑 +橦 +醑 +觱 +磡 +𥕢 +磜 +豮 +鹾 +虤 +暿 +曌 +曈 +㬚 +蹅 +踶 +䗛 +螗 +疁 +㠓 +幪 +嶦 +𨱑 +馞 +穄 +篚 +篯 +簉 +鼽 +衠 +盦 +螣 +縢 +鲭 +鲯 +鲰 +鲺 +鲹 +亸 +癀 +瘭 +羱 +糒 +燋 +熻 +燊 +燚 +燏 +濩 +濋 +澪 +澽 +澴 +澭 +澼 +憷 +憺 +懔 +黉 +嬛 +鹨 +翯 +璱 +𤩽 +璬 +璮 +髽 +擿 +薿 +薸 +檑 +櫆 +檞 +醨 +繄 +磹 +磻 +瞫 +瞵 +蹐 +蟏 +㘎 +镤 +镥 +镨 +𨱔 +矰 +穙 +穜 +穟 +簕 +簃 +簏 +儦 +魋 +斶 +艚 +谿 +䲠 +鲾 +鲿 +鳁 +鳂 +鳈 +鳉 +獯 +䗪 +馘 +襕 +襚 +螱 +甓 +嬬 +嬥 +𦈡 +瓀 +釐 +鬶 +爇 +鞳 +鞮 +藟 +藦 +藨 +鹲 +檫 +黡 +礞 +礌 +𥖨 +蹢 +蹜 +蟫 +䗴 +嚚 +髃 +镮 +镱 +酂 +馧 +簠 +簝 +簰 +鼫 +鼩 +皦 +臑 +䲢 +鳑 +鳒 +鹱 +鹯 +癗 +𦒍 +旞 +翷 +冁 +䎖 +瀔 +瀍 +瀌 +襜 +䴙 +嚭 +㰀 +鬷 +醭 +蹯 +蠋 +翾 +鳘 +儳 +儴 +鼗 +𩾌 +鳚 +鳛 +麑 +麖 +蠃 +彟 +嬿 +鬒 +蘘 +欂 +醵 +颥 +甗 +𨟠 +巇 +酅 +髎 +犨 +𨭉 +㸌 +爔 +瀱 +瀹 +瀼 +瀵 +襫 +孅 +骦 +耰 +𤫉 +瓖 +鬘 +趯 +罍 +鼱 +鳠 +鳡 +鳣 +爟 +爚 +灈 +韂 +糵 +蘼 +礵 +鹴 +躔 +皭 +龢 +鳤 +亹 +籥 +鼷 +玃 +醾 +齇 +觿 +蠼 +𬣙 +𬇕 +𬣞 +𬘓 +𫭟 +𫭢 +𫇭 +𫐄 +𫵷 +𬇙 +𬣡 +𫸩 +𫘜 +𬘘 +𫘝 +𬨂 +𬀩 +𬀪 +𬬩 +𫍣 +𬣳 +𬩽 +𬮿 +𬯀 +𫰛 +𬳵 +𬳶 +𫠊 +𬍛 +鿍 +𬜬 +𪾢 +𪨰 +𫓧 +𬬮 +𬬱 +𬬭 +𬘡 +𬳽 +𬘩 +𫄧 +𪟝 +𬍤 +𫭼 +𬜯 +𬂩 +𫠆 +𬌗 +𫑡 +𪨶 +𬬸 +𬬻 +𬬹 +𬬿 +𬭁 +𫢸 +𫗧 +𬊈 +𬒈 +𬳿 +𫄨 +𬘫 +𫮃 +鿎 +𬱖 +𬟽 +𫓯 +𫟹 +𫟼 +𬇹 +𬍡 +𬤇 +𫍯 +𬤊 +𫍲 +𬯎 +𬘬 +𬘭 +𬴂 +𫘦 +𫟅 +𬘯 +𫘧 +𪣻 +𬃊 +𬷕 +𫐐 +𬹼 +𫶇 +𫖮 +鿏 +𬭊 +𫓶 +𬭎 +𫖯 +𬱟 +𫛭 +𫷷 +𬮱 +𬊤 +𬴃 +𫘨 +𬪩 +𬒔 +𬨎 +𫐓 +𫫇 +𫓹 +𬭚 +𬭛 +𬕂 +𬶋 +𬶍 +𫔶 +𫌀 +𫖳 +𫘪 +𫘬 +𫞩 +𪤗 +𬸘 +𬒗 +𫚖 +𬭤 +𫚕 +𬶐 +𬶏 +𬸚 +𬤝 +𬙂 +𬭩 +𬸣 +𫍽 +𬴊 +𬞟 +𫟦 +𬺈 +𫠜 +𪩘 +𬭬 +𬭯 +𫗴 +𬸦 +𫄷 +𬭳 +𬭶 +𫔍 +𬭸 +𬭼 +𫔎 +𬸪 +𬶟 +𬶠 +𬶨 +𫄸 +𬟁 +𬙊 +𬶭 +𬶮 +𬙋 +𬺓 +𫚭 +廠 +蔔 +兒 +幾 +幹 +虧 +纔 +與 +萬 +韆 +億 +個 +廣 +門 +義 +衛 +飛 +習 +馬 +鄉 +豐 +開 +無 +雲 +專 +藝 +廳 +區 +歷 +曆 +車 +貝 +岡 +見 +氣 +長 +僕 +幣 +僅 +從 +侖 +倉 +風 +烏 +鳳 +爲 +鬥 +憶 +計 +訂 +認 +譏 +醜 +隊 +辦 +鄧 +勸 +雙 +書 +擊 +撲 +節 +術 +厲 +龍 +滅 +軋 +東 +盧 +業 +舊 +帥 +歸 +葉 +電 +號 +衹 +隻 +嘰 +嘆 +們 +儀 +叢 +爾 +樂 +處 +鼕 +鳥 +務 +飢 +饑 +馮 +閃 +蘭 +匯 +彙 +頭 +漢 +寧 +討 +寫 +讓 +禮 +訓 +議 +訊 +記 +齣 +遼 +邊 +發 +髮 +聖 +對 +臺 +颱 +檯 +糾 +絲 +動 +鞏 +執 +擴 +掃 +場 +揚 +亞 +樸 +機 +權 +過 +協 +壓 +厭 +頁 +誇 +奪 +達 +夾 +軌 +堯 +劃 +邁 +畢 +貞 +師 +塵 +當 +噹 +籲 +嚇 +蟲 +麯 +團 +糰 +嗎 +嶼 +歲 +迴 +豈 +則 +剛 +網 +硃 +遷 +喬 +偉 +傳 +優 +傷 +價 +倫 +華 +僞 +嚮 +後 +會 +殺 +閤 +衆 +爺 +傘 +創 +雜 +負 +壯 +衝 +妝 +莊 +慶 +劉 +齊 +産 +閉 +問 +闖 +關 +燈 +湯 +興 +講 +諱 +軍 +訝 +許 +訛 +論 +訟 +農 +諷 +設 +訪 +訣 +尋 +盡 +儘 +導 +孫 +陣 +陽 +階 +陰 +婦 +媽 +戲 +觀 +歡 +買 +紅 +馱 +纖 +縴 +馴 +約 +級 +紀 +馳 +紉 +壽 +麥 +瑪 +進 +遠 +違 +韌 +運 +撫 +壇 +罎 +壞 +摳 +擾 +貢 +垻 +壩 +摺 +掄 +搶 +墳 +護 +殻 +塊 +聲 +報 +擬 +蕪 +葦 +蒼 +嚴 +蘆 +勞 +蘇 +囌 +極 +楊 +兩 +麗 +醫 +勵 +還 +殲 +來 +連 +軒 +鹵 +滷 +堅 +時 +縣 +裏 +嘔 +園 +曠 +圍 +噸 +郵 +睏 +員 +聽 +嗆 +嗚 +彆 +嶇 +崗 +帳 +財 +針 +釘 +亂 +體 +傭 +徹 +餘 +穀 +鄰 +腸 +龜 +猶 +狽 +條 +島 +飯 +飲 +係 +繫 +凍 +狀 +畝 +庫 +療 +應 +這 +廬 +閏 +閑 +間 +悶 +竈 +燦 +瀝 +淪 +滄 +溝 +滬 +瀋 +懷 +憂 +窮 +證 +啓 +評 +補 +識 +詐 +訴 +診 +詞 +譯 +靈 +層 +遲 +張 +際 +陸 +陳 +墜 +勁 +鷄 +緯 +驅 +純 +紗 +綱 +納 +駁 +縱 +紛 +紙 +紋 +紡 +驢 +紐 +環 +責 +現 +錶 +規 +攏 +揀 +擔 +頂 +擁 +勢 +攔 +擰 +撥 +擇 +蘋 +範 +莖 +樞 +櫃 +闆 +鬆 +槍 +楓 +構 +喪 +畫 +棗 +賣 +鬱 +礬 +礦 +碼 +厠 +奮 +態 +歐 +毆 +壟 +轟 +頃 +轉 +斬 +輪 +軟 +齒 +虜 +腎 +賢 +國 +暢 +嚨 +鳴 +羅 +幟 +嶺 +凱 +敗 +賬 +販 +貶 +購 +貯 +圖 +釣 +製 +颳 +俠 +僥 +偵 +側 +憑 +僑 +貨 +質 +徑 +捨 +覓 +貪 +貧 +膚 +腫 +脹 +骯 +脅 +魚 +獰 +備 +飾 +飽 +飼 +變 +龐 +廟 +瘧 +劑 +廢 +閘 +鬧 +鄭 +捲 +單 +爐 +淺 +濘 +瀉 +潑 +澤 +憐 +學 +寶 +寵 +審 +簾 +實 +試 +詩 +誠 +襯 +視 +話 +誕 +詭 +詢 +該 +詳 +肅 +録 +隸 +彌 +瀰 +陝 +駕 +參 +艱 +綫 +練 +組 +紳 +細 +駛 +織 +駒 +終 +駐 +絆 +駝 +紹 +繹 +經 +貫 +貳 +幫 +項 +挾 +撓 +趙 +擋 +墊 +擠 +揮 +薦 +帶 +繭 +蕩 +榮 +葷 +熒 +鬍 +蔭 +藥 +標 +棧 +棟 +欄 +檸 +樹 +鹹 +磚 +硯 +麵 +牽 +鷗 +殘 +軸 +輕 +鴉 +戰 +點 +臨 +覽 +竪 +嘗 +啞 +顯 +貴 +蝦 +蟻 +螞 +雖 +駡 +勛 +嘩 +響 +喲 +峽 +罰 +賤 +貼 +貽 +鈣 +鈍 +鈔 +鍾 +鐘 +鋼 +鈉 +鑰 +欽 +鈞 +鈎 +鈕 +氈 +氫 +選 +適 +種 +鞦 +復 +複 +倆 +貸 +順 +儉 +須 +鬚 +劍 +朧 +膽 +勝 +狹 +獅 +獨 +獄 +貿 +餌 +饒 +蝕 +餃 +餅 +巒 +彎 +將 +奬 +瘡 +瘋 +親 +閨 +聞 +閩 +閥 +閣 +養 +薑 +類 +婁 +總 +煉 +爍 +爛 +窪 +潔 +灑 +澆 +濁 +測 +瀏 +濟 +渾 +濃 +惱 +舉 +覺 +憲 +竊 +誡 +誣 +語 +襖 +誤 +誘 +誨 +説 +誦 +墾 +晝 +費 +遜 +隕 +險 +嬌 +賀 +壘 +綁 +絨 +結 +繞 +驕 +繪 +給 +絢 +駱 +絡 +絶 +絞 +駭 +統 +艷 +蠶 +頑 +盞 +撈 +載 +趕 +鹽 +損 +撿 +摯 +剝 +熱 +搗 +壺 +聶 +萊 +蓮 +獲 +穫 +惡 +噁 +瑩 +鶯 +檔 +橋 +樺 +樁 +樣 +賈 +礫 +礎 +顧 +轎 +較 +頓 +斃 +緻 +慮 +監 +緊 +黨 +曬 +曉 +嘮 +鴨 +暈 +鴦 +罷 +圓 +賊 +賄 +賂 +贜 +錢 +鉗 +鑽 +鉀 +鐵 +鈴 +鉛 +犧 +敵 +積 +稱 +筆 +債 +傾 +賃 +艦 +艙 +聳 +愛 +頒 +頌 +臟 +髒 +臍 +膠 +腦 +膿 +鴕 +鴛 +皺 +餓 +餒 +戀 +槳 +漿 +準 +癥 +齋 +離 +資 +競 +閲 +煩 +燒 +燭 +遞 +濤 +澇 +渦 +塗 +滌 +潤 +澗 +漲 +燙 +澀 +憫 +寬 +傢 +賓 +竅 +請 +諸 +諾 +讀 +誹 +襪 +課 +誰 +調 +諒 +諄 +談 +誼 +懇 +劇 +難 +預 +絹 +綉 +驗 +繼 +駿 +瑣 +擲 +據 +摻 +職 +蘿 +螢 +營 +蕭 +薩 +夢 +檢 +醖 +碩 +聾 +襲 +輔 +輛 +顱 +懸 +躍 +纍 +囉 +嘯 +嶄 +邏 +嬰 +銬 +鐺 +鋁 +銅 +銘 +鏟 +銀 +矯 +穢 +籠 +償 +軀 +釁 +銜 +盤 +鴿 +斂 +領 +臉 +獵 +餡 +館 +癢 +鏇 +閻 +闡 +蓋 +斷 +獸 +鴻 +漸 +淵 +漁 +澱 +滲 +慚 +懼 +驚 +慘 +慣 +謀 +諜 +謊 +諧 +禱 +禍 +謂 +諺 +謎 +彈 +墮 +隨 +隱 +嬸 +頗 +頸 +績 +緒 +續 +騎 +綽 +繩 +維 +綿 +綳 +綢 +綜 +綻 +緑 +綴 +瓊 +趨 +攬 +攙 +擱 +摟 +攪 +聯 +蔣 +韓 +橢 +確 +頰 +靂 +暫 +翹 +輩 +鑿 +輝 +賞 +睞 +噴 +疇 +踐 +遺 +鵑 +賦 +賭 +贖 +賜 +賠 +鑄 +鋪 +鏈 +銷 +鎖 +鋤 +鍋 +銹 +鋒 +鋅 +鋭 +鵝 +築 +篩 +儲 +懲 +禦 +釋 +臘 +魯 +憊 +饋 +饞 +裝 +蠻 +闊 +糞 +滯 +濕 +潰 +濺 +灣 +憤 +竄 +窩 +褲 +禪 +謝 +謡 +謗 +謙 +屬 +屢 +緬 +纜 +緝 +緞 +緩 +締 +縷 +騙 +編 +騷 +緣 +鵡 +攝 +擺 +襬 +攤 +鵲 +藍 +濛 +懞 +矇 +獻 +欖 +樓 +賴 +礙 +尷 +霧 +輻 +輯 +輸 +頻 +齡 +鑒 +蹺 +蝸 +錯 +錨 +錫 +鑼 +錘 +錐 +錦 +鍵 +鋸 +錳 +辭 +頽 +籌 +簽 +籤 +簡 +膩 +鵬 +騰 +鮑 +穎 +觸 +雛 +饃 +餾 +醬 +謄 +糧 +數 +滿 +濾 +濫 +灕 +濱 +灘 +譽 +窺 +寢 +謹 +謬 +闢 +縛 +縫 +纏 +繽 +贅 +墻 +衊 +藹 +檻 +釀 +願 +轄 +輾 +顆 +踴 +蠟 +蠅 +蟬 +賺 +鍬 +鍛 +鍍 +穩 +籮 +簫 +輿 +鮮 +饅 +瀟 +賽 +譚 +譜 +騾 +縮 +攆 +聰 +藴 +櫻 +飄 +黴 +瞞 +題 +囑 +鎮 +鎬 +鎊 +簍 +鯉 +鯽 +癟 +癱 +顔 +鯊 +瀾 +額 +譴 +鶴 +繚 +顛 +轍 +鸚 +贈 +鏡 +贊 +籃 +籬 +鯨 +癮 +辯 +瀕 +懶 +繮 +繳 +矚 +贍 +鰐 +辮 +贏 +驟 +囂 +鐮 +鰭 +鷹 +巔 +顫 +癬 +鱉 +鬢 +鱗 +躪 +贛 +鑲 +韋 +閂 +訃 +勱 +芻 +鄺 +訐 +訌 +訕 +訖 +馭 +璣 +壙 +捫 +薌 +厙 +釔 +傴 +倀 +傖 +獷 +獁 +鳬 +鄔 +餳 +懺 +謳 +詎 +訥 +紆 +紂 +紇 +紈 +璵 +摶 +塢 +㩳 +蕓 +藶 +莧 +萇 +蓯 +磯 +奩 +歟 +軔 +鄴 +嘸 +囈 +嚦 +暘 +唄 +幃 +峴 +嵐 +圇 +釗 +釙 +釕 +僉 +鳩 +鄒 +飩 +餼 +飪 +飫 +飭 +廡 +癤 +闈 +閎 +閔 +煬 +灃 +漚 +渢 +潙 +憮 +慪 +愾 +悵 +愴 +詁 +訶 +詛 +詆 +謅 +詔 +詒 +隴 +陘 +嫵 +嫗 +嬀 +剄 +紜 +紕 +紝 +綸 +紓 +瑋 +匭 +壚 +擓 +蘢 +蔦 +塋 +煢 +櫪 +梘 +棖 +樅 +碭 +甌 +郟 +軛 +鳶 +曇 +蟣 +黽 +嚀 +噝 +巋 +劌 +剴 +嶧 +釷 +釺 +釧 +釩 +釹 +釵 +儈 +儕 +儂 +劊 +慫 +糴 +戧 +膞 +邇 +梟 +餞 +飴 +癘 +瘍 +煒 +熰 +熗 +瀧 +瀘 +濼 +涇 +㥮 +懌 +誆 +誄 +詿 +詰 +詼 +鄆 +禕 +誅 +詵 +詬 +詮 +詣 +諍 +詫 +諢 +詡 +駑 +紺 +紲 +紱 +駟 +駙 +縐 +絀 +驛 +駘 +瓏 +頇 +埡 +撾 +撻 +賁 +壋 +撏 +莢 +貰 +蓽 +蕎 +薈 +薺 +堊 +滎 +犖 +蕁 +藎 +蓀 +蕒 +葤 +櫛 +櫳 +櫨 +櫟 +檉 +酈 +硨 +碸 +殤 +軲 +軻 +轤 +軼 +軫 +蠆 +覘 +瞘 +嘵 +嗶 +噦 +剮 +鄖 +噲 +噥 +嶢 +幀 +嶠 +貺 +鈈 +鈦 +鋇 +鈑 +鈐 +鎢 +鈁 +鈀 +篤 +儔 +儼 +儷 +腖 +臚 +脛 +鴇 +獪 +颮 +猻 +餉 +餄 +餎 +孿 +孌 +癧 +瘲 +颯 +闥 +閭 +闓 +閡 +熾 +烴 +浹 +澮 +滸 +潯 +濜 +慟 +懨 +愷 +惻 +惲 +誚 +禰 +誥 +誑 +鴆 +婭 +嬈 +懟 +絝 +驍 +驊 +絎 +絳 +駢 +頊 +璫 +琿 +塒 +塤 +堝 +贄 +蒔 +萵 +蕕 +鴣 +蒓 +橈 +楨 +榿 +檜 +邐 +礪 +礱 +軾 +輊 +輅 +鶇 +躉 +齔 +鸕 +矓 +嘜 +鴞 +蜆 +嗩 +嶗 +崍 +覬 +賅 +鈺 +鉦 +鈷 +鉢 +鈸 +鉞 +鉭 +鉬 +鈿 +鈾 +鉑 +鑠 +鉚 +鈰 +鉉 +鉈 +鉍 +鈮 +鈹 +鏺 +鐸 +氬 +筧 +頎 +徠 +膾 +鴟 +璽 +鴝 +獫 +裊 +餑 +欒 +攣 +癰 +痙 +頏 +閫 +鬮 +誾 +閬 +鄲 +燁 +燴 +燼 +淶 +漣 +潿 +慳 +諏 +諑 +禎 +諉 +諛 +諗 +諂 +誶 +媧 +嫻 +綆 +驪 +綃 +騁 +綏 +縧 +綈 +駸 +鷥 +燾 +璉 +麩 +擄 +摑 +鷙 +撣 +慤 +摜 +縈 +槤 +覡 +欞 +嗇 +匱 +硤 +磽 +鴯 +龔 +殞 +殮 +賚 +輒 +塹 +嘖 +囀 +嚙 +蹌 +蠣 +蠱 +蟶 +幘 +幗 +賕 +賑 +賒 +銠 +鉺 +鋏 +鐃 +銦 +鎧 +鍘 +銖 +銑 +鋌 +鏵 +銓 +鎩 +鉿 +銚 +鉻 +錚 +銫 +鉸 +銥 +銃 +銨 +銣 +鴰 +穠 +箋 +籩 +僨 +僂 +皚 +鴴 +艫 +龕 +玀 +獼 +餜 +餛 +鸞 +闍 +閾 +閹 +閶 +鬩 +閽 +閼 +羥 +糲 +燜 +漬 +瀆 +澠 +愜 +憚 +諶 +諫 +皸 +謔 +襠 +謁 +諤 +諭 +諼 +讒 +諳 +諦 +諞 +糶 +嬋 +綾 +騏 +綺 +緋 +緔 +騍 +緄 +騅 +綬 +綹 +綣 +綰 +驂 +緇 +靚 +輦 +黿 +頡 +撳 +蟄 +壪 +蔞 +櫝 +欏 +賫 +鵓 +鸝 +殫 +輥 +輞 +槧 +輟 +輜 +瞼 +躒 +蛺 +蟯 +螄 +蠐 +嘍 +嶸 +嶁 +賧 +鋙 +錸 +鏗 +鋥 +鋰 +鋯 +鋨 +銼 +鐧 +銻 +鋃 +鋦 +錒 +犢 +鵠 +篳 +牘 +儻 +儐 +儺 +嬃 +頜 +鵒 +魷 +魨 +魴 +潁 +颶 +觴 +熲 +餷 +餿 +褻 +臠 +癆 +癇 +賡 +頦 +鷳 +闌 +闃 +闋 +鵜 +憒 +嚳 +謨 +褳 +襇 +讜 +謖 +謚 +謐 +騭 +巰 +翬 +騖 +緙 +緗 +緘 +緹 +緲 +緦 +緱 +縋 +緡 +饗 +耮 +驁 +韞 +攄 +擯 +轂 +驀 +鶓 +薊 +蘺 +鎣 +頤 +櫚 +櫸 +磧 +磣 +鵪 +輳 +齟 +齙 +韙 +囁 +躂 +蹕 +躚 +躋 +噯 +鍺 +錛 +錡 +鍀 +錁 +錕 +錮 +鍁 +錈 +錠 +錙 +覦 +頷 +鮁 +鮃 +鮎 +鱸 +穌 +鮒 +鮐 +鵮 +颼 +饈 +鶉 +瘮 +闔 +闐 +闕 +灧 +瀅 +潷 +灤 +澦 +懾 +鱟 +騫 +竇 +謾 +謫 +嬡 +嬪 +縉 +縝 +縟 +轡 +騮 +縞 +縭 +縊 +縑 +騸 +覯 +韜 +靉 +攖 +薔 +藺 +鶘 +檳 +櫧 +釅 +殯 +霽 +轅 +齜 +齦 +瞜 +曖 +躊 +蟈 +鶚 +嚶 +羆 +賻 +罌 +鶻 +鍥 +鍇 +鍶 +鍔 +鍤 +鏘 +鎂 +鏤 +簀 +篋 +簞 +籙 +臏 +鮭 +鮪 +鱭 +鮫 +鱘 +饉 +鑾 +瘻 +闞 +鮝 +糝 +鷀 +瀲 +濰 +譖 +褸 +譙 +讕 +譎 +鶥 +嬙 +鶩 +驃 +縹 +縵 +縲 +纓 +驄 +繆 +繅 +耬 +瓔 +擷 +擼 +攛 +聵 +覲 +韃 +鞽 +蘄 +賾 +檣 +靨 +魘 +饜 +轆 +齬 +齪 +覷 +顒 +躓 +躑 +蠑 +螻 +顎 +嚕 +顓 +鑷 +鎘 +鎸 +鎳 +鎦 +鎰 +鎵 +鑌 +簣 +鷂 +鯁 +鱺 +鰱 +鰹 +鰣 +鯀 +鯇 +觶 +饊 +饌 +齏 +讞 +襤 +譫 +屨 +纈 +繕 +繒 +驏 +擻 +顳 +顢 +藪 +櫓 +櫞 +贋 +飆 +鏨 +轔 +蟎 +鐯 +鏢 +鏜 +鏝 +鏰 +鏞 +鏑 +鏃 +鏐 +氌 +穡 +魎 +鯪 +鯡 +鯤 +鯧 +鯝 +鯢 +鯛 +鯔 +獺 +鷓 +贇 +癭 +斕 +瀨 +顙 +繾 +繰 +繯 +蘚 +鷯 +齲 +齷 +躡 +蹣 +羈 +鐔 +鐝 +鐐 +鐓 +鑭 +鑹 +鏹 +鐙 +籪 +鷦 +鱝 +鰈 +鯷 +鰓 +鰍 +鰉 +鯿 +鷲 +懣 +鷸 +鰲 +韉 +顥 +鷺 +䴉 +髏 +鑊 +鐳 +鐲 +讎 +鰨 +鰥 +鰩 +癩 +攢 +靄 +躥 +髖 +髕 +鑔 +籟 +鰳 +鰾 +鱈 +鰻 +鱅 +讖 +驥 +纘 +瓚 +鼉 +黷 +黲 +鑣 +鑞 +臢 +鱖 +鱔 +鱒 +驤 +顰 +鱧 +癲 +灝 +鸛 +鑱 +趲 +顴 +躦 +饢 +戇 +戔 +訏 +訒 +釓 +俔 +閆 +澫 +訢 +訩 +詝 +紃 +纊 +瑒 +剗 +塸 +壢 +埨 +撝 +蔿 +榪 +軑 +軏 +咼 +㠣 +覎 +㑳 +颺 +閌 +潕 +湋 +澐 +浿 +諓 +禡 +詗 +詘 +詖 +屓 +彄 +紘 +馹 +馼 +紵 +紞 +駃 +紖 +瑲 +薴 +棡 +軝 +暐 +晛 +崬 +釴 +釤 +鍆 +鍚 +鄶 +獮 +飿 +嶨 +詷 +詪 +鄩 +鳲 +隑 +隮 +娙 +逕 +駓 +駔 +駉 +絅 +騶 +䮄 +紼 +紿 +瓅 +韍 +墶 +塏 +薘 +蕘 +蔄 +葒 +鳾 +龑 +軹 +軤 +轢 +軺 +睍 +曨 +噠 +鈃 +鈇 +鉅 +鋹 +釿 +錀 +鈧 +鈥 +鈄 +倈 +艤 +鶬 +颭 +餏 +湞 +溮 +滻 +褘 +絰 +駰 +絪 +駪 +綎 +綖 +驫 +勣 +璕 +𡑍 +䓣 +薟 +藭 +椏 +梜 +頍 +硜 +輄 +輈 +輇 +貲 +嗊 +曄 +暉 +鄳 +幬 +輋 +嶮 +贐 +鉥 +鉕 +鑪 +鉮 +鉊 +鉧 +僤 +鴒 +魛 +餗 +燖 +溳 +礐 +窵 +襏 +駼 +絺 +綌 +騂 +綄 +璡 +墠 +壼 +聹 +蘀 +勩 +罃 +檮 +棶 +厴 +䃮 +磑 +礄 +鴷 +齕 +頔 +廼 +凢 +亾 +枒 +屍 +匃 +匄 +紥 +紮 +疋 +殀 +讐 +觔 +兇 +宂 +㕥 +㠯 +栞 +佈 +佔 +呌 +敂 +冄 +坵 +僊 +怱 +悤 +冊 +夘 +戼 +牠 +妳 +嬭 +摃 +釦 +攷 +託 +衺 +衕 +弔 +喫 +囙 +㠶 +颿 +秊 +倣 +髣 +佀 +朶 +氷 +決 +併 +並 +竝 +汙 +汚 +異 +姦 +廵 +挵 +衖 +搤 +阯 +撦 +埳 +阬 +誌 +㕁 +卻 +刦 +刧 +刼 +芲 +蘤 +桿 +槓 +荳 +獃 +唫 +脗 +皁 +彿 +髴 +疘 +刪 +鉋 +鑤 +況 +牀 +恡 +棄 +洶 +汎 +災 +烖 +菑 +禩 +侷 +跼 +坿 +玅 +姉 +妬 +翫 +搨 +柺 +拕 +牴 +觝 +倖 +抝 +盃 +桮 +傑 +逩 +肎 +菓 +崐 +崑 +呪 +虖 +嘑 +謼 +詠 +㟁 +嵒 +巗 +巖 +雰 +稈 +咊 +嶽 +妷 +姪 +廹 +徃 +餚 +採 +寀 +唸 +週 +昬 +兎 +兔 +亯 +亱 +䘚 +淨 +劵 +匟 +㳒 +灋 +洩 +霑 +淚 +註 +恠 +箒 +屆 +絃 +圅 +旾 +珎 +掛 +垜 +艸 +茘 +査 +栢 +柵 +栁 +桺 +柹 +韮 +揹 +昰 +閧 +鬨 +冐 +暎 +嚥 +倃 +𠴰 +偺 +喒 +齩 +欬 +榘 +㑺 +儁 +敍 +敘 +肧 +脈 +䘑 +衇 +跡 +蹟 +砲 +礮 +薙 +鬀 +恆 +怳 +卹 +䘏 +賉 +婣 +畊 +揑 +綑 +輓 +恥 +躭 +晉 +棲 +覈 +慄 +翄 +脣 +槕 +㨪 +螡 +蟁 +㤙 +陗 +峩 +峯 +乗 +椉 +咲 +筍 +俛 +頫 +勌 +䠶 +躳 +慇 +拏 +㧱 +挐 +脃 +胷 +肐 +貍 +㽞 +畱 +淒 +悽 +蓆 +効 +傚 +涼 +缾 +菸 +煙 +淛 +湧 +誖 +猂 +醼 +讌 +㝠 +寃 +孃 +桒 +毬 +瑠 +璢 +瑯 +㨗 +搥 +搯 +蔆 +惏 +楳 +槑 +捄 +廂 +慽 +慼 +瞇 +埜 +畧 +虵 +稭 +棃 +犂 +迻 +媮 +兠 +舩 +慾 +綵 +腳 +𩓐 +夠 +豬 +貓 +湊 +減 +庻 +蔴 +菴 +朢 +睠 +觕 +麤 +釬 +銲 +痳 +殽 +婬 +滛 +湻 +㴱 +樑 +顇 +㝛 +窰 +窯 +琹 +欵 +墖 +趂 +隄 +愽 +揷 +揫 +煑 +朞 +㪚 +塟 +蔥 +蔕 +稜 +棊 +碁 +椶 +偪 +㕑 +廚 +廈 +鴈 +冣 +㝡 +晳 +鼃 +餧 +餵 +嗁 +諠 +㡌 +賸 +筴 +筞 +筩 +栰 +暠 +皜 +踰 +蝟 +㪟 +燄 +遊 +媿 +嘅 +庽 +窓 +牎 +牕 +窻 +徧 +僱 +帬 +裠 +強 +彊 +疎 +壻 +瓌 +䰟 +皷 +擕 +㩗 +㩦 +攜 +懃 +鞾 +幙 +㮣 +酧 +詶 +醻 +掽 +踫 +㼝 +盌 +磟 +覩 +倸 +㬉 +煗 +煖 +晻 +闇 +炤 +跥 +䗬 +蠭 +寘 +辠 +稺 +穉 +燬 +譭 +瘉 +癒 +顋 +骽 +猨 +蝯 +稟 +痺 +癡 +亷 +㢘 +韻 +泝 +遡 +昚 +躶 +臝 +羣 +㬪 +曡 +疊 +勦 +琍 +瓈 +𤋮 +熈 +牓 +搾 +謌 +堿 +鹻 +鹼 +矁 +燻 +髈 +𤺥 +辢 +旂 +𡚁 +潄 +砦 +詧 +嫰 +櫈 +撐 +墪 +譔 +鞵 +鞌 +蕋 +橤 +蘂 +醕 +譆 +跴 +蹤 +蜨 +蠍 +稾 +殭 +惪 +厀 +襃 +癅 +䊀 +餬 +潛 +癄 +顦 +鷰 +藷 +櫥 +螎 +蹏 +蟇 +譟 +簒 +彫 +琱 +鵰 +餹 +餻 +簷 +粦 +燐 +緐 +幑 +蹧 +粇 +穅 +臋 +籐 +繙 +飜 +孼 +蠏 +燿 +蝡 +稬 +穤 +惷 +覇 +鑵 +戹 +阨 +剳 +帀 +巵 +亙 +佇 +竚 +穽 +岅 +虯 +𦍑 +羗 +啎 +姙 +㘭 +袟 +袠 +逈 +㒺 +犛 +氂 +偘 +甕 +罋 +冺 +姍 +蝨 +琺 +瑇 +尅 +梔 +斮 +斲 +斵 +暱 +毘 +蝱 +吚 +哶 +峝 +粃 +竢 +狥 +秈 +烱 +㳄 +袵 +盇 +涖 +蒞 +碪 +蠔 +唕 +倐 +儵 +雋 +皐 +臯 +衂 +䶊 +臙 +獧 +痾 +皰 +湼 +澣 +濬 +塚 +襢 +娿 +勅 +勑 +戞 +廐 +廄 +眥 +覜 +勗 +啗 +噉 +傯 +挱 +㥫 +惥 +慂 +陻 +蕚 +萲 +蕿 +蘐 +藼 +櫂 +箠 +槨 +啑 +蹠 +蚘 +痐 +蛕 +蜖 +瘖 +遯 +醃 +飱 +冪 +簑 +枏 +柟 +檝 +楥 +矴 +椗 +嘷 +獋 +粺 +䈰 +諐 +齶 +堘 +疿 +雝 +秔 +稉 +槀 +搉 +廝 +叡 +嘠 +蜋 +筯 +篛 +麞 +糉 +緥 +璿 +髥 +臕 +餈 +剹 +橜 +罇 +蜺 +矙 +憇 +翺 +饍 +瞖 +羴 +羶 +爕 +繦 +騌 +鬉 +騣 +蔾 +䠀 +簮 +躕 +蹵 +䝔 +貛 +鼴 +麐 +塡 +あ +い +う +え +お +か +き +く +け +こ +さ +し +す +せ +そ +た +ち +つ +て +と +な +に +ぬ +ね +の +は +ひ +ふ +へ +ほ +ま +み +む +め +も +や +ゆ +よ +ら +り +る +れ +ろ +わ +を +ん +が +ぎ +ぐ +げ +ご +ざ +じ +ず +ぜ +ぞ +だ +ぢ +づ +で +ど +ば +び +ぶ +べ +ぼ +ぱ +ぴ +ぷ +ぺ +ぽ +ぁ +ぃ +ぅ +ぇ +ぉ +っ +ゃ +ゅ +ょ +ゎ +ゕ +ゖ +ア +イ +ウ +エ +オ +カ +キ +ク +ケ +コ +サ +シ +ス +セ +ソ +タ +チ +ツ +テ +ト +ナ +ニ +ヌ +ネ +ノ +ハ +ヒ +フ +ヘ +ホ +マ +ミ +ム +メ +モ +ヤ +ユ +ヨ +ラ +リ +ル +レ +ロ +ワ +ヲ +ン +ガ +ギ +グ +ゲ +ゴ +ザ +ジ +ズ +ゼ +ゾ +ダ +ヂ +ヅ +デ +ド +バ +ビ +ブ +ベ +ボ +パ +ピ +プ +ペ +ポ +ァ +ィ +ゥ +ェ +ォ +ッ +ャ +ュ +ョ +ヮ +ヵ +ヶ +ヷ +ヸ +ヹ +ヺ +・ +ー +ヽ +ヾ +ヿ +ア +イ +ウ +エ +オ +カ +キ +ク +ケ +コ +サ +シ +ス +セ +ソ +タ +チ +ツ +テ +ト +ナ +ニ +ヌ +ネ +ノ +ハ +ヒ +フ +ヘ +ホ +マ +ミ +ム +メ +モ +ヤ +ユ +ヨ +ラ +リ +ル +レ +ロ +ワ +ヲ +ン +゙ +゚ +ァ +ィ +ゥ +ェ +ォ +ッ +ャ +ュ +ョ +円 +気 +糸 +絵 +楽 +帰 +戸 +広 +黒 +図 +線 +読 +売 +歩 +毎 +亜 +悪 +圧 +扱 +囲 +為 +壱 +隠 +栄 +営 +駅 +塩 +縁 +艶 +応 +桜 +穏 +仮 +価 +箇 +ゑ +ゝ +ゞ +ヰ +ヴ +㈱ +両 +丼 +丿 +亀 +仏 +伝 +侶 +俤 +値 +倶 +倹 +偐 +偽 +働 +儛 +兌 +児 +冑 +冨 +凞 +処 +凪 +別 +剣 +剤 +剰 +劔 +労 +勧 +勲 +匁 +匂 +匲 +卍 +単 +厳 +収 +呂 +呉 +呑 +呰 +唖 +喚 +喩 +喰 +噛 +噺 +嚢 +囃 +団 +圀 +圏 +堀 +堺 +塀 +塁 +塙 +増 +墺 +壊 +壌 +壷 +変 +奨 +姫 +娯 +嫐 +嬢 +嬾 +孁 +宍 +実 +宮 +寔 +寛 +対 +専 +尭 +峠 +崋 +嶋 +巀 +巌 +巣 +巻 +帯 +幇 +庁 +廃 +廻 +弉 +弌 +弐 +弖 +弾 +従 +徳 +徴 +忯 +恵 +悩 +惣 +懐 +懽 +戦 +戯 +戻 +払 +抜 +択 +拝 +拠 +拡 +拵 +挙 +挿 +捗 +捜 +掟 +掲 +掻 +揃 +換 +揺 +摂 +撃 +撹 +斉 +斎 +旛 +旡 +晧 +晩 +暁 +暦 +曽 +杁 +杢 +杣 +杮 +枓 +枠 +枡 +柾 +栂 +栃 +桝 +桟 +桾 +梛 +梱 +梲 +梶 +椙 +検 +椥 +楕 +楡 +楢 +榊 +榎 +槇 +様 +槙 +槻 +樋 +権 +樫 +橿 +檥 +欅 +歎 +歓 +歯 +歳 +歴 +毀 +沖 +沢 +浄 +涙 +済 +渉 +渋 +渓 +渕 +満 +滝 +漑 +潅 +澁 +瀞 +瀬 +焔 +焼 +煇 +煕 +煥 +燗 +爼 +犠 +狛 +猟 +獏 +獣 +珊 +瑤 +甞 +畑 +畠 +畳 +畷 +畺 +痩 +癪 +発 +県 +眞 +砕 +碕 +礒 +禖 +禿 +稲 +穂 +穣 +竃 +竜 +竴 +笹 +筈 +筬 +筰 +箆 +箏 +箙 +篠 +篭 +簺 +籾 +粂 +粋 +粛 +粧 +糺 +紬 +絁 +経 +絖 +絣 +絽 +継 +続 +綟 +総 +縄 +縅 +縒 +縦 +繊 +繋 +繍 +繝 +繧 +纐 +纒 +罠 +罧 +罵 +羂 +羇 +羨 +聟 +聡 +聨 +聴 +脇 +脳 +膣 +膵 +臈 +臓 +臥 +舎 +舖 +舗 +舘 +芿 +苅 +茲 +荊 +荘 +莬 +莵 +菫 +萠 +蔵 +薗 +薫 +薬 +薭 +蘊 +蛍 +蝋 +蝿 +蟷 +衞 +衵 +袙 +袞 +袰 +袴 +袿 +裃 +裡 +裲 +褄 +褌 +襴 +襷 +覗 +覚 +覧 +観 +訳 +証 +諌 +諚 +諟 +諡 +諮 +譛 +譲 +讃 +豅 +豊 +豎 +賎 +賛 +贔 +躙 +躰 +転 +軽 +輌 +辥 +辺 +辻 +込 +逓 +遅 +遙 +邉 +郷 +酔 +醗 +醤 +醸 +釈 +鉄 +鉇 +鉤 +鉱 +鉾 +銈 +銕 +銭 +鋲 +鋳 +鋺 +錆 +錍 +錣 +錬 +錵 +鍑 +鍮 +鍼 +鎌 +鎗 +鎚 +鎹 +鐇 +鐚 +鐡 +鑁 +鑑 +鑚 +鑢 +閇 +関 +閦 +闘 +陥 +険 +隣 +隷 +雑 +雫 +霊 +靜 +靫 +靭 +靱 +鞄 +鞆 +頚 +頬 +頴 +頼 +顕 +顗 +餝 +饂 +駄 +駆 +駈 +騒 +験 +騨 +髄 +髙 +髪 +髷 +鯖 +鯰 +鯱 +鰒 +鰯 +鰰 +鳰 +鴎 +鴫 +鵄 +鵞 +鵺 +鶏 +鹸 +麁 +麺 +麿 +黌 +黙 +鼈 +齢 +龗 +縯 +蟅 +坖 +祂 +鼂 +鱚 +蛻 +屌 +呾 +煔 +吶 +扥 +蚖 +銂 +尃 +夋 +鵼 +徬 +寳 +彡 +舨 +湳 +麼 +鍈 +崈 +鱣 +盺 +拺 +瑥 +茷 +焻 +奀 +驎 +鱰 +砢 +痟 +廱 +僜 +瘺 +鱊 +擥 +嶰 +淓 +跅 +浵 +媗 +璦 +煠 +檊 +媃 +峅 +躄 +鉟 +塽 +蟴 +鯮 +弍 +烒 +鵵 +妑 +孋 +蚡 +恊 +輭 +廞 +產 +曅 +盜 +騤 +囪 +鱀 +茇 +葊 +逹 +狓 +崢 +趖 +凃 +羙 +鮸 +昞 +楿 +渽 +圗 +麪 +屇 +鍉 +葝 +沯 +爭 +幵 +筭 +寊 +銋 +貮 +鎭 +熺 +昜 +鍱 +墬 +愒 +磺 +嚈 +稘 +珮 +釆 +殑 +鍩 +䲁 +蕷 +鐿 +僡 +佹 +輶 +冴 +襶 +賔 +猙 +辧 +絛 +磾 +韁 +螔 +譳 +礑 +鋱 +魩 +嚗 +棆 +牆 +敟 +柶 +瓛 +魣 +巎 +轘 +襌 +枼 +鸌 +逺 +錏 +縡 +帢 +騄 +媼 +埅 +鄤 +萐 +祙 +旼 +詥 +鶲 +燉 +卲 +銱 +庲 +伱 +氽 +嵿 +挻 +煵 +窋 +鐤 +鮊 +鱬 +鰧 +嬤 +譞 +諲 +脭 +悳 +崘 +阭 +內 +袾 +冚 +壐 +咗 +礠 +孮 +痲 +埈 +肹 +鰮 +鮓 +濊 +塜 +凜 +蒢 +噰 +桼 +峍 +焴 +鶒 +鋮 +綠 +鶹 +熿 +毴 +咟 +嘥 +睺 +繡 +郎 +瘞 +鉶 +蔎 +秠 +緤 +蝀 +躝 +蟜 +繃 +囮 +墫 +乭 +胊 +濙 +瘓 +榣 +鑛 +鐫 +嶴 +甹 +坮 +銾 +蒭 +睜 +俋 +餠 +榢 +蓳 +盋 +堷 +鍏 +苝 +巛 +蚵 +暏 +熤 +嬨 +墎 +鏽 +戶 +菺 +膮 +熖 +睪 +栜 +捱 +榗 +鍷 +曧 +犽 +韑 +袓 +䖝 +焄 +喦 +髲 +疌 +㴪 +侊 +貐 +蕅 +禠 +蕑 +囯 +暊 +儞 +佋 +柎 +㐱 +鰤 +苳 +鱥 +謤 +遶 +眀 +鑀 +羋 +顏 +陜 +銩 +黶 +苼 +蒤 +棛 +儫 +咁 +抦 +衚 +棩 +焿 +脫 +麅 +玏 +埧 +淸 +黁 +淽 +彠 +鮨 +沜 +糀 +厓 +楧 +嶌 +簹 +檵 +鱇 +嶬 +廸 +卽 +樀 +贌 +酼 +籛 +沒 +晸 +諪 +蕡 +妏 +鄋 +蒍 +奧 +抇 +蓨 +薆 +鱷 +巘 +䝉 +亰 +寈 +槩 +誒 +麴 +蕟 +溎 +蘗 +榦 +斿 +暟 +炲 +拚 +娖 +繖 +橚 +寜 +爀 +饟 +悅 +鯏 +彜 +眾 +葯 +嬝 +埮 +獇 +馛 +溙 +瀦 +熼 +硓 +鈢 +樆 +輬 +鰜 +蔘 +渙 +澔 +嗮 +旉 +籜 +媊 +燘 +儚 +頹 +缽 +俽 +逨 +鱓 +郞 +歊 +杴 +珡 +杋 +醁 +鰏 +鵾 +鐽 +鮋 +巶 +荅 +薾 +囓 +蹻 +獎 +禑 +鎓 +榲 +僴 +綞 +尓 +敭 +曔 +褔 +鬅 +亊 +鏦 +蓘 +裬 +鱲 +薡 +鰗 +箑 +鬪 +縂 +璸 +甙 +茮 +辵 +岻 +覿 +滈 +鯶 +鑂 +囶 +舺 +溋 +拋 +菾 +敾 +虨 +綝 +蝍 +醂 +禨 +賹 +廧 +絕 +槗 +徫 +鎔 +曮 +蠂 +捒 +堈 +莕 +蓪 +敎 +禃 +櫱 +綧 +瀶 +逌 +浤 +碻 +刄 +逤 +剏 +氹 +菈 +娫 +蜛 +嵗 +糎 +螶 +譓 +鏳 +嵙 +瑊 +隲 +檨 +緈 +畵 +砯 +簗 +彅 +鰺 +騋 +窶 +嚒 +嵻 +尙 +頵 +槰 +虉 +醞 +巂 +彔 +偊 +畇 +鱨 +妸 +塲 +畐 +鈫 +錟 +磪 +摠 +彥 +璙 +囝 +寗 +耎 +鮡 +蘓 +弅 +焃 +飥 +戙 +塰 +儱 +槺 +噏 +魟 +禵 +佧 +咘 +盪 +瑈 +鉲 +睭 +鏌 +鼇 +郋 +魮 +朖 +滽 +渃 +滙 +熯 +醿 +鎅 +褀 +鬬 +巄 +螥 +眜 +釚 +柉 +壎 +峇 +姸 +唭 +鮜 +鈖 +嫈 +壄 +洤 +黃 +伕 +堦 +嶔 +鮰 +鞞 +漎 +鉓 +鮗 +壴 +阝 +妀 +矽 +獢 +倗 +銪 +鴓 +橒 +凈 +哖 +屚 +偍 +瑺 +媯 +淍 +驌 +椇 +赬 +薐 +糹 +碽 +濲 +釭 +晭 +纕 +寖 +閞 +歿 +呎 +鶆 +屄 +櫿 +犎 +旲 +㙟 +龎 +翜 +螾 +說 +衜 +泆 +軎 +鵂 +荎 +嚧 +硂 +桖 +褭 +筊 +鰷 +秳 +戩 +轀 +鬹 +飬 +卋 +暸 +狦 +搢 +娋 +鏴 +溫 +毉 +淰 +謩 +餺 +鵙 +鳽 +鮀 +狶 +氻 +轝 +妺 +袛 +蓭 +梂 +娛 +牼 +稅 +兿 +玾 +煚 +僩 +鶿 +鬄 +崠 +鉆 +鯓 +蚢 +庀 +鵟 +坣 +殼 +悞 +熅 +敻 +鍠 +曶 +愼 +搳 +姃 +砳 +槼 +臞 +韾 +靑 +鸊 +薲 +虛 +蠄 +啟 +鶺 +苺 +滾 +褞 +仺 +胇 +憻 +郳 +烉 +驩 +冇 +枖 +夌 +搵 +匸 +盨 +櫾 +霤 +麊 +貒 +噓 +嗢 +笩 +晈 +冂 +銳 +毿 +慜 +囧 +閜 +娸 +庢 +壆 +馯 +桱 +兗 +葃 +侅 +煐 +鐦 +藸 +鷎 +嵰 +逎 +弒 +匋 +鐭 +廔 +砩 +孆 +灴 +伷 +兪 +鴗 +澯 +幚 +旙 +勻 +礽 +婑 +鱮 +娍 +銶 +吳 +鍟 +仼 +鳧 +彞 +娽 +昛 +鰼 +剎 +佉 +鉏 +偸 +鰆 +讙 +橪 +啱 +岀 +孻 +釪 +乹 +鈳 +漇 +檦 +埻 +祿 +爌 +禇 +鱵 +㸃 +梉 +燝 +霙 +炁 +飮 +蠙 +勷 +鵎 +儥 +鐠 +唻 +廰 +嚿 +嵕 +墱 +紑 +搖 +瘜 +皝 +鸑 +瀁 +粵 +撚 +巑 +梀 +啯 +眛 +諴 +夊 +僙 +鍝 +裖 +鮣 +凬 +飡 +灊 +橓 +嫳 +筳 +咑 +粍 +瓑 +璌 +伃 +閰 +傜 +黐 +謢 +驒 +橫 +蛯 +寕 +蠵 +瞓 +旳 +翏 +硏 +寯 +韡 +楤 +鰃 +朿 +侞 +鵯 +愨 +祹 +厔 +丌 +盩 +謏 +魕 +啣 +閱 +曺 +枛 +罉 +卐 +樻 +鷉 +鯒 +鋡 +磱 +枱 +攴 +蠷 +穈 +嚟 +檽 +趐 +奐 +鋐 +檇 +薀 +峼 +咭 +訔 +韠 +鑴 +鸐 +唃 +捦 +鸜 +誴 +罳 +璄 +暃 +夀 +賨 +鞥 +鈊 +灡 +鮍 +懮 +籣 +昐 +陁 +襾 +鮠 +鈏 +囍 +婯 +艔 +貭 +䰾 +姁 +禼 +堖 +鋶 +仛 +鏷 +謜 +鑅 +忬 +蘶 +謠 +觙 +奫 +狟 +泩 +桙 +飈 +垰 +啍 +嚞 +鯕 +蒧 +榞 +徸 +璹 +揔 +欉 +魞 +菶 +玧 +鳯 +廍 +侚 +岰 +岧 +鋕 +凵 +彣 +崱 +媜 +倢 +鵐 +砋 +鷚 +鱠 +鮻 +繻 +摵 +贓 +磵 +錻 +痠 +粩 +胅 +奣 +塨 +瀠 +鸘 +啚 +娳 +霶 +壔 +峚 +甂 +廁 +覌 +鰂 +猳 +鱻 +盫 +裿 +杬 +歛 +澋 +蘞 +嵜 +尐 +旽 +鉌 +鎛 +豿 +凖 +榤 +禓 +龝 +悧 +鷟 +鮟 +吋 +喢 +岪 +吥 +漵 +頠 +豔 +巿 +鑨 +醣 +熳 +懍 +湥 +檡 +韺 +戱 +緖 +鐈 +凉 +緃 +鮹 +媐 +爯 +巆 +褍 +鐬 +昍 +扙 +鍳 +芛 +蟳 +嬅 +糬 +吔 +塭 +譿 +冧 +鏓 +嶪 +嗹 +椵 +姀 +閿 +褧 +錞 +玆 +笘 +篔 +萡 +鶡 +螐 +鮄 +鰟 +脷 +啲 +杤 +蓚 +尗 +娎 +殟 +淥 +蝚 +蓧 +彐 +嚤 +銍 +囒 +坶 +淩 +鶼 +鱂 +喼 +燫 +肏 +姵 +廌 +禟 +籝 +迵 +嵨 +堮 +蟌 +憍 +廕 +蜑 +緁 +唘 +竩 +崙 +璚 +粄 +栨 +罈 +梫 +貤 +藔 +蜯 +訁 +斖 +煶 +馦 +妠 +閟 +疕 +夆 +鎪 +膥 +澻 +嘢 +嚐 +靁 +鎻 +鰛 +穵 +烋 +縕 +褎 +疒 +壠 +溼 +圂 +咅 +鯭 +鯙 +磘 +玨 +珤 +朊 +蚼 +濶 +薞 +嚩 +丟 +嫺 +鯻 +椲 +鰕 +刂 +蠘 +踎 +瀴 +琁 +鰶 +瑴 +肜 +㐂 +欥 +媺 +竻 +讚 +𣇉 +裵 +緜 +廩 +齧 +叄 +俌 +厰 +滀 +錄 +鷫 +鯗 +攞 +姌 +蔝 +幷 +縤 +屻 +鯃 +雞 +纁 +嫲 +嵮 +屭 +嶃 +跩 +鋗 +蕢 +篊 +俬 +淎 +暻 +鏻 +憓 +玗 +溈 +笭 +糢 +勳 +閒 +沍 +咾 +鉷 +蘵 +俁 +崵 +毸 +苪 +掙 +鴡 +萭 +俴 +屜 +蒾 +艹 +剷 +慍 +朮 +枴 +氳 +猓 +甽 +箝 +譁 +贗 +迆 +鈽 +鍊 +鍰 +鏍 +靦 +餽 +丮 +丱 +仜 +仩 +伬 +伔 +仱 +伀 +伻 +佢 +佒 +侀 +侇 +佷 +佌 +佪 +侐 +侜 +俓 +侲 +俉 +侻 +侳 +俇 +倅 +倇 +倰 +倛 +倳 +倷 +俷 +倠 +偯 +偞 +偠 +偋 +偝 +偛 +偢 +偅 +偟 +偩 +偫 +傛 +傔 +傞 +傋 +傌 +傎 +傝 +偨 +傂 +傽 +傿 +僆 +傮 +僄 +僈 +傰 +僁 +傱 +僋 +僗 +僛 +僪 +僝 +僓 +僿 +儃 +儰 +僸 +僶 +僾 +儌 +僽 +儜 +儓 +儗 +儑 +儢 +儤 +儠 +儸 +儹 +儽 +冓 +冘 +冞 +凊 +凅 +凔 +刌 +刉 +刓 +刜 +刞 +刵 +刲 +剆 +刱 +剉 +剚 +剒 +剫 +剭 +剬 +剺 +剸 +剻 +剼 +劀 +劋 +劖 +劘 +劗 +劙 +劦 +勴 +匊 +匢 +匰 +匴 +匷 +匽 +卌 +卼 +厎 +厒 +厗 +厞 +厜 +厤 +厬 +厹 +吰 +吷 +吪 +呿 +咈 +呫 +呺 +呥 +呬 +呴 +茍 +咷 +咮 +咶 +哅 +咠 +咢 +唦 +唗 +唒 +哤 +唚 +唈 +哫 +唅 +唴 +啢 +唶 +啒 +啅 +唌 +唲 +喨 +喥 +喭 +噅 +喓 +喣 +啽 +喌 +嗃 +嗛 +嗋 +嗀 +喿 +喍 +嗏 +嗕 +嗈 +嘕 +嘒 +嗼 +嘐 +嘓 +嘂 +嗺 +嘝 +嘄 +嗿 +噈 +噊 +噆 +噚 +嘳 +嘽 +嘾 +噮 +噳 +噣 +噭 +噞 +嚌 +嚍 +嚃 +嚘 +嚜 +嚫 +嚪 +嚬 +嚲 +嚵 +嚽 +嚾 +囆 +囅 +囋 +囗 +圁 +圞 +圠 +坁 +坅 +坲 +坱 +垀 +坴 +垗 +垝 +垔 +垘 +垽 +垼 +埢 +埶 +堩 +堣 +塈 +堥 +塓 +塉 +塯 +塕 +塼 +墆 +塿 +塴 +墋 +塺 +墝 +墯 +壈 +墽 +壖 +壝 +壛 +壾 +壿 +夃 +夎 +夒 +夗 +奅 +奊 +奰 +奲 +奼 +妦 +妎 +妢 +妐 +妵 +姏 +姎 +㚷 +姡 +姺 +姼 +娭 +婐 +婟 +婥 +婓 +婗 +媔 +媟 +媢 +婸 +媦 +媥 +媬 +媕 +娷 +嫇 +嫋 +媰 +媻 +嫮 +嫥 +嫢 +嫛 +嫿 +嫴 +嫷 +嫶 +嬎 +嬓 +嬐 +嬲 +嬽 +孈 +屘 +孲 +孷 +宎 +宨 +寪 +寍 +寋 +寑 +寙 +寠 +寱 +尌 +尒 +尟 +尰 +尳 +屖 +屔 +屝 +屧 +屩 +屮 +屴 +岏 +岋 +岉 +岒 +岮 +岤 +岯 +岟 +岝 +峐 +峌 +峞 +峉 +峊 +峬 +峮 +峷 +崝 +崨 +崥 +崏 +崰 +崣 +崷 +嵃 +嵑 +崳 +崺 +嵂 +嵱 +嵣 +嵥 +嵞 +嶀 +嵽 +嶆 +嵺 +嵷 +嶊 +嶉 +嶈 +嵾 +嶕 +嶜 +嶡 +嶚 +嶞 +嶱 +嶩 +嶵 +嶭 +巃 +巏 +巕 +巟 +巹 +帊 +帗 +帟 +帣 +帠 +帤 +帩 +帾 +帴 +幏 +幎 +幓 +幩 +幝 +幠 +幧 +幨 +幦 +幭 +幰 +庂 +庉 +庌 +庈 +庰 +庛 +庣 +庨 +庮 +庪 +庬 +庴 +廅 +廇 +廘 +廗 +廎 +廜 +緳 +廦 +廥 +廮 +廯 +蠯 +廾 +弚 +弝 +弣 +弤 +弮 +弳 +彃 +彉 +彋 +彏 +彯 +彴 +彸 +彾 +徦 +徥 +徯 +徲 +徾 +徿 +忀 +忁 +忔 +忕 +忨 +忣 +忷 +忥 +怭 +怲 +怋 +怴 +怗 +怚 +怞 +怬 +怢 +怐 +怮 +怓 +怷 +怹 +恲 +恞 +恅 +恇 +恉 +恛 +恌 +恀 +恟 +悀 +悁 +悕 +悗 +悇 +悊 +悐 +悾 +悺 +惓 +惤 +惈 +悷 +惉 +悹 +惌 +惢 +惄 +愊 +愖 +愅 +惵 +愓 +惸 +惼 +惾 +慉 +慅 +愶 +愲 +愮 +愯 +愬 +慁 +慞 +慱 +慒 +慓 +慲 +憀 +慴 +慔 +慺 +慛 +憃 +慹 +憱 +憰 +憢 +憉 +憛 +憯 +憟 +憪 +憡 +憝 +憖 +懅 +憴 +懆 +懁 +憿 +憸 +憵 +憼 +懧 +懠 +懥 +懤 +懘 +懭 +懱 +懪 +懰 +懫 +懻 +戁 +戃 +戄 +戉 +戠 +酨 +戺 +扐 +扜 +扤 +扡 +扢 +抆 +抌 +抎 +抏 +扻 +抭 +抴 +拑 +抾 +抪 +抶 +抮 +挍 +挋 +挃 +拫 +拹 +挏 +挌 +拸 +挀 +拲 +捖 +挬 +挶 +揤 +捊 +挼 +挩 +捁 +挴 +捘 +捔 +捥 +掝 +掗 +掫 +掯 +捵 +掜 +捼 +掤 +掔 +掱 +揎 +揥 +揨 +揯 +揊 +揲 +揵 +摡 +揟 +揝 +揜 +揘 +揅 +揱 +搆 +搟 +搕 +搘 +搹 +搷 +搣 +搰 +搊 +搚 +摀 +搧 +搫 +摍 +摝 +摲 +摦 +摎 +摋 +摓 +摐 +摿 +摮 +摰 +撢 +撠 +撗 +撜 +撋 +撊 +撌 +撟 +擗 +擖 +擏 +擉 +撽 +擩 +擣 +擫 +擭 +擨 +擽 +擸 +攇 +攐 +攍 +攌 +攗 +攕 +攓 +攡 +攠 +攦 +攩 +攭 +攲 +攳 +敁 +敊 +敆 +敓 +敧 +敪 +敤 +敜 +敯 +敳 +敶 +敺 +敹 +敿 +斁 +斀 +斄 +斒 +斔 +斞 +斨 +斪 +斻 +旍 +旓 +旚 +旝 +旟 +昲 +昦 +昢 +晇 +晥 +晜 +晼 +晬 +暀 +暆 +暍 +暋 +暡 +暰 +暩 +曀 +曊 +曋 +曏 +曒 +曚 +曣 +曭 +朁 +朅 +朄 +朒 +朘 +朣 +朾 +朹 +朻 +朼 +杅 +杇 +杝 +杗 +枎 +杶 +枆 +枌 +柲 +枺 +枻 +柸 +柀 +柅 +柫 +柤 +柍 +柮 +柣 +柂 +柧 +栚 +桋 +桏 +栱 +栵 +栫 +栭 +栯 +栘 +栔 +梡 +梇 +梐 +桭 +梮 +楖 +梬 +梩 +桵 +梒 +椌 +椄 +棜 +棷 +棳 +棌 +椈 +楰 +棯 +椔 +棸 +楟 +楎 +楱 +楅 +楺 +楈 +楛 +楉 +楬 +椳 +楀 +楄 +楶 +楘 +榶 +槉 +榠 +榬 +榼 +榙 +榩 +榾 +榯 +槄 +榽 +榹 +槥 +槸 +樕 +樠 +槬 +槢 +樛 +樝 +槾 +樧 +槮 +樔 +槷 +橀 +樴 +橉 +橧 +樲 +橨 +橝 +橭 +橶 +樿 +橁 +檍 +檖 +檁 +檟 +橾 +檛 +檓 +檕 +檃 +櫅 +檹 +櫡 +櫠 +櫌 +櫑 +櫙 +櫋 +櫜 +櫐 +櫫 +櫬 +櫰 +櫹 +櫺 +櫼 +欃 +欋 +欈 +欐 +欑 +欘 +欨 +欴 +欯 +欭 +欱 +欶 +欳 +欷 +欿 +歂 +歈 +歍 +歋 +歕 +歔 +歜 +歠 +歭 +歾 +肂 +殈 +殏 +殔 +殗 +殙 +殠 +殥 +殢 +殦 +殧 +殰 +殶 +毃 +毄 +毈 +毇 +毊 +毚 +毞 +毦 +毤 +毨 +毣 +毰 +毲 +毻 +毼 +毾 +氁 +氀 +氄 +氠 +氶 +汃 +汒 +汏 +汍 +汸 +沋 +汱 +汯 +沕 +汦 +汳 +泬 +沶 +沬 +泧 +沷 +泭 +泲 +泒 +沴 +洟 +洊 +洀 +浺 +浶 +洍 +涒 +浘 +浢 +涊 +涆 +浧 +涗 +涳 +涬 +淢 +涷 +淔 +渀 +淈 +涾 +淊 +涽 +淭 +湆 +湇 +湅 +湢 +渿 +湁 +渜 +渳 +湀 +渻 +渮 +湨 +湡 +渱 +渨 +湠 +湱 +湩 +渹 +溛 +滖 +溓 +溔 +滒 +溰 +溾 +滜 +滵 +滱 +漃 +漥 +漮 +潎 +漙 +漧 +漘 +漒 +滭 +漊 +潳 +滮 +潀 +漰 +潃 +漅 +濆 +澒 +澅 +潚 +潠 +澖 +潶 +潬 +潒 +潐 +潗 +澓 +潝 +濇 +濎 +濈 +濄 +澞 +澨 +瀄 +濌 +澩 +濴 +濔 +濣 +濭 +濧 +濦 +瀇 +瀎 +濿 +瀀 +濻 +瀙 +瀖 +瀫 +瀡 +瀢 +瀩 +瀯 +瀷 +灂 +瀸 +瀿 +瀺 +灄 +灉 +灖 +灗 +灛 +灟 +灨 +灩 +灪 +炾 +炰 +烓 +烑 +缹 +焍 +烰 +焠 +焮 +焣 +煆 +煣 +煝 +熐 +熉 +熀 +熂 +熚 +燅 +燂 +熸 +燀 +燡 +爁 +爊 +爂 +爓 +爞 +爢 +爣 +牄 +牉 +牋 +牏 +牣 +牬 +牰 +牸 +牷 +犈 +犉 +犆 +犅 +犌 +犑 +犐 +犗 +犕 +犓 +犘 +犚 +犝 +犞 +犥 +犦 +犤 +犣 +犩 +犪 +犮 +犵 +犿 +狆 +狖 +狋 +狘 +狜 +狔 +狚 +狌 +狑 +狊 +狤 +狫 +狪 +狣 +猀 +狾 +猑 +猘 +猈 +狿 +猏 +猋 +猒 +猧 +猲 +猭 +猦 +猣 +猵 +猼 +獂 +獀 +獊 +獑 +獌 +獘 +獞 +獟 +獝 +獛 +獡 +獩 +獦 +獥 +獳 +獶 +獽 +獿 +玂 +玁 +玈 +玊 +玔 +珓 +珶 +琖 +瑵 +璊 +瑽 +璅 +瑿 +璗 +瓁 +瓋 +瓝 +瓟 +瓡 +瓥 +瓨 +瓬 +瓵 +瓾 +瓽 +甀 +甃 +甈 +甋 +甐 +甒 +甔 +甖 +甝 +甮 +甿 +畟 +畣 +畽 +疀 +疧 +痁 +疻 +痀 +痎 +痏 +痋 +痌 +痑 +痚 +痡 +痝 +痗 +痯 +瘏 +痷 +痸 +痻 +瘈 +瘑 +瘝 +瘣 +瘯 +瘱 +瘽 +癈 +癉 +癙 +癐 +癓 +癠 +癵 +癹 +皊 +皏 +皫 +皯 +皵 +皻 +皽 +皾 +盄 +盓 +盝 +盬 +盭 +盳 +眃 +眅 +盻 +眝 +眐 +眓 +眒 +眣 +眑 +眕 +眹 +眱 +眲 +眴 +眳 +眽 +睆 +睅 +睊 +睋 +睌 +睕 +睟 +睒 +睖 +睩 +睧 +睔 +瞁 +睼 +瞂 +睮 +睯 +瞏 +瞉 +瞚 +瞝 +瞡 +瞛 +瞲 +瞷 +瞶 +瞴 +矂 +矉 +矊 +矌 +矎 +矏 +矐 +矔 +矕 +矘 +矠 +矱 +矲 +矹 +矺 +砅 +砐 +砏 +砎 +砨 +硈 +硉 +硠 +硥 +硱 +硰 +硩 +碔 +碄 +碅 +碆 +硾 +碫 +碞 +磍 +磌 +磎 +磈 +磃 +磝 +磩 +磥 +磞 +磛 +磳 +磼 +磿 +礔 +礉 +礝 +礛 +礜 +礥 +礣 +礧 +礨 +礭 +礿 +祌 +祅 +祔 +祒 +祑 +祤 +祩 +祪 +祣 +祫 +祡 +祴 +祳 +禂 +禗 +禜 +禫 +禭 +禬 +禴 +禷 +禸 +歶 +秅 +秏 +秖 +秎 +秮 +秪 +秺 +秶 +稊 +稒 +稫 +穊 +稰 +稯 +穋 +穛 +穖 +穧 +穨 +穮 +穬 +穭 +穱 +穾 +窆 +窉 +窌 +窏 +窔 +窐 +窙 +窢 +窞 +窫 +窲 +窴 +窱 +窾 +竀 +竁 +竷 +笐 +笓 +笅 +笵 +笻 +笴 +笰 +笢 +笝 +笲 +筄 +筡 +箈 +箊 +箌 +箛 +箎 +箘 +箄 +箷 +箾 +篎 +箯 +箹 +篞 +篣 +篧 +篕 +篨 +篹 +簅 +篲 +篿 +篻 +簎 +篴 +簂 +簁 +篸 +篽 +簜 +簩 +簙 +簭 +簦 +簨 +簢 +簥 +簳 +簼 +簬 +簻 +籉 +籈 +籊 +籔 +籗 +籧 +籦 +籯 +籺 +籸 +籹 +粊 +粔 +粻 +糔 +糪 +糱 +糷 +紎 +紟 +紒 +紽 +紸 +紶 +紩 +絇 +紾 +絘 +絯 +絓 +絧 +絏 +絭 +絫 +綀 +綍 +絿 +綅 +絻 +絼 +綔 +綷 +緂 +綪 +緀 +緅 +緎 +緆 +緌 +綯 +綼 +緷 +緛 +緪 +緧 +縃 +緺 +緶 +緰 +縗 +縌 +縓 +縎 +縜 +縚 +縏 +縼 +繂 +縳 +顈 +繈 +縸 +縪 +繉 +繀 +縩 +緵 +縰 +縿 +縶 +繜 +繐 +繣 +繘 +繢 +繟 +繑 +繠 +繶 +繵 +繸 +繷 +繺 +繲 +繴 +纀 +纇 +纋 +纆 +纑 +纗 +纚 +缿 +罊 +罏 +罜 +罞 +罝 +罛 +罣 +罥 +罦 +罭 +罫 +罬 +罻 +罼 +罺 +罿 +羃 +羉 +羍 +羒 +羜 +羛 +羢 +羠 +羦 +羬 +羭 +羵 +羳 +羷 +羺 +羾 +翋 +翍 +翐 +翑 +翇 +翢 +翣 +翭 +翪 +翨 +翴 +翲 +翽 +翿 +耟 +耞 +耡 +耴 +耾 +耹 +聇 +聈 +聑 +聏 +聝 +肕 +肙 +肒 +肣 +肵 +胘 +胑 +胐 +胕 +胉 +胏 +胹 +胵 +脁 +胻 +脀 +胾 +胔 +脰 +脥 +脤 +脙 +脡 +脕 +脧 +腃 +腏 +腄 +腇 +脽 +腍 +腤 +腷 +腜 +腛 +腢 +腲 +朡 +腞 +腶 +膉 +膆 +膃 +膇 +膍 +膌 +膋 +膟 +膕 +膢 +膱 +膹 +膫 +膰 +膬 +膴 +膲 +臇 +膷 +臄 +臅 +臒 +臐 +臗 +臛 +臡 +臦 +臩 +臮 +臲 +臷 +臸 +臿 +舋 +舑 +舕 +舝 +舡 +舼 +舽 +艀 +艂 +艓 +艒 +艐 +艑 +艕 +艛 +艵 +艼 +芀 +芐 +芅 +芓 +芔 +苀 +芚 +芵 +芧 +芞 +芺 +苙 +苨 +苖 +苬 +苲 +苵 +苶 +茙 +茥 +茿 +茦 +茢 +荂 +茪 +荍 +茖 +茤 +茠 +茩 +茻 +莐 +莣 +莍 +荺 +莤 +荴 +莏 +莁 +荵 +莔 +莃 +莌 +莋 +荾 +莥 +菨 +萒 +菧 +菤 +菆 +菣 +菿 +菋 +菎 +菵 +萉 +菞 +菳 +菕 +蓱 +萿 +葹 +葥 +葀 +葧 +萰 +葍 +葽 +蔇 +葞 +萷 +萺 +萴 +葅 +菙 +葋 +萯 +葂 +葟 +葌 +蓎 +蒬 +蒮 +蒫 +蒪 +蒚 +蒝 +蓌 +蒛 +蒩 +蒘 +蒶 +蒠 +蔤 +蔏 +蔩 +蔉 +蔍 +蔧 +蔜 +蓻 +蓺 +蓴 +蔪 +蓲 +蓷 +蓫 +蔒 +蓩 +蔖 +蓾 +蔨 +蔮 +蔂 +蓶 +蔱 +蓹 +蔠 +蔰 +蕫 +蕍 +蕀 +蕆 +蕄 +蕇 +蕣 +蕛 +蕱 +蕵 +蕮 +蕧 +蕠 +蕦 +蕝 +薃 +薧 +薕 +薠 +薋 +薣 +薚 +蕼 +薉 +蕸 +薎 +薖 +薍 +薝 +薂 +藆 +藀 +藃 +藂 +薵 +薽 +藇 +藄 +藋 +藈 +藅 +薱 +薶 +藒 +藫 +藱 +藙 +藡 +藚 +藗 +藲 +藬 +藘 +藣 +藑 +藰 +蘁 +藾 +蘛 +蘉 +蘌 +蘪 +蘦 +蘟 +蘣 +蘜 +蘙 +蘮 +蘡 +蘠 +蘥 +蘴 +蘳 +蘬 +虀 +蘹 +蘱 +蘻 +蘾 +虃 +虆 +虇 +虈 +虌 +虋 +虙 +虡 +虣 +虩 +虪 +虰 +虭 +虴 +蚑 +蚞 +蚇 +蚗 +蚚 +蚅 +蚥 +蚙 +蚿 +蚷 +蛂 +蛁 +蛅 +蛈 +蚹 +蚳 +蚸 +蛌 +蚻 +蛢 +蛦 +蛓 +蛣 +蛚 +蛪 +蛝 +蛫 +蛜 +蛬 +蛗 +蜄 +蛷 +蜌 +蛖 +蛵 +蜁 +蛶 +蜳 +蝫 +蜙 +蝃 +蜬 +蝁 +蝆 +蜠 +蜲 +蜪 +蜭 +蜼 +蜵 +蝂 +蜦 +蜧 +蜸 +蜤 +蜰 +蝖 +蝷 +蟡 +蝳 +蝔 +蝛 +蝒 +蝑 +蝞 +蝭 +蝪 +蝐 +蝝 +蝬 +蝺 +蝜 +螛 +螏 +螓 +螒 +螁 +螖 +螘 +蝹 +螇 +螑 +螝 +螜 +螚 +螪 +螰 +螹 +螼 +螮 +蟉 +蟃 +蟂 +螷 +螴 +螿 +螸 +蟞 +蟧 +蟦 +蟢 +蟟 +蟤 +蟔 +蟓 +蟭 +蟘 +螤 +蟗 +蟙 +蠁 +蟨 +蠀 +蟺 +蠉 +蠌 +蟼 +蠈 +蟿 +蠗 +蠩 +蠝 +蠛 +蠠 +蠤 +蠜 +蠫 +蠬 +蠨 +蠦 +蠪 +蠥 +蠰 +蠮 +蠳 +蠸 +蠾 +蠽 +蠿 +衁 +衈 +衋 +衧 +衪 +衭 +衶 +袀 +衱 +衯 +袃 +袉 +袕 +袨 +袚 +袑 +袡 +袘 +袧 +袬 +袌 +袺 +裗 +袹 +袸 +裀 +袶 +袽 +袲 +裋 +裍 +裞 +裚 +裷 +裧 +裺 +裮 +裶 +裯 +裻 +褁 +褅 +褋 +褗 +褆 +褖 +褑 +褦 +褮 +褱 +褢 +褩 +褵 +褼 +褾 +襒 +褷 +襂 +褽 +襓 +襋 +襆 +襐 +襛 +襗 +襡 +襘 +襝 +襣 +襭 +襩 +襮 +襳 +襹 +襺 +覂 +覅 +覕 +覛 +覝 +覢 +覤 +覣 +覭 +覮 +覶 +觓 +觤 +觡 +觠 +觢 +觩 +觰 +觬 +觲 +觷 +觺 +觻 +觼 +觾 +訑 +訰 +訧 +訬 +訞 +詍 +訹 +詙 +詀 +詄 +詅 +訿 +誂 +詻 +誃 +誫 +誙 +誋 +諆 +誸 +諔 +諕 +誻 +諀 +諅 +諵 +諝 +諰 +諈 +謞 +謘 +謑 +謋 +謒 +謕 +謍 +謈 +謪 +謧 +謣 +謰 +謵 +譇 +謯 +謱 +謥 +謷 +謦 +譐 +譈 +譊 +譀 +譋 +譕 +譑 +譠 +譪 +譝 +譨 +譣 +譥 +譹 +譸 +譅 +譺 +譻 +譾 +讄 +讂 +讆 +讋 +讔 +讘 +讟 +谹 +谻 +谽 +谾 +豃 +豋 +豍 +豏 +豗 +豜 +豝 +豟 +豥 +豤 +豦 +豭 +豰 +豲 +豱 +豯 +豵 +豷 +豶 +豻 +豽 +貁 +貀 +貄 +貏 +貑 +貕 +貙 +貗 +貜 +貣 +貾 +賌 +賥 +賟 +賙 +賵 +賮 +贆 +贕 +贙 +赨 +赩 +赮 +赸 +趀 +趌 +趎 +趏 +趍 +趓 +趠 +趜 +趡 +趥 +趧 +趬 +趪 +趭 +趫 +趮 +趷 +趹 +跘 +跓 +跍 +跇 +跜 +跕 +跙 +跈 +跰 +跠 +跮 +跦 +跢 +跧 +跲 +跫 +踂 +跿 +踍 +踃 +踇 +踆 +跾 +踠 +踥 +踤 +踡 +踕 +踛 +踖 +踑 +踙 +踧 +踘 +踓 +踳 +踾 +踸 +踼 +蹎 +蹍 +蹓 +蹗 +蹖 +蹞 +蹥 +蹛 +蹡 +蹝 +蹔 +蹸 +蹳 +蹪 +躆 +躈 +躖 +躗 +躟 +躠 +躤 +躣 +躩 +躨 +躽 +軓 +軘 +軞 +軯 +軷 +軦 +軮 +軥 +軵 +軧 +軨 +軶 +軱 +軬 +輆 +軿 +輁 +輀 +輂 +輐 +輑 +輤 +輘 +輚 +輠 +輣 +輖 +輗 +輮 +輵 +輲 +輹 +輷 +輴 +轃 +轇 +轈 +轒 +轑 +轏 +轐 +轓 +轙 +轖 +轗 +轕 +轚 +轞 +轛 +轠 +辴 +迉 +迒 +迋 +迍 +迖 +迣 +迡 +迾 +迿 +逜 +逿 +遝 +遳 +遰 +遻 +邆 +邅 +遾 +邍 +邔 +邟 +邥 +邞 +邧 +郱 +郕 +郖 +郠 +郙 +郣 +郥 +郘 +郰 +郲 +郔 +鄬 +郼 +鄈 +郹 +郻 +鄁 +鄇 +郺 +鄐 +鄍 +鄏 +鄎 +鄟 +鄝 +鄡 +鄛 +鄨 +鄪 +鄦 +鄮 +鄵 +鄸 +鄻 +鄾 +酀 +酁 +酄 +酇 +酖 +酘 +酓 +酟 +酳 +醆 +醊 +醓 +醙 +醟 +醥 +醧 +醰 +醱 +醷 +醲 +醳 +醹 +醽 +釂 +釃 +釢 +釱 +釳 +釸 +鈚 +鈌 +鈒 +釽 +鈆 +鉒 +鉠 +鉯 +鈶 +鉼 +銤 +銛 +銔 +鉹 +銗 +鋄 +鋀 +鋟 +鋘 +鋩 +鋝 +鋂 +鋊 +錧 +錼 +錭 +錎 +鋋 +鎡 +鎃 +鎯 +鍖 +鍜 +鍐 +鍭 +鍌 +鎒 +鎷 +鎝 +鎉 +鎎 +鎞 +鏏 +鏂 +鏚 +鏬 +鏙 +鐋 +鐏 +鏾 +鐕 +鐨 +鐍 +鐀 +鐎 +鐖 +鐻 +鐶 +鑐 +鑋 +鑕 +鑮 +鑯 +钂 +钀 +钁 +钃 +镺 +镻 +镼 +镽 +閈 +閍 +閺 +閵 +闀 +闉 +闅 +閷 +闒 +闑 +闚 +闛 +闠 +闟 +闤 +阞 +阢 +阤 +阠 +阰 +阹 +阸 +阺 +陏 +陓 +陊 +陼 +陭 +陫 +隇 +陾 +隉 +隒 +隓 +隞 +隤 +隿 +雂 +雈 +雓 +雔 +雗 +雚 +雟 +雘 +雺 +雽 +雿 +霂 +霋 +霒 +霐 +霠 +霣 +霢 +霩 +霫 +霬 +霮 +霵 +霿 +靆 +靃 +靪 +靮 +靷 +靲 +靾 +鞃 +鞀 +鞂 +靻 +鞊 +鞎 +鞈 +鞙 +鞗 +鞚 +鞜 +鞤 +鞪 +鞷 +鞶 +鞹 +鞻 +鞿 +韄 +韅 +韇 +韎 +韐 +韏 +韕 +韔 +韗 +韝 +韟 +韣 +韥 +韰 +韱 +韹 +韽 +頄 +頖 +頞 +頝 +頩 +頨 +頯 +頲 +顁 +顄 +顊 +顉 +顅 +顐 +顑 +顜 +顝 +顠 +顣 +顟 +顤 +顪 +顩 +顲 +颬 +颲 +颸 +颽 +颻 +颾 +飁 +飂 +飉 +飋 +飌 +飣 +飶 +餂 +餀 +飺 +餔 +餖 +餕 +餤 +餟 +餥 +餫 +餪 +餲 +餯 +餭 +餱 +餰 +饁 +饇 +饐 +饎 +饙 +饘 +饛 +饡 +馣 +馲 +馰 +馵 +馻 +馺 +駂 +馽 +駜 +駍 +駏 +駎 +駖 +駮 +駬 +駥 +駤 +駣 +駩 +駺 +駴 +駷 +駹 +駶 +駻 +駽 +駾 +騃 +騉 +騑 +騊 +騇 +騚 +騕 +騥 +騝 +騛 +騢 +騠 +騧 +騞 +騜 +騵 +騲 +騴 +騱 +騬 +騪 +騩 +騹 +騽 +驆 +騺 +驓 +驔 +驈 +驉 +驖 +驞 +驠 +驦 +驨 +骭 +骫 +骹 +骿 +骴 +骾 +髇 +髊 +髆 +髍 +髐 +髟 +髧 +髬 +髳 +髶 +髺 +髾 +鬁 +髼 +鬋 +鬊 +鬎 +鬌 +鬐 +鬕 +鬗 +鬖 +鬙 +鬞 +鬠 +鬤 +鬫 +鬳 +鬵 +鬺 +鬾 +鬿 +魊 +魌 +魖 +魠 +魡 +魧 +魱 +魦 +魶 +魵 +鮅 +鮇 +魼 +魾 +魻 +鮂 +鮚 +鮞 +鮛 +鮦 +鮥 +鮤 +鮆 +鯆 +鮿 +鮵 +鯈 +鯫 +鯠 +鯞 +鯦 +鯬 +鰌 +鰋 +鰅 +鯸 +鰫 +鰝 +鰬 +鱆 +鰿 +鱄 +鱁 +鰴 +鱐 +鱍 +鱋 +鱕 +鱦 +鱢 +鱞 +鱴 +鱳 +鱹 +鳦 +鳪 +鳭 +鳱 +鳵 +鳼 +鳺 +鳿 +鳷 +鴀 +鳹 +鳻 +鴅 +鴃 +鴥 +鴠 +鴔 +鴩 +鴘 +鴢 +鴐 +鴳 +鵁 +鵧 +鴶 +鴮 +鴱 +鴸 +鵅 +鵃 +鴾 +鵀 +鴽 +鵏 +鵊 +鵛 +鵋 +鵖 +鵌 +鵗 +鵔 +鵷 +鶁 +鶊 +鶄 +鶈 +鵱 +鶀 +鵸 +鶋 +鶌 +鵽 +鵫 +鵴 +鵩 +鶅 +鵳 +鵻 +鶂 +鵹 +鶟 +鶙 +鶤 +鶝 +鶐 +鶛 +鶠 +鶔 +鶜 +鶪 +鶗 +鶢 +鶨 +鶞 +鶣 +鶖 +鶷 +鶶 +鷁 +鷇 +鷊 +鷏 +鶾 +鷅 +鷃 +鶵 +鷈 +鶱 +鶭 +鷛 +鷒 +鷞 +鷋 +鷐 +鷜 +鷑 +鷩 +鷘 +鷖 +鷵 +鷕 +鷻 +鷷 +鷣 +鷤 +鷶 +鷡 +鷮 +鷢 +鸂 +鷾 +鸇 +鸃 +鸆 +鸅 +鸀 +鸁 +鸉 +鷿 +鷽 +鸄 +鸋 +鸍 +鸏 +鸒 +鸔 +鸓 +鸗 +鸙 +鹺 +麃 +麆 +麉 +麎 +麌 +麔 +麙 +麛 +麚 +麜 +麠 +麡 +麧 +麮 +麰 +麶 +麷 +黀 +黂 +黈 +黓 +黕 +黖 +黚 +黤 +黫 +黮 +黭 +黰 +黳 +黵 +黺 +鼁 +鼀 +鼆 +鼊 +鼏 +鼖 +鼛 +鼘 +鼜 +鼤 +鼣 +鼥 +鼪 +鼨 +鼭 +鼰 +鼮 +鼵 +鼳 +鼲 +鼸 +鼶 +齀 +齂 +齃 +齌 +齍 +齎 +齖 +齗 +齘 +齛 +齠 +齞 +齝 +齥 +齤 +齫 +齱 +齰 +齮 +齯 +齴 +齵 +齸 +齻 +齺 +齹 +齾 +龒 +龤 +堔 +礂 +蒏 +蒆 +兙 +兛 +兞 +兝 +兡 +兣 +嗧 +瓩 +忼 +擡 +氊 +穇 +擧 +譌 +! +" +# +$ +% +& +' +( +) +* ++ +, +- +. +/ +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 +© +° +² +´ +½ +Á +Ä +Å +Ç +È +É +Í +Ó +Ö +× +Ü +ß +à +á +â +ã +ä +å +æ +ç +è +é +ê +ë +í +ð +ñ +ò +ó +ô +õ +ö +ø +ú +û +ü +ý +ā +ă +ą +ć +Č +č +đ +ē +ė +ę +ğ +ī +ı +Ł +ł +ń +ň +ō +ř +Ş +ş +Š +š +ţ +ū +ż +Ž +ž +Ș +ș +ț +Δ +α +λ +μ +φ +Г +О +а +в +л +о +р +с +т +я +ồ +— +― +’ +“ +” +… +℃ +→ +∇ +− +■ +☆ +、 +。 +々 +〆 +〈 +〉 +「 +」 +『 +』 +〔 +〕 +〜 +! +# +% +& +( +) ++ +, +- +. +/ +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 +R +S +T +U +V +W +X +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/screenshot-monitor/android/ScreenshotModule.kt b/plugins/screenshot-monitor/android/ScreenshotModule.kt new file mode 100644 index 0000000..a5baec9 --- /dev/null +++ b/plugins/screenshot-monitor/android/ScreenshotModule.kt @@ -0,0 +1,90 @@ +package com.beancount.mobile.screenshot + +import android.net.Uri +import android.util.Base64 +import android.provider.MediaStore +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.beancount.mobile.accessibility.ReactContextHolder + +/** + * 截图监控 RN Module(plan.md「3.13 截图自动记账通道」)。 + * + * 由 JS 端调用 start()/stop() 控制 ContentObserver 的注册/注销。 + * 检测到截图时读取 base64 并通过 DeviceEventEmitter 发送到 JS。 + * + * 事件格式(WritableNativeMap)与 BillingAccessibilityService.sendScreenshotEvent 一致: + * { base64: "data:image/jpeg;base64,...", packageName: "...", timestamp: Long, displayName: "..." } + */ +class ScreenshotModule(private val reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + + private var observer: ScreenshotObserver? = null + + override fun getName() = "ScreenshotMonitor" + + override fun invalidate() { + stop() + super.invalidate() + } + + /** + * 启动截图监控。 + * 在主线程注册 ContentObserver,检测到截图时发送 billingScreenshot 事件。 + */ + @ReactMethod + fun start() { + if (observer != null) return + // 确保 ReactContextHolder 有上下文 + ReactContextHolder.context = reactContext + observer = ScreenshotObserver(reactContext) { uri -> + sendScreenshotEvent(uri) + } + observer?.register() + } + + /** 停止截图监控。 */ + @ReactMethod + fun stop() { + observer?.unregister() + observer = null + } + + /** 读取截图 base64 并发送到 JS(WritableNativeMap 格式,与无障碍服务一致)。 */ + private fun sendScreenshotEvent(uri: Uri) { + try { + val resolver = reactContext.contentResolver + // 读取图片为 base64 + val inputStream = resolver.openInputStream(uri) ?: return + val bytes = inputStream.use { it.readBytes() } + inputStream.close() + val base64 = "data:image/png;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP) + + // 同时查询显示名 + val projection = arrayOf(MediaStore.Images.Media.DISPLAY_NAME) + var displayName = "" + resolver.query(uri, projection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + displayName = cursor.getString(0) ?: "" + } + } + + val map = WritableNativeMap() + map.putString("base64", base64) + map.putString("uri", uri.toString()) + map.putString("packageName", "") + map.putString("displayName", displayName) + map.putDouble("timestamp", System.currentTimeMillis().toDouble()) + + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingScreenshot", map) + + } catch (e: Exception) { + android.util.Log.e("ScreenshotModule", "发送截图事件失败: ${e.message}", e) + } + } +} diff --git a/plugins/screenshot-monitor/android/ScreenshotObserver.kt b/plugins/screenshot-monitor/android/ScreenshotObserver.kt new file mode 100644 index 0000000..46460a0 --- /dev/null +++ b/plugins/screenshot-monitor/android/ScreenshotObserver.kt @@ -0,0 +1,116 @@ +package com.beancount.mobile.screenshot + +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.util.Log + +/** + * 截图监听 ContentObserver(plan.md「3.13 截图自动记账通道」)。 + * + * 参考 BeeCount 的 ScreenshotObserver.kt: + * - 监听 MediaStore.Images.Media.EXTERNAL_CONTENT_URI 变化 + * - 关键词匹配(screenshot/截屏/截图/screen_shot) + * - 30 秒时间窗(过滤旧截图) + * - processedPaths 去重(最多 200 条) + * - 过滤小米 .pending- 临时文件 + * - 500ms 防抖 + * + * 由 Config Plugin 注册,触发后通过 DeviceEventEmitter 推送到 JS 层 ScreenshotChannel。 + * + * ⚠️ 与 src/domain/constants.ts 同步:TIME_WINDOW_MS、MAX_PROCESSED、SCREENSHOT_KEYWORDS + * 修改时需两边同时更新。 + */ +class ScreenshotObserver( + private val context: Context, + private val handler: Handler = Handler(Looper.getMainLooper()), + private val onScreenshot: (uri: Uri) -> Unit, +) : ContentObserver(handler) { + + companion object { + private const val TAG = "ScreenshotObserver" + private const val TIME_WINDOW_MS = 30_000L + private const val MAX_PROCESSED = 200 + private val SCREENSHOT_KEYWORDS = listOf("screenshot", "截屏", "截图", "screen_shot", "Screenshot") + } + + private val resolver = context.contentResolver + private val processedPaths = LinkedHashSet() + @Volatile private var lastCheckTime = System.currentTimeMillis() + + /** 注册监听。 */ + fun register() { + resolver.registerContentObserver( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + true, + this, + ) + Log.i(TAG, "截图监听已注册") + } + + /** 注销监听。 */ + fun unregister() { + resolver.unregisterContentObserver(this) + Log.i(TAG, "截图监听已注销") + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + uri ?: return + handler.post { processNewScreenshot(uri) } + } + + private fun processNewScreenshot(uri: Uri) { + try { + // 查询截图信息 + val projection = arrayOf( + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.DATE_ADDED, + MediaStore.Images.Media.DISPLAY_NAME, + ) + resolver.query(uri, projection, null, null, null)?.use { cursor -> + if (!cursor.moveToFirst()) return + val path = cursor.getString(0) ?: "" + val dateAdded = cursor.getLong(1) + val displayName = cursor.getString(2) ?: "" + + // 1. 时间窗过滤(30 秒内) + val now = System.currentTimeMillis() / 1000 + if (now - dateAdded > TIME_WINDOW_MS / 1000) { + Log.d(TAG, "忽略旧截图(超过30秒): $displayName") + return + } + + // 2. 关键词匹配(必须是截图) + if (!SCREENSHOT_KEYWORDS.any { kw -> displayName.contains(kw, true) || path.contains(kw, true) }) { + return + } + + // 3. 过滤小米 .pending- 临时文件 + if (path.endsWith(".pending-") || path.contains(".pending-")) { + Log.d(TAG, "过滤小米 pending 临时文件: $displayName") + return + } + + // 4. 去重 + if (processedPaths.contains(path)) return + processedPaths.add(path) + if (processedPaths.size > MAX_PROCESSED) { + val it = processedPaths.iterator() + if (it.hasNext()) { + it.next() + it.remove() + } + } + + Log.i(TAG, "检测到新截图: $displayName") + onScreenshot(uri) + } + } catch (e: Exception) { + Log.e(TAG, "处理截图异常: ${e.message}", e) + } + } +} diff --git a/plugins/screenshot-monitor/android/ScreenshotPackage.kt b/plugins/screenshot-monitor/android/ScreenshotPackage.kt new file mode 100644 index 0000000..2e373b9 --- /dev/null +++ b/plugins/screenshot-monitor/android/ScreenshotPackage.kt @@ -0,0 +1,20 @@ +package com.beancount.mobile.screenshot + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +/** + * ReactPackage 注册 ScreenshotModule。 + * 由 Config Plugin 的 withMainApplication 注入 add(ScreenshotPackage()) 到 MainApplication。 + */ +class ScreenshotPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(ScreenshotModule(reactContext)) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} diff --git a/plugins/size-optimization/app.plugin.js b/plugins/size-optimization/app.plugin.js new file mode 100644 index 0000000..5bd54cd --- /dev/null +++ b/plugins/size-optimization/app.plugin.js @@ -0,0 +1,48 @@ +const { withDangerousMod } = require('@expo/config-plugins'); +const fs = require('fs'); +const path = require('path'); + +function withSizeOptimization(config) { + // 1. 在 prebuild 时修改 gradle.properties + config = withDangerousMod(config, [ + 'android', + async (modConfig) => { + const projectRoot = modConfig.modRequest.platformProjectRoot; + const propertiesPath = path.join(projectRoot, 'gradle.properties'); + if (fs.existsSync(propertiesPath)) { + let content = fs.readFileSync(propertiesPath, 'utf8'); + if (content.includes('reactNativeArchitectures=')) { + content = content.replace(/reactNativeArchitectures=.*/, 'reactNativeArchitectures=arm64-v8a'); + } else { + content += '\nreactNativeArchitectures=arm64-v8a\n'; + } + fs.writeFileSync(propertiesPath, content, 'utf8'); + } + return modConfig; + } + ]); + + // 2. 在 prebuild 时修改 app/build.gradle 启用 ABI Splits 分包 + config = withDangerousMod(config, [ + 'android', + async (modConfig) => { + const projectRoot = modConfig.modRequest.platformProjectRoot; + const buildGradlePath = path.join(projectRoot, 'app/build.gradle'); + if (fs.existsSync(buildGradlePath)) { + let content = fs.readFileSync(buildGradlePath, 'utf8'); + 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'); + } + } + return modConfig; + } + ]); + + return config; +} + +module.exports = withSizeOptimization; diff --git a/plugins/size-optimization/package.json b/plugins/size-optimization/package.json new file mode 100644 index 0000000..151b415 --- /dev/null +++ b/plugins/size-optimization/package.json @@ -0,0 +1,4 @@ +{ + "name": "size-optimization", + "main": "app.plugin.js" +} diff --git a/plugins/sms-receiver/android/BillingSmsReceiver.kt b/plugins/sms-receiver/android/BillingSmsReceiver.kt new file mode 100644 index 0000000..4bcb256 --- /dev/null +++ b/plugins/sms-receiver/android/BillingSmsReceiver.kt @@ -0,0 +1,73 @@ +package com.beancount.mobile.sms + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.provider.Telephony +import android.telephony.SmsMessage +import android.util.Log +import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.beancount.mobile.accessibility.ReactContextHolder + +/** + * 短信监听 Receiver(plan.md「4.2 短信监听服务」+「决策 4 Config Plugin」)。 + * + * 参考 AutoAccounting 的 SmsReceiver: + * - 监听 SMS_RECEIVED_ACTION + * - 从 PDU 解析发送方 + 正文 + * - 关键词预过滤(银行短信含「交易/消费/余额」等,JS 层进一步过滤) + * - 通过 DeviceEventEmitter 推送到 JS 层 SmsChannel + * + * 需在 manifest 注册 RECEIVE_SMS 权限(由 Config Plugin 注入)。 + */ +class BillingSmsReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "BillingSms" + + /** 银行短信关键词(预过滤,减少 JS 层负担)。 */ + private val BANK_KEYWORDS = listOf("交易", "消费", "收入", "支出", "余额", "转账", "入账", "扣款", "退款") + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return + + runCatching { + val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) + for (message in messages) { + val sender = message.displayOriginatingAddress ?: "" + val body = message.displayMessageBody ?: "" + if (sender.isBlank() || body.isBlank()) continue + + // 关键词预过滤(仅处理疑似银行短信) + if (!BANK_KEYWORDS.any { body.contains(it) }) continue + + Log.d(TAG, "收到银行短信: sender=$sender") + sendSmsEvent(sender, body) + } + }.onFailure { + Log.e(TAG, "短信处理异常: ${it.message}", it) + } + } + + /** 把短信事件推送到 JS 层 SmsChannel.handleSms。 */ + private fun sendSmsEvent(sender: String, body: String) { + val reactContext = ReactContextHolder.context ?: run { + Log.w(TAG, "RN 上下文未就绪,丢弃短信") + return + } + try { + val map = WritableNativeMap().apply { + putString("sender", sender) + putString("body", body) + putDouble("timestamp", System.currentTimeMillis().toDouble()) + } + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit("billingSms", map) + } catch (e: Exception) { + Log.e(TAG, "推送短信事件失败: ${e.message}") + } + } +} diff --git a/src/ai/chatAssistant.ts b/src/ai/chatAssistant.ts index 0344e8c..87224b3 100644 --- a/src/ai/chatAssistant.ts +++ b/src/ai/chatAssistant.ts @@ -9,6 +9,7 @@ * 复用 domain/ai.ts 的 processNaturalLanguage + buildNaturalLanguagePrompt。 */ +import { CHAT_TRANSACTION_KEYWORDS } from '../domain/constants'; import { processNaturalLanguage, removeThink, type AiProvider, type AiBillResult } from '../domain/ai'; export interface ChatMessage { @@ -29,14 +30,12 @@ export interface ChatResponse { error?: string; } -/** 记账意图关键词(plan.md「8.1」)。 */ -const TRANSACTION_KEYWORDS = ['买', '花', '消费', '支付', '记账', '付', '收入', '赚', '工资', '收']; const AMOUNT_PATTERN = /\d+(?:\.\d+)?/; /** 判断是否为记账意图(金额 + 关键词)。 */ export function isTransactionIntent(input: string): boolean { const hasAmount = AMOUNT_PATTERN.test(input); - const hasKeyword = TRANSACTION_KEYWORDS.some(kw => input.includes(kw)); + const hasKeyword = CHAT_TRANSACTION_KEYWORDS.some(kw => input.includes(kw)); return hasAmount && hasKeyword; } diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx index 7e21f19..e659643 100644 --- a/src/app/(tabs)/index.tsx +++ b/src/app/(tabs)/index.tsx @@ -34,8 +34,6 @@ export default function HomeScreen() { if (monthlyData.length === 0) return null; return monthlyData[monthlyData.length - 1]; }, [monthlyData]); - const recentTx = useMemo(() => transactions.slice(-5).reverse(), [transactions]); - // 1. 账户树计算 const accountNodes = useMemo(() => { if (!ledger || !ledger.accounts) return []; @@ -51,12 +49,14 @@ export default function HomeScreen() { // 周期记账自动触发:启动时自动确认标记了 autoConfirm 的到期项 const autoConfirmed = useRef(false); useEffect(() => { + let isMounted = true; if (autoConfirmed.current) return; const autoDue = dueRecurring.filter(r => r.autoConfirm); if (autoDue.length === 0) return; autoConfirmed.current = true; (async () => { for (const rec of autoDue) { + if (!isMounted) break; try { const draft = instantiateRecurring(rec); await addTransaction(draft); @@ -67,6 +67,7 @@ export default function HomeScreen() { } } })(); + return () => { isMounted = false; }; }, [dueRecurring, addTransaction, updateRecurringTransaction]); const handleConfirmRecurring = async (rec: any) => { @@ -93,91 +94,104 @@ export default function HomeScreen() { return ( - {t('app.name')} + + {t('app.name')} + - {/* 净资产卡片 */} - - - - {t('home.assets')} - + {/* 净资产 Hero 卡片 */} + + + {t('home.netWorthTitle')} + + + {fmtAmount(netWorth.netWorth)} + + + + + {t('home.assets')} + + {fmtAmount(netWorth.assets)} - - {t('home.liabilities')} - - {fmtAmount(netWorth.liabilities)} + + + {t('home.liabilities')} - - - {t('home.netWorth')} - - {fmtAmount(netWorth.netWorth)} + + {fmtAmount(netWorth.liabilities)} - {/* 周期性记账提醒 */} - {dueRecurring.length > 0 && ( - - - {dueRecurring.map(rec => ( - - - - {rec.name} + {/* Bento 双格排列 */} + {(dueRecurring.length > 0 || currentMonth) && ( + + {dueRecurring.length > 0 ? ( + + + {dueRecurring.slice(0, 2).map(rec => ( + + + {rec.name} + + handleConfirmRecurring(rec)} + style={({ pressed }) => [ + styles.confirmBtnMini, + { + backgroundColor: theme.colors.bgTertiary, + borderColor: theme.colors.border, + borderRadius: theme.radii.sm, + opacity: pressed ? 0.7 : 1, + }, + ]} + > + + {rec.draft.postings[0]?.amount} CNY + + + + ))} + + + ) : null} + + {currentMonth ? ( + + + + + {t('home.income')} - - {t('home.dueAmount')}: {rec.draft.postings[0]?.amount} CNY + + +{fmtAmount(currentMonth.income.toString())} + + + + + {t('home.expense')} + + + -{fmtAmount(currentMonth.expense.toString())} - handleConfirmRecurring(rec)} - style={({ pressed }) => [ - styles.confirmBtn, - { - backgroundColor: theme.colors.accent, - borderRadius: theme.radii.sm, - opacity: pressed ? 0.7 : 1, - }, - ]} - > - {t('home.confirmRecurring')} - - ))} - - - )} - - {/* 本月收支 */} - {currentMonth && ( - - - - {t('home.income')} - - {currentMonth.income.toFixed(2)} - - - - {t('home.expense')} - - {currentMonth.expense.toFixed(2)} - - - - {t('home.balance')} - - {(currentMonth.income - currentMonth.expense).toFixed(2)} - - - - + + ) : null} + )} {/* 月度趋势 */} @@ -193,22 +207,6 @@ export default function HomeScreen() { )} - - {/* 最近交易 */} - - {recentTx.length === 0 && ( - - {t('home.recentEmpty')} - - )} - {recentTx.map(tx => ( - router.push(`/transaction/${tx.id}`)} - /> - ))} - router.push('/transaction/new') }, @@ -223,9 +221,11 @@ const styles = StyleSheet.create({ page: { flex: 1 }, header: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 12 }, content: { padding: 16, paddingBottom: 96 }, - balanceRow: { flexDirection: 'row', gap: 8 }, - balanceItem: { flex: 1, alignItems: 'center', gap: 4 }, + heroFooter: { flexDirection: 'row', justifyContent: 'space-between', borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: 'rgba(255,255,255,0.15)', paddingTop: 10, marginTop: 4 }, + bentoRow: { flexDirection: 'row', gap: 12 }, + bentoCol: { flex: 1 }, recurringList: { gap: 8 }, - recurringRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 8, borderBottomWidth: 1 }, - confirmBtn: { paddingVertical: 6, paddingHorizontal: 12 }, + recurringItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingBottom: 6, borderBottomWidth: StyleSheet.hairlineWidth }, + confirmBtnMini: { paddingVertical: 4, paddingHorizontal: 8, borderWidth: StyleSheet.hairlineWidth }, + bentoStats: { gap: 8 }, }); diff --git a/src/app/(tabs)/report.tsx b/src/app/(tabs)/report.tsx index 501ed42..2bd8c6d 100644 --- a/src/app/(tabs)/report.tsx +++ b/src/app/(tabs)/report.tsx @@ -10,18 +10,20 @@ import React, { useMemo, useRef, useState } from 'react'; import { Alert, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { captureRef } from 'react-native-view-shot'; import * as Sharing from 'expo-sharing'; import { useLedgerStore } from '../../store/ledgerStore'; import { useSettingsStore } from '../../store/settingsStore'; -import { useTheme } from '../../theme'; +import { useTheme, createCommonStyles } from '../../theme'; import { useT } from '../../i18n'; import { Card } from '../../components/Card'; import { Button } from '../../components/Button'; import { MonthlyReport } from '../../components/charts/MonthlyReport'; import { CategoryPie } from '../../components/charts/CategoryPie'; -import { CalendarHeatmap } from '../../components/charts/CalendarHeatmap'; +import { CalendarView } from '../../components/CalendarView'; +import { TransactionCard } from '../../components/TransactionCard'; import { NetWorthChart } from '../../components/charts/NetWorthChart'; import { AnnualReport } from '../../components/charts/AnnualReport'; import { calculateMonthlyStats, generatePlainTextSummary, generateMonthlySummary } from '../../ai/monthlySummary'; @@ -29,18 +31,29 @@ import { BaseOpenAIProvider } from '../../domain/ai'; export default function ReportScreen() { const { theme } = useTheme(); + const commonStyles = createCommonStyles(theme); const t = useT(); + const router = useRouter(); const ledger = useLedgerStore(s => s.ledger); const aiEnabled = useSettingsStore(s => s.aiEnabled); const aiApiKey = useSettingsStore(s => s.aiApiKey); const aiBaseUrl = useSettingsStore(s => s.aiBaseUrl); const aiModel = useSettingsStore(s => s.aiModel); + // Tab 状态 + const [activeTab, setActiveTab] = useState<'weekly' | 'monthly' | 'annual'>('monthly'); + // 月份切换状态 const now = new Date(); const [viewYear, setViewYear] = useState(now.getFullYear()); const [viewMonth, setViewMonth] = useState(now.getMonth() + 1); + // 周切换状态 + const [viewWeekDate, setViewWeekDate] = useState(new Date()); + + // 日历选中日期用于明细展开 + const [selectedReportDate, setSelectedReportDate] = useState(null); + // AI 总结弹窗 const [summaryModal, setSummaryModal] = useState(false); const [summaryText, setSummaryText] = useState(''); @@ -50,6 +63,41 @@ export default function ReportScreen() { const transactions = useMemo(() => ledger?.transactions ?? [], [ledger]); + // 周报表的时间范围:计算周一和周日 + const weeklyRange = useMemo(() => { + const d = new Date(viewWeekDate); + const day = d.getDay(); // 0 is Sunday, 1-6 is Mon-Sat + const diffToMonday = day === 0 ? -6 : 1 - day; + + const monday = new Date(d); + monday.setDate(d.getDate() + diffToMonday); + monday.setHours(0,0,0,0); + + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23,59,59,999); + + // 计算 ISO 周数 + const date = new Date(monday.getTime()); + date.setHours(0, 0, 0, 0); + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); + const week1 = new Date(date.getFullYear(), 0, 4); + const weekNum = 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); + + return { + startStr: monday.toISOString().slice(0, 10), + endStr: sunday.toISOString().slice(0, 10), + label: `${monday.getFullYear()}年第${weekNum}周 (${monday.getMonth()+1}/${monday.getDate()} ~ ${sunday.getMonth()+1}/${sunday.getDate()})` + }; + }, [viewWeekDate]); + + const changeWeek = (days: number) => { + setSelectedReportDate(null); + const next = new Date(viewWeekDate); + next.setDate(next.getDate() + days); + setViewWeekDate(next); + }; + // 净资产趋势的日期序列(基于选中月份,往前 6 个月) const netWorthDates = useMemo(() => { const dates: string[] = []; @@ -61,14 +109,72 @@ export default function ReportScreen() { }, [viewYear, viewMonth]); const prevMonth = () => { + setSelectedReportDate(null); if (viewMonth === 1) { setViewMonth(12); setViewYear(y => y - 1); } else setViewMonth(m => m - 1); }; const nextMonth = () => { + setSelectedReportDate(null); if (viewMonth === 12) { setViewMonth(1); setViewYear(y => y + 1); } else setViewMonth(m => m + 1); }; + // 过滤后的交易 + const weeklyTxs = useMemo(() => { + return transactions.filter(t => t.date >= weeklyRange.startStr && t.date <= weeklyRange.endStr); + }, [transactions, weeklyRange]); + + const monthlyTxs = useMemo(() => { + const prefix = `${viewYear}-${String(viewMonth).padStart(2, '0')}`; + return transactions.filter(t => t.date.startsWith(prefix)); + }, [transactions, viewYear, viewMonth]); + + const annualTxs = useMemo(() => { + return transactions.filter(t => t.date.startsWith(String(viewYear))); + }, [transactions, viewYear]); + + // 周收支统计 + const weeklyStats = useMemo(() => { + let income = 0; + let expense = 0; + for (const t of weeklyTxs) { + for (const p of t.postings) { + if (!p.amount) continue; + const amt = parseFloat(p.amount); + if (p.account.startsWith('Income')) income += -amt; + if (p.account.startsWith('Expenses')) expense += amt; + } + } + return { income, expense, net: income - expense, count: weeklyTxs.length }; + }, [weeklyTxs]); + + // 月度收支统计 + const monthlyStats = useMemo(() => { + let income = 0; + let expense = 0; + for (const t of monthlyTxs) { + for (const p of t.postings) { + if (!p.amount) continue; + const amt = parseFloat(p.amount); + if (p.account.startsWith('Income')) income += -amt; + if (p.account.startsWith('Expenses')) expense += amt; + } + } + return { income, expense, net: income - expense, count: monthlyTxs.length }; + }, [monthlyTxs]); + + // 选中日期的交易明细 + const selectedDayTxs = useMemo(() => { + if (!selectedReportDate) return []; + return monthlyTxs.filter(t => t.date.slice(0, 10) === selectedReportDate); + }, [monthlyTxs, selectedReportDate]); + + // 千分位格式化金额 + const fmtAmount = (num: number) => { + const formatted = num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + return num >= 0 ? `¥${formatted}` : `-¥${Math.abs(num).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }; + // 导出报表为图片 const handleExportImage = async () => { try { @@ -84,10 +190,9 @@ export default function ReportScreen() { } }; - // AI / 纯文本月度总结(AI 已配置时用真实 AI,否则降级为纯文本) + // AI / 纯文本月度总结 const handleMonthlySummary = async () => { const stats = calculateMonthlyStats(transactions, viewYear, viewMonth); - // 若 AI 已配置,调真实 AI 生成总结;否则降级纯文本 if (aiEnabled && aiApiKey && aiBaseUrl) { try { const provider = new (class extends BaseOpenAIProvider {})({ @@ -124,24 +229,91 @@ export default function ReportScreen() { - {/* 月份切换器 */} - - [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> - - - - {monthLabel} - - [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> - - + {/* Tab 选择切换栏 */} + + {([ + { key: 'weekly', label: t('report.tabWeekly') }, + { key: 'monthly', label: t('report.tabMonthly') }, + { key: 'annual', label: t('report.tabAnnual') } + ] as const).map(tab => ( + { + setActiveTab(tab.key); + setSelectedReportDate(null); + }} + style={[ + styles.tabBtn, + activeTab === tab.key && { borderBottomColor: theme.colors.accent } + ]} + > + + {tab.label} + + + ))} + {/* 日期选择切换器 */} + {activeTab === 'weekly' && ( + + changeWeek(-7)} style={({ pressed }) => [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> + + + + {weeklyRange.label} + + changeWeek(7)} style={({ pressed }) => [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> + + + + )} + + {activeTab === 'monthly' && ( + + [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> + + + + {monthLabel} + + [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> + + + + )} + + {activeTab === 'annual' && ( + + setViewYear(y => y - 1)} style={({ pressed }) => [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> + + + + {viewYear}年 + + setViewYear(y => y + 1)} style={({ pressed }) => [styles.monthBtn, { borderColor: theme.colors.border, opacity: pressed ? 0.6 : 1 }]}> + + + + )} + - {transactions.length === 0 ? ( + {/* 数据空值判断 */} + {((activeTab === 'weekly' && weeklyTxs.length === 0) || + (activeTab === 'monthly' && monthlyTxs.length === 0) || + (activeTab === 'annual' && annualTxs.length === 0)) ? ( {t('report.empty')} @@ -149,33 +321,98 @@ export default function ReportScreen() { ) : ( <> - - - + {/* 周报表看板 */} + {activeTab === 'weekly' && ( + <> + + + + {t('report.weeklyIncome')} + {fmtAmount(weeklyStats.income)} + + + {t('report.weeklyExpense')} + -{fmtAmount(weeklyStats.expense)} + + + {t('report.weeklyNet')} + {fmtAmount(weeklyStats.net)} + + + + {t('report.weeklyStats', { count: weeklyStats.count })} + + - - - + + + )} - - - + {/* 月度报表看板 */} + {activeTab === 'monthly' && ( + <> + + + + {t('report.monthlyIncome')} + {fmtAmount(monthlyStats.income)} + + + {t('report.monthlyExpense')} + -{fmtAmount(monthlyStats.expense)} + + + {t('report.monthlyNet')} + {fmtAmount(monthlyStats.net)} + + + + {t('report.monthlyStats', { count: monthlyStats.count })} + + - - - + - + + + {selectedReportDate && selectedReportDate.startsWith(monthLabel) && ( + + {selectedDayTxs.map(t => ( + router.push(`/transaction/${t.id}`)} + /> + ))} + {selectedDayTxs.length === 0 && ( + + {t('calendar.noDayTx')} + + )} + + )} + + + + )} + + {/* 年度报表看板 */} + {activeTab === 'annual' && ( - + )} )} - {/* AI 月度总结弹窗 */} + {/* AI 总结弹窗 */} setSummaryModal(false)}> - - + + {monthLabel} {t('report.aiSummary')} @@ -200,8 +437,10 @@ const styles = StyleSheet.create({ headerActions: { flexDirection: 'row', gap: 16 }, iconBtn: { padding: 4 }, monthSwitcher: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 20, paddingBottom: 12 }, - monthBtn: { width: 32, height: 32, borderRadius: 16, borderWidth: 1, alignItems: 'center', justifyContent: 'center' }, + monthBtn: { width: 32, height: 32, borderRadius: 16, borderWidth: StyleSheet.hairlineWidth, alignItems: 'center', justifyContent: 'center' }, content: { padding: 16, paddingBottom: 96 }, - modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24, backgroundColor: 'rgba(0,0,0,0.5)' }, - modalCard: { width: '100%', borderRadius: 12, padding: 20 }, + tabContainer: { flexDirection: 'row', justifyContent: 'space-around', borderBottomWidth: StyleSheet.hairlineWidth, paddingHorizontal: 16, marginBottom: 8 }, + tabBtn: { flex: 1, alignItems: 'center', borderBottomWidth: 2, borderBottomColor: 'transparent' }, + statsRow: { flexDirection: 'row', justifyContent: 'space-around', marginVertical: 8, width: '100%' }, + statItem: { alignItems: 'center', gap: 4 }, }); diff --git a/src/app/(tabs)/settings.tsx b/src/app/(tabs)/settings.tsx index fb6758a..20e8a7f 100644 --- a/src/app/(tabs)/settings.tsx +++ b/src/app/(tabs)/settings.tsx @@ -26,7 +26,7 @@ export default function SettingsScreen() { ]} > - + {label} @@ -43,13 +43,11 @@ export default function SettingsScreen() { {/* 数据与基础配置跳转 */} - {renderNavLink('download-outline', t('tab.import'), '/import')} {renderNavLink('git-branch-outline', t('tab.rules'), '/rules')} {renderNavLink('wallet-outline', t('account.title'), '/account')} {renderNavLink('list', t('settings.categories'), '/category')} {renderNavLink('pricetags', t('settings.tags'), '/tag')} {renderNavLink('pie-chart', t('settings.budgets'), '/budget')} - {renderNavLink('calendar', t('settings.calendar'), '/calendar')} {renderNavLink('repeat-outline', t('settings.recurringTitle'), '/recurring')} {renderNavLink('card-outline', t('settings.creditCards'), '/credit-card' as Href)} {renderNavLink('text-outline', t('remark.title'), '/remark-template' as Href)} diff --git a/src/app/(tabs)/transactions.tsx b/src/app/(tabs)/transactions.tsx index bd8cc38..ad2551d 100644 --- a/src/app/(tabs)/transactions.tsx +++ b/src/app/(tabs)/transactions.tsx @@ -13,17 +13,19 @@ import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from 'react-na import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { useLedgerStore } from '../../store/ledgerStore'; -import { useTheme } from '../../theme'; +import { useTheme, createCommonStyles } from '../../theme'; import { useT } from '../../i18n'; import { Card } from '../../components/Card'; import { SearchBar } from '../../components/SearchBar'; import { TransactionCard } from '../../components/TransactionCard'; +import { Ionicons } from '@expo/vector-icons'; import { useSearch, type SearchFilters } from '../../hooks/useSearch'; type DirectionFilter = 'all' | 'expense' | 'income' | 'transfer'; export default function TransactionsScreen() { const { theme } = useTheme(); + const commonStyles = createCommonStyles(theme); const t = useT(); const router = useRouter(); const ledger = useLedgerStore(s => s.ledger); @@ -36,7 +38,17 @@ export default function TransactionsScreen() { const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); - const allTransactions = useMemo(() => ledger?.transactions ?? [], [ledger]); + const allTransactions = useMemo(() => { + if (!ledger?.transactions) return []; + return ledger.transactions + .map((tx, idx) => ({ tx, idx })) + .sort((a, b) => { + const dateCompare = b.tx.date.localeCompare(a.tx.date); + if (dateCompare !== 0) return dateCompare; + return b.idx - a.idx; + }) + .map(item => item.tx); + }, [ledger]); const allAccounts = useMemo(() => { const accts = new Set(); for (const tx of allTransactions) { @@ -81,18 +93,14 @@ export default function TransactionsScreen() { key={tab.key} onPress={() => setDirection(tab.key)} style={[ - styles.filterChip, - { - backgroundColor: direction === tab.key ? theme.colors.accent : theme.colors.bgTertiary, - borderRadius: theme.radii.sm, - }, + commonStyles.chip, + direction === tab.key && commonStyles.chipActive, ]} > - + {tab.label} @@ -103,13 +111,17 @@ export default function TransactionsScreen() { setShowAdvancedFilters(!showAdvancedFilters)} - style={({ pressed }) => [styles.filterChip, { - backgroundColor: showAdvancedFilters ? theme.colors.accent : theme.colors.bgTertiary, - borderRadius: theme.radii.sm, - opacity: pressed ? 0.6 : 1, - }]} + style={({ pressed }) => [ + commonStyles.chip, + showAdvancedFilters && commonStyles.chipActive, + { opacity: pressed ? 0.8 : 1 }, + ]} > - + {t('transactions.advancedFilter')} @@ -124,7 +136,7 @@ export default function TransactionsScreen() { onChangeText={setAccountFilter} placeholder={t('transactions.accountFilterPlaceholder')} placeholderTextColor={theme.colors.fgSecondary} - style={[styles.advInput, { backgroundColor: theme.colors.bgTertiary, color: theme.colors.fgPrimary, borderColor: theme.colors.border }]} + style={commonStyles.input} /> {/* 日期范围 */} @@ -133,14 +145,14 @@ export default function TransactionsScreen() { onChangeText={setDateFrom} placeholder={t('transactions.dateFrom')} placeholderTextColor={theme.colors.fgSecondary} - style={[styles.advInput, { backgroundColor: theme.colors.bgTertiary, color: theme.colors.fgPrimary, borderColor: theme.colors.border, flex: 1 }]} + style={[commonStyles.input, { flex: 1 }]} /> @@ -178,10 +190,15 @@ export default function TransactionsScreen() { {/* 诊断(折叠) */} setShowDiagnostics(!showDiagnostics)} - style={styles.diagToggle} + style={[styles.diagToggle, { flexDirection: 'row', alignItems: 'center', gap: 4 }]} > - - {showDiagnostics ? '▼' : '▶'} {t('diagnostics.title')} + + + {t('diagnostics.title')} {showDiagnostics && ( @@ -215,8 +232,6 @@ const styles = StyleSheet.create({ page: { flex: 1 }, header: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 8 }, filterRow: { flexDirection: 'row', gap: 6, paddingHorizontal: 16, paddingBottom: 8 }, - filterChip: { paddingVertical: 5, paddingHorizontal: 12 }, - advInput: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 10, paddingVertical: 6, fontSize: 13 }, content: { padding: 16, paddingTop: 8 }, diagToggle: { paddingVertical: 12, alignItems: 'center' }, }); diff --git a/src/app/_error.tsx b/src/app/_error.tsx index 255f105..44644fb 100644 --- a/src/app/_error.tsx +++ b/src/app/_error.tsx @@ -5,6 +5,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../theme'; import { useT } from '../i18n'; import { buildErrorRecoveryData, crashReporter } from '../services/crash'; +import * as FileSystem from 'expo-file-system/legacy'; interface ErrorBoundaryProps { error: Error; @@ -16,8 +17,17 @@ export function ErrorScreen({ error, resetError }: ErrorBoundaryProps) { const t = useT(); const data = buildErrorRecoveryData(error); - const clearCacheAndRestart = () => { - // 模拟重置错误缓存以恢复启动 + const clearCacheAndRestart = async () => { + try { + const settingsFile = FileSystem.documentDirectory + 'settings.json'; + const metadataFile = FileSystem.documentDirectory + 'metadata.json'; + const settingsInfo = await FileSystem.getInfoAsync(settingsFile); + if (settingsInfo.exists) await FileSystem.deleteAsync(settingsFile); + const metadataInfo = await FileSystem.getInfoAsync(metadataFile); + if (metadataInfo.exists) await FileSystem.deleteAsync(metadataFile); + } catch { + // 忽略删除错误 + } resetError(); }; diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 8b0adb2..76c8306 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { StatusBar } from 'expo-status-bar'; import { Stack } from 'expo-router'; import { AppState, DeviceEventEmitter, Platform, Text, View } from 'react-native'; @@ -17,6 +17,27 @@ import { setupDeepLinking } from '../services/deepLink'; import { initPersistence } from '../store/storePersistence'; import * as FileSystem from 'expo-file-system/legacy'; import { useRouter } from 'expo-router'; +import { parseNotification } from '../services/notification'; +import { parseSms } from '../services/sms'; +import { processScreenshotEvent, handleIncomingBillEvent, parseAndProcessAccessibilityTexts } from '../services/automationPipeline'; +import { getAccessibilityBridge } from '../services/accessibilityBridge'; +import { logger } from '../utils/logger'; +import { + useFonts, + Quicksand_400Regular, + Quicksand_500Medium, + Quicksand_600SemiBold, + Quicksand_700Bold, +} from '@expo-google-fonts/quicksand'; +import { + Caveat_400Regular, + Caveat_500Medium, + Caveat_600SemiBold, + Caveat_700Bold, +} from '@expo-google-fonts/caveat'; + +let lastSignature = ''; +let lastProcessedTime = 0; /** 示例账本(后续由文件选择器导入,当前 demo 用内置)。 */ const SAMPLE_LEDGER = { @@ -26,14 +47,22 @@ include "mobile.bean" `, }; -/** 渠道 → 账户映射:导入账单时由规则自动匹配,此处仅提供首次缺省映射。 */ -const DEMO_ACCOUNT_MAP: Record = {}; - /** 内层布局:引导/锁屏/路由三态切换。 */ function AppShell() { const { theme, isDark } = useTheme(); const t = useT(); const router = useRouter(); + + const [fontsLoaded, fontError] = useFonts({ + Quicksand_400Regular, + Quicksand_500Medium, + Quicksand_600SemiBold, + Quicksand_700Bold, + Caveat_400Regular, + Caveat_500Medium, + Caveat_600SemiBold, + Caveat_700Bold, + }); const loadLedger = useLedgerStore(s => s.loadLedger); const setContext = useImportStore(s => s.setContext); const appLockEnabled = useSettingsStore(s => s.appLockEnabled); @@ -42,22 +71,79 @@ function AppShell() { const [phase, setPhase] = useState<'loading' | 'onboarding' | 'locked' | 'ready'>('loading'); const [privacyOverlay, setPrivacyOverlay] = useState(false); + const lastMainFileRef = useRef<{ modificationTime?: number; size?: number } | null>(null); + + const checkAndReloadLedger = useCallback(async () => { + const mainPath = FileSystem.documentDirectory + 'main.bean'; + try { + const info = await FileSystem.getInfoAsync(mainPath); + if (!info.exists) return; + + const currentMtime = info.modificationTime; + const currentSize = info.size; + + // 如果是第一次加载,或者文件时间/大小发生了改变,则重新读取并加载 + if ( + !lastMainFileRef.current || + lastMainFileRef.current.modificationTime !== currentMtime || + lastMainFileRef.current.size !== currentSize + ) { + logger.info('layout', `检测到 main.bean 发生外部文件修改,正在重新加载... (mtime: ${currentMtime}, size: ${currentSize})`); + const content = await FileSystem.readAsStringAsync(mainPath); + const rules = useMetadataStore.getState().rules; + const categories = useMetadataStore.getState().categories; + + await loadLedger([{ path: 'main.bean', content }], new FileSystemBackend()); + setContext({ rules, categories }); + + lastMainFileRef.current = { modificationTime: currentMtime, size: currentSize }; + logger.info('layout', 'main.bean 重新解析并加载成功'); + } + } catch (e) { + logger.error('layout', 'checkAndReloadLedger 失败', e); + } + }, [loadLedger, setContext]); + useEffect(() => { // 1. 初始化持久化并还原数据 - initPersistence().then(() => { - // 2. 检查并读取本地真实主账本 + initPersistence().then(async () => { const mainPath = FileSystem.documentDirectory + 'main.bean'; + const mobilePath = FileSystem.documentDirectory + 'mobile.bean'; + + try { + // 一次性合并迁移:将旧的 mobile.bean 内容追加到 main.bean 并物理删除它 + const mobileInfo = await FileSystem.getInfoAsync(mobilePath); + if (mobileInfo.exists) { + const mobileContent = await FileSystem.readAsStringAsync(mobilePath); + if (mobileContent.trim()) { + const mainInfo = await FileSystem.getInfoAsync(mainPath); + let mainContent = ''; + if (mainInfo.exists) { + mainContent = await FileSystem.readAsStringAsync(mainPath); + } + const mergedContent = `${mainContent.trimEnd()}\n\n${mobileContent.trim()}\n`.trim(); + await FileSystem.writeAsStringAsync(mainPath, mergedContent); + logger.info('layout', '已成功将旧 mobile.bean 交易追加合并至 main.bean'); + } + await FileSystem.deleteAsync(mobilePath, { idempotent: true }); + } + } catch (e) { + logger.error('layout', '合并旧 mobile.bean 数据失败', e); + } + + // 2. 检查并读取本地真实主账本 FileSystem.getInfoAsync(mainPath).then(async (info) => { let content = SAMPLE_LEDGER.content; if (info.exists) { content = await FileSystem.readAsStringAsync(mainPath); + lastMainFileRef.current = { modificationTime: info.modificationTime, size: info.size }; } const rules = useMetadataStore.getState().rules; const categories = useMetadataStore.getState().categories; loadLedger([{ path: 'main.bean', content }], new FileSystemBackend()).then(() => { - setContext({ rules, categories, accountMap: DEMO_ACCOUNT_MAP }); + setContext({ rules, categories }); const completed = useSettingsStore.getState().onboardingCompleted; const locked = useSettingsStore.getState().appLockEnabled; @@ -109,31 +195,152 @@ function AppShell() { const subscriptions = [ DeviceEventEmitter.addListener('billingNotification', (event) => { - // 原生事件格式:{ packageName, title, text, timestamp } - // parseNotification 复用 OCR 规则匹配提取账单信息 - const { parseNotification } = require('../services/notification'); - const bill = parseNotification(event); - if (bill) { - useAutomationStore.getState().addDetected('notification', bill); + try { + logger.debug('layout', `收到原生通知, 包名: ${event.packageName}, 标题: ${event.title}, 内容: ${event.text}`); + 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}`); + handleIncomingBillEvent('notification', bill, `[${event.title}] ${event.text}`); + } else { + logger.debug('layout', `通知未匹配为账单: [${event.title}] ${event.text}`); + } + } catch (e) { + logger.error('layout', '通知解析失败', e); } }), DeviceEventEmitter.addListener('billingSms', (event) => { - // 原生事件格式:{ sender, body, timestamp } - // parseSms 提取金额/卡号/方向/商户 - const { parseSms } = require('../services/sms'); - const bill = parseSms(event); - if (bill) { - useAutomationStore.getState().addDetected('sms', bill); + try { + logger.debug('layout', `收到原生短信, 发送者: ${event.address || event.sender || '未知'}, 内容: ${event.body}`); + const smsEvent = { + sender: event.sender || event.address || '', + body: event.body, + timestamp: event.timestamp || Date.now(), + }; + const bill = parseSms(smsEvent); + if (bill) { + logger.debug('layout', `短信解析成功 [时间: ${bill.occurredAt}, 方向: ${bill.direction === 'income' ? '收入' : '支出'}, 金额: ${bill.amount} ${bill.currency}, 商户: ${bill.counterparty}, 备注: ${bill.memo}] | 原始短信: ${event.body}`); + handleIncomingBillEvent('sms', bill, event.body); + } else { + logger.debug('layout', `短信未匹配为账单: ${event.body}`); + } + } catch (e) { + logger.error('layout', '短信解析失败', e); } }), DeviceEventEmitter.addListener('billingScreenshot', (event) => { - // 截图事件格式:{ base64, packageName, timestamp, ... } - // 经 OcrProcessor 三层降级(规则→OCR→AI Vision)识别为账单事件 - const { processScreenshotEvent } = require('../services/automationPipeline'); + logger.debug('layout', `收到原生截图/无障碍截图, 包名: ${event.packageName}`); processScreenshotEvent(event).catch((e: unknown) => { - console.warn('[layout] 截图 OCR 处理失败:', e); + logger.error('layout', '截图 OCR 处理失败', e); }); }), + DeviceEventEmitter.addListener('billingOpenApp', (res) => { + try { + logger.info('layout', `收到原生悬浮窗跳转 App 请求: ${JSON.stringify(res)}`); + if (res.draftId) { + try { + const { pendingDrafts } = require('../services/automationPipeline'); + pendingDrafts.delete(res.draftId); + } catch (err) { + // 忽略 + } + } + const amt = res.amount || '0'; + const currency = 'CNY'; + let postings: any[] = []; + if (res.direction === 'expense') { + postings = [ + { account: res.account, amount: `-${amt}`, currency }, + { account: res.category, amount: amt, currency } + ]; + } else if (res.direction === 'income') { + postings = [ + { account: res.account, amount: amt, currency }, + { account: res.category, amount: `-${amt}`, currency } + ]; + } else { + postings = [ + { account: res.account, amount: `-${amt}`, currency }, + { account: res.category, amount: amt, 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) } + }); + } 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)}`); + }), + 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)}`); + + try { + const bridge = getAccessibilityBridge(); + if (!isManual) { + if (bridge) { + const signatures = await bridge.getPageSignatures(); + const isWhitelisted = signatures.some(s => s.signature === sigKey); + if (!isWhitelisted) { + logger.info('layout', `[无障碍监听] 页面 ${sigKey} 未在自动记账白名单中,拒绝弹窗`); + return; + } + } + + // 防抖:3秒内避免对同一页面重复触发记账/截图 + if (sigKey === lastSignature && Date.now() - lastProcessedTime < 3000) { + return; + } + lastSignature = sigKey; + lastProcessedTime = Date.now(); + } + + if (pkg === 'com.tencent.mm') { + // 微信:识别是否是详情页 + const isDetail = texts.includes('交易单号') || texts.includes('退款单号') || texts.includes('本服务由财付通提供'); + if (isDetail) { + const success = await parseAndProcessAccessibilityTexts(texts, pkg); + if (!success) { + logger.info('layout', '[无障碍] 微信详情页直接文本解析未成功,降级触发 OCR 识别'); + bridge?.triggerManualOcr(); + } + } else { + logger.info('layout', '[无障碍] 微信非详情页面,不触发记账与截图'); + } + } else if (pkg === 'com.eg.android.AlipayGphone') { + // 支付宝:识别是否是详情页 + const isDetail = texts.includes('创建时间') || texts.includes('账单详情') || texts.includes('对此订单有疑问'); + if (isDetail) { + const success = await parseAndProcessAccessibilityTexts(texts, pkg); + if (!success) { + logger.info('layout', '[无障碍] 支付宝详情页直接文本解析未成功,降级触发 OCR 识别'); + bridge?.triggerManualOcr(); + } + } else { + logger.info('layout', '[无障碍] 支付宝非详情页面,不触发记账与截图'); + } + } else { + // 其他白名单应用(如各大银行、QQ、TIM等),直接通过截图 + OCR 识别 + logger.info('layout', `[无障碍] 检测到白名单应用 ${pkg},直接触发 OCR 识别`); + bridge?.triggerManualOcr(); + } + } catch (e) { + logger.error('layout', '无障碍调试及直接解析执行失败', e); + } + }), ]; return () => subscriptions.forEach(s => s.remove()); @@ -144,12 +351,15 @@ function AppShell() { const sub = AppState.addEventListener('change', (state) => { // 切到后台/非活跃时立即遮盖,回到前台时移除 setPrivacyOverlay(state !== 'active'); + if (state === 'active' && phase === 'ready') { + checkAndReloadLedger(); + } }); return () => sub.remove(); - }, []); + }, [phase, checkAndReloadLedger]); // loading 状态 - if (phase === 'loading') { + if (phase === 'loading' || (!fontsLoaded && !fontError)) { return ( {t('app.name')} @@ -176,7 +386,7 @@ function AppShell() { // ready:主应用 return ( - + @@ -185,7 +395,6 @@ function AppShell() { - @@ -202,7 +411,7 @@ function AppShell() { {privacyOverlay && ( )} - + ); } diff --git a/src/app/account/index.tsx b/src/app/account/index.tsx index a8689bf..1371c91 100644 --- a/src/app/account/index.tsx +++ b/src/app/account/index.tsx @@ -92,6 +92,13 @@ export default function AccountScreen() { }) .join(':'); + const rootType = sanitized.split(':')[0]; + const validRoots = ['Assets', 'Liabilities', 'Income', 'Expenses', 'Equity']; + if (!validRoots.includes(rootType)) { + Alert.alert(t('account.addFail'), t('account.invalidRootAccount')); + return; + } + try { await autoOpenAccounts([sanitized]); setIsAdding(false); @@ -220,7 +227,7 @@ export default function AccountScreen() { {balance} {currency} @@ -241,8 +248,8 @@ export default function AccountScreen() { onPress={() => handleCloseAccount(account)} style={[styles.closeBtn, { borderColor: theme.colors.border }]} > - - + + {t('account.closeAccount')} diff --git a/src/app/ai/chat.tsx b/src/app/ai/chat.tsx index ca37465..54a3ab3 100644 --- a/src/app/ai/chat.tsx +++ b/src/app/ai/chat.tsx @@ -125,9 +125,12 @@ export default function AIChatScreen() { {card.counterparty} · {card.narration} - - {t('ai.tapToRecord')} → - + + + {t('ai.tapToRecord')} + + + ))} diff --git a/src/app/automation/index.tsx b/src/app/automation/index.tsx index c0bfb99..7ae6f5f 100644 --- a/src/app/automation/index.tsx +++ b/src/app/automation/index.tsx @@ -16,7 +16,7 @@ * - 支付 App 白名单展示 */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Alert, FlatList, Linking, NativeModules, Platform, Pressable, StyleSheet, Text, View } from 'react-native'; +import { Alert, AppState, FlatList, Linking, NativeModules, Platform, Pressable, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; @@ -25,7 +25,6 @@ import { useT } from '../../i18n'; import { useAutomationStore } from '../../store/automationStore'; import { useLedgerStore } from '../../store/ledgerStore'; import { useMetadataStore } from '../../store/metadataStore'; -import { useImportStore } from '../../store/importStore'; import { Card } from '../../components/Card'; import { Button } from '../../components/Button'; import type { AutomationSource } from '../../store/automationStore'; @@ -89,6 +88,16 @@ export default function AutomationScreen() { useEffect(() => { refreshAccessibilityState(); + + const subscription = AppState.addEventListener('change', (nextAppState) => { + if (nextAppState === 'active') { + refreshAccessibilityState(); + } + }); + + return () => { + subscription.remove(); + }; }, [refreshAccessibilityState]); const handleProcess = async () => { @@ -96,9 +105,8 @@ export default function AutomationScreen() { const rules = useMetadataStore.getState().rules; const categories = useMetadataStore.getState().categories; const history = ledger.transactions; - const accountMap = useImportStore.getState().accountMap; try { - await processAll(ledger, rules, categories, history, accountMap); + await processAll(ledger, rules, categories, history); } catch (e) { Alert.alert(String(e)); } @@ -136,8 +144,8 @@ export default function AutomationScreen() { const handleOpenAccessibilitySettings = () => { if (Platform.OS !== 'android') return; - // 打开系统无障碍设置页面 - Linking.openURL('android.settings.ACCESSIBILITY_SETTINGS').catch(() => { + // 打开系统无障碍设置页面 (Intent Action) + Linking.sendIntent('android.settings.ACCESSIBILITY_SETTINGS').catch(() => { Alert.alert(t('automation.openSettingsFail')); }); }; @@ -148,16 +156,30 @@ export default function AutomationScreen() { Alert.alert(t('automation.bridgeUnavailable')); return; } - try { - const result = await bridge.rememberCurrentPage(); - Alert.alert( - t('automation.rememberPageSuccess'), - t('automation.rememberPageDesc', { pkg: getPackageLabel(result.package), activity: result.activity }), - ); - refreshAccessibilityState(); - } catch (e) { - Alert.alert(t('automation.rememberPageFail'), String((e as Error)?.message ?? e)); - } + Alert.alert( + '准备记录目标页面', + '点击“开始”后,请在 8 秒内切换到支付宝/微信的账单详情页,系统将在倒计时结束后自动记录该页面的签名。', + [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: '开始 (8s 倒计时)', + onPress: () => { + setTimeout(async () => { + try { + const result = await bridge.rememberCurrentPage(); + Alert.alert( + t('automation.rememberPageSuccess'), + t('automation.rememberPageDesc', { pkg: getPackageLabel(result.package), activity: result.activity }), + ); + refreshAccessibilityState(); + } catch (e) { + Alert.alert(t('automation.rememberPageFail'), String((e as Error)?.message ?? e)); + } + }, 8000); + }, + }, + ], + ); }; const handleManualOcr = async () => { @@ -166,12 +188,25 @@ export default function AutomationScreen() { Alert.alert(t('automation.bridgeUnavailable')); return; } - try { - await bridge.triggerManualOcr(); - Alert.alert(t('automation.manualOcrTriggered')); - } catch (e) { - Alert.alert(t('automation.manualOcrFail'), String((e as Error)?.message ?? e)); - } + Alert.alert( + '准备手动识别', + '点击“开始”后,请在 8 秒内切换到你想识别的账单详情页,系统将在 8 秒后自动截图并识别该页面。', + [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: '开始 (8s 倒计时)', + onPress: () => { + setTimeout(async () => { + try { + await bridge.triggerManualOcr(); + } catch (e) { + Alert.alert(t('automation.manualOcrFail'), String((e as Error)?.message ?? e)); + } + }, 8000); + }, + }, + ], + ); }; const handleRemoveSignature = async (sig: string) => { @@ -222,7 +257,7 @@ export default function AutomationScreen() { `${item.draft.date}-${index}`} + keyExtractor={(item, index) => item.draft.sourceEventId ?? `${item.draft.date}-${index}`} renderItem={({ item, index }) => ( @@ -257,7 +292,7 @@ export default function AutomationScreen() { {/* 服务状态 */} - + {serviceRunning ? t('automation.serviceRunning') : t('automation.serviceStopped')} diff --git a/src/app/budget/index.tsx b/src/app/budget/index.tsx index 8324950..6954e47 100644 --- a/src/app/budget/index.tsx +++ b/src/app/budget/index.tsx @@ -50,6 +50,12 @@ export default function BudgetScreen() { const categoryAccount = values.categoryAccount?.trim() || undefined; const startDate = values.startDate?.trim() || today; + const amtNum = parseFloat(amount); + if (isNaN(amtNum) || amtNum <= 0) { + Alert.alert(t('budget.invalidAmountTitle'), t('budget.invalidAmountDesc')); + return; + } + if (modal?.type === 'add') { addBudget({ id: generateId('bud'), name, amount, period, categoryAccount, startDate }); } else if (modal?.type === 'edit') { @@ -61,10 +67,10 @@ export default function BudgetScreen() { const periodLabel = (p: string) => p === 'monthly' ? t('budget.periodMonthly') : p === 'weekly' ? t('budget.periodWeekly') : t('budget.periodYearly'); const editFields: FormField[] = modal?.type === 'edit' ? [ - { key: 'name', label: t('budget.fieldName'), placeholder: '餐饮月预算', defaultValue: modal.budget.name }, - { key: 'amount', label: t('budget.fieldAmount'), placeholder: '2000', defaultValue: modal.budget.amount, keyboardType: 'decimal-pad' }, + { key: 'name', label: t('budget.fieldName'), placeholder: t('budget.fieldNamePlaceholder'), defaultValue: modal.budget.name }, + { key: 'amount', label: t('budget.fieldAmount'), placeholder: t('budget.fieldAmountPlaceholder'), defaultValue: modal.budget.amount, keyboardType: 'decimal-pad' }, { key: 'period', label: t('budget.fieldPeriod'), placeholder: 'monthly', defaultValue: modal.budget.period }, - { key: 'categoryAccount', label: t('budget.fieldCategoryAccount'), placeholder: 'Expenses:餐饮', defaultValue: modal.budget.categoryAccount ?? '' }, + { key: 'categoryAccount', label: t('budget.fieldCategoryAccount'), placeholder: t('budget.fieldCategoryPlaceholder'), defaultValue: modal.budget.categoryAccount ?? '' }, { key: 'startDate', label: t('budget.fieldStartDate'), placeholder: '2026-01-01', defaultValue: modal.budget.startDate }, ] : []; @@ -131,10 +137,10 @@ export default function BudgetScreen() { visible={modal !== null} title={modal?.type === 'edit' ? t('budget.editTitle') : t('budget.add')} fields={modal?.type === 'edit' ? editFields : [ - { key: 'name', label: t('budget.fieldName'), placeholder: '餐饮月预算' }, - { key: 'amount', label: t('budget.fieldAmount'), placeholder: '2000', keyboardType: 'decimal-pad' }, + { key: 'name', label: t('budget.fieldName'), placeholder: t('budget.fieldNamePlaceholder') }, + { key: 'amount', label: t('budget.fieldAmount'), placeholder: t('budget.fieldAmountPlaceholder'), keyboardType: 'decimal-pad' }, { key: 'period', label: t('budget.fieldPeriod'), placeholder: 'monthly' }, - { key: 'categoryAccount', label: t('budget.fieldCategoryAccount'), placeholder: 'Expenses:餐饮' }, + { key: 'categoryAccount', label: t('budget.fieldCategoryAccount'), placeholder: t('budget.fieldCategoryPlaceholder') }, { key: 'startDate', label: t('budget.fieldStartDate'), placeholder: '2026-01-01' }, ]} onConfirm={handleConfirm} diff --git a/src/app/calendar/index.tsx b/src/app/calendar/index.tsx deleted file mode 100644 index 14975e9..0000000 --- a/src/app/calendar/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * 日历视图页面(plan.md「1.1 calendar/index」)。 - */ -import React, { useState } from 'react'; -import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { useRouter } from 'expo-router'; -import { Ionicons } from '@expo/vector-icons'; -import { useTheme } from '../../theme'; -import { CalendarView } from '../../components/CalendarView'; -import { TransactionCard } from '../../components/TransactionCard'; -import { useT } from '../../i18n'; -import { useLedgerStore } from '../../store/ledgerStore'; -import type { Transaction } from '../../domain/types'; - -export default function CalendarScreen() { - const { theme } = useTheme(); - const t = useT(); - const router = useRouter(); - const ledger = useLedgerStore(s => s.ledger); - const transactions = ledger?.transactions ?? []; - const [selectedDate, setSelectedDate] = useState(null); - const today = new Date(); - const dayTx = selectedDate ? transactions.filter(t => t.date.slice(0, 10) === selectedDate) : []; - - return ( - - - router.back()}> - {t('calendar.title')} - - - - {selectedDate && ( - - {selectedDate} - {dayTx.map(t => router.push(`/transaction/${tx.id}`)} />)} - {dayTx.length === 0 && ( - {t('calendar.noDayTx')} - )} - - )} - - - ); -} - -const styles = StyleSheet.create({ - page: { flex: 1 }, - header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingTop: 8, paddingBottom: 12 }, - content: { padding: 16 }, -}); diff --git a/src/app/category/index.tsx b/src/app/category/index.tsx index c0094fe..103d85a 100644 --- a/src/app/category/index.tsx +++ b/src/app/category/index.tsx @@ -80,9 +80,9 @@ export default function CategoryScreen() { ); const editFields: FormField[] = modal?.type === 'edit' ? [ - { key: 'name', label: t('category.fieldName'), placeholder: '餐饮', defaultValue: modal.category.name }, - { key: 'linkedAccount', label: t('category.fieldLinkedAccount'), placeholder: 'Expenses:餐饮', defaultValue: modal.category.linkedAccount }, - { key: 'keywords', label: t('category.fieldKeywords'), placeholder: '餐, 饭, 咖啡', defaultValue: modal.category.keywords.join(', ') }, + { key: 'name', label: t('category.fieldName'), placeholder: t('category.namePlaceholder'), defaultValue: modal.category.name }, + { key: 'linkedAccount', label: t('category.fieldLinkedAccount'), placeholder: t('category.accountPlaceholder'), defaultValue: modal.category.linkedAccount }, + { key: 'keywords', label: t('category.fieldKeywords'), placeholder: t('category.keywordsPlaceholder'), defaultValue: modal.category.keywords.join(', ') }, ] : []; return ( @@ -115,9 +115,9 @@ export default function CategoryScreen() { visible={modal !== null} title={modal?.type === 'edit' ? t('category.editTitle') : t('category.addTitle', { type: modal?.catType === 'income' ? t('category.income') : t('category.expense') })} fields={modal?.type === 'edit' ? editFields : [ - { key: 'name', label: t('category.fieldName'), placeholder: '餐饮' }, - { key: 'linkedAccount', label: t('category.fieldLinkedAccount'), placeholder: 'Expenses:餐饮' }, - { key: 'keywords', label: t('category.fieldKeywords'), placeholder: '餐, 饭, 咖啡' }, + { key: 'name', label: t('category.fieldName'), placeholder: t('category.namePlaceholder') }, + { key: 'linkedAccount', label: t('category.fieldLinkedAccount'), placeholder: t('category.accountPlaceholder') }, + { key: 'keywords', label: t('category.fieldKeywords'), placeholder: t('category.keywordsPlaceholder') }, ]} onConfirm={handleConfirm} onCancel={() => setModal(null)} diff --git a/src/app/import/index.tsx b/src/app/import/index.tsx index 553b696..0964173 100644 --- a/src/app/import/index.tsx +++ b/src/app/import/index.tsx @@ -136,7 +136,6 @@ export default function ImportScreen() { importCsv(content, adapter); setManualDuplicates([]); setStatus(t('importFlow.importSuccess', { - channel: isWeChat ? t('importFlow.channelWechat') : t('importFlow.channelAlipay'), format: isExcel ? t('importFlow.formatExcel') : t('importFlow.formatCsv'), name: file.name, })); @@ -361,7 +360,7 @@ export default function ImportScreen() { {item.memo || t('importFlow.noDesc')} - {t('importFlow.duplicateAmount', { amount: item.amount, currency: item.currency, channel: item.channel })} + {t('importFlow.duplicateAmount', { amount: item.amount, currency: item.currency })} {t('recurring.perAmount')} - {amount} CNY + {amount} CNY @@ -151,8 +151,8 @@ export default function RecurringScreen() { onPress={() => handleDelete(item)} style={[styles.closeBtn, { borderColor: theme.colors.border }]} > - - + + {t('recurring.delete')} diff --git a/src/app/rules/index.tsx b/src/app/rules/index.tsx index dd991eb..8cfeca2 100644 --- a/src/app/rules/index.tsx +++ b/src/app/rules/index.tsx @@ -43,10 +43,9 @@ export default function RulesScreen() { const handleConfirm = (values: Record) => { const rule: Omit = { priority: parseInt(values.priority, 10) || 0, - channel: values.channel?.trim() || undefined, counterpartyContains: values.counterpartyContains?.trim() || undefined, memoContains: values.memoContains?.trim() || undefined, - channelAccount: values.channelAccount?.trim() || 'Assets:Unknown', + sourceAccount: values.sourceAccount?.trim() || 'Assets:Unknown', categoryAccount: values.categoryAccount?.trim() || 'Expenses:Uncategorized', narration: values.narration?.trim() || undefined, tags: values.tags ? values.tags.split(/[,,\s]+/).filter(Boolean) : [], @@ -61,10 +60,9 @@ export default function RulesScreen() { const fieldsFor = (rule?: Rule): FormField[] => [ { key: 'priority', label: t('rules.fieldPriority'), placeholder: '100', defaultValue: rule ? String(rule.priority) : '', keyboardType: 'numeric' }, - { key: 'channel', label: t('rules.fieldChannel'), placeholder: 'Alipay', defaultValue: rule?.channel ?? '' }, { key: 'counterpartyContains', label: t('rules.fieldCounterparty'), placeholder: '咖啡', defaultValue: rule?.counterpartyContains ?? '' }, { key: 'memoContains', label: t('rules.fieldMemo'), placeholder: '', defaultValue: rule?.memoContains ?? '' }, - { key: 'channelAccount', label: t('rules.fieldChannelAccount'), placeholder: 'Assets:支付宝余额', defaultValue: rule?.channelAccount ?? '' }, + { key: 'sourceAccount', label: t('rules.fieldSourceAccount'), placeholder: 'Assets:支付宝余额', defaultValue: rule?.sourceAccount ?? '' }, { key: 'categoryAccount', label: t('rules.fieldCategoryAccount'), placeholder: 'Expenses:餐饮', defaultValue: rule?.categoryAccount ?? '' }, { key: 'narration', label: t('rules.fieldNarration'), placeholder: '咖啡', defaultValue: rule?.narration ?? '' }, { key: 'tags', label: t('rules.fieldTags'), placeholder: 'food', defaultValue: rule?.tags?.join(', ') ?? '' }, @@ -72,7 +70,6 @@ export default function RulesScreen() { const formatCondition = (rule: Rule): string => { const parts: string[] = []; - if (rule.channel) parts.push(t('rules.condChannel', { val: rule.channel })); if (rule.counterpartyContains) parts.push(t('rules.condCounterparty', { val: rule.counterpartyContains })); if (rule.memoContains) parts.push(t('rules.condMemo', { val: rule.memoContains })); return parts.length > 0 ? parts.join(' · ') : t('rules.condNone'); @@ -110,9 +107,12 @@ export default function RulesScreen() { {formatCondition(rule)} - - → {rule.categoryAccount} ({t('rules.hitsSuffix', { count: rule.hits })}) - + + + + {rule.categoryAccount} ({t('rules.hitsSuffix', { count: rule.hits })}) + + ))} diff --git a/src/app/settings/sync.tsx b/src/app/settings/sync.tsx index eb70bb9..456a321 100644 --- a/src/app/settings/sync.tsx +++ b/src/app/settings/sync.tsx @@ -30,7 +30,7 @@ export default function SyncSettingsScreen() { const webdavUrl = useSettingsStore(s => s.webdavUrl) || ''; const webdavUsername = useSettingsStore(s => s.webdavUsername) || ''; const webdavPassword = useSettingsStore(s => s.webdavPassword) || ''; - const webdavRemotePath = useSettingsStore(s => s.webdavRemotePath) || 'mobile.bean'; + const webdavRemotePath = useSettingsStore(s => s.webdavRemotePath) || 'main.bean'; const gitRemoteUrl = useSettingsStore(s => s.gitRemoteUrl) || ''; const gitBranch = useSettingsStore(s => s.gitBranch) || 'main'; const gitUsername = useSettingsStore(s => s.gitUsername) || ''; @@ -51,7 +51,7 @@ export default function SyncSettingsScreen() { { key: 'url', label: t('sync.webdavUrl'), placeholder: 'https://dav.example.com/beancount/', defaultValue: webdavUrl }, { key: 'username', label: t('sync.username'), placeholder: 'username', defaultValue: webdavUsername }, { key: 'password', label: t('sync.password'), placeholder: 'password', defaultValue: webdavPassword }, - { key: 'remotePath', label: t('sync.remotePath'), placeholder: 'mobile.bean', defaultValue: webdavRemotePath }, + { key: 'remotePath', label: t('sync.remotePath'), placeholder: 'main.bean', defaultValue: webdavRemotePath }, ]; // Git credentials form fields @@ -145,7 +145,7 @@ export default function SyncSettingsScreen() { try { const fs = new MockICloudFileSystem(); fs.available = true; - fs.setRemoteFile('mobile.bean', `${t('sync.iCloudDemoContent')}\n`, new Date().toISOString()); + fs.setRemoteFile('main.bean', `${t('sync.iCloudDemoContent')}\n`, new Date().toISOString()); const result = await syncWithICloud(fs, mobileBean, new Date().toISOString()); if (result.action === 'pulled' || result.action === 'conflict') { await replaceMobileBean(result.content); @@ -164,9 +164,49 @@ export default function SyncSettingsScreen() { const handleBackup = async () => { try { const files = ledger?.files ?? []; - const bundle = createBackupBundle(files, mobileBean); + // 收集设置(剔除敏感字段和函数) + const settingsState = useSettingsStore.getState(); + const settings: Record = {}; + for (const [k, v] of Object.entries(settingsState)) { + if (typeof v !== 'function' && !['webdavPassword', 'gitPassword', 'appLockPin'].includes(k)) { + settings[k] = v; + } + } + // 收集元数据(分类/规则/预算/标签/信用卡/周期记账) + const metaState = useMetadataStore.getState(); + const metadata: Record = { + categories: metaState.categories, + tags: metaState.tags, + budgets: metaState.budgets, + creditCards: metaState.creditCards, + rules: metaState.rules, + recurringTransactions: metaState.recurringTransactions, + }; + + const bundle = createBackupBundle(files, mobileBean, settings, metadata); const json = serializeBundle(bundle); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + + // Android 特殊处理:使用 SAF (存储访问框架) 直接让用户选择文件夹并保存 + if (Platform.OS === 'android') { + try { + const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(); + if (permissions.granted) { + const fileUri = await FileSystem.StorageAccessFramework.createFileAsync( + permissions.directoryUri, + `backup-${timestamp}`, + 'application/json', + ); + await FileSystem.writeAsStringAsync(fileUri, json); + Alert.alert(t('sync.backupSuccess'), t('sync.backupSuccessFolder')); + return; + } + } catch (safError) { + // 如果 SAF 失败,降级到系统分享 + console.warn('SAF backup failed, falling back to sharing:', safError); + } + } + const backupPath = FileSystem.documentDirectory + `backup-${timestamp}.json`; await FileSystem.writeAsStringAsync(backupPath, json); @@ -195,10 +235,33 @@ export default function SyncSettingsScreen() { const content = await FileSystem.readAsStringAsync(file.uri); const bundle = deserializeBundle(content); const restoredFiles = restoreFiles(bundle); - // 用恢复的 mobileBean 替换当前内容 - const restoredMobileBean = restoredFiles.find(f => f.path === 'mobile.bean')?.content ?? ''; + // 用恢复的 content 替换当前内容,支持新版 main.bean 并且兼容旧版 mobile.bean + const restoredMobileBean = restoredFiles.find(f => f.path === 'main.bean')?.content || restoredFiles.find(f => f.path === 'mobile.bean')?.content || ''; await replaceMobileBean(restoredMobileBean); - Alert.alert(t('sync.restoreSuccess'), t('sync.restoreDesc', { name: file.name })); + + // 恢复设置(v2) + if (bundle.settings) { + useSettingsStore.getState().hydrate(bundle.settings as any); + // 同步持久化到文件 + const settingsPath = FileSystem.documentDirectory + 'settings.json'; + await FileSystem.writeAsStringAsync(settingsPath, JSON.stringify(bundle.settings, null, 2)); + } + + // 恢复元数据(v2) + if (bundle.metadata) { + useMetadataStore.getState().hydrate(bundle.metadata as any); + // 同步持久化到文件 + const metadataPath = FileSystem.documentDirectory + 'metadata.json'; + await FileSystem.writeAsStringAsync(metadataPath, JSON.stringify(bundle.metadata, null, 2)); + } + + const restoredParts = [t('sync.restorePartsTx')]; + if (bundle.settings) restoredParts.push(t('sync.restorePartsSettings')); + if (bundle.metadata) restoredParts.push(t('sync.restorePartsMeta')); + Alert.alert( + t('sync.restoreSuccess'), + `${t('sync.restoreDesc', { name: file.name })}:${restoredParts.join('、')}`, + ); } catch (e) { Alert.alert(t('sync.restoreFail'), e instanceof Error ? e.message : String(e)); } @@ -282,6 +345,7 @@ export default function SyncSettingsScreen() { const workbook = XLSX.utils.book_new(); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const exportPath = FileSystem.documentDirectory + `transactions-${timestamp}.xlsx`; + let base64Content = ''; // 用 xlsx 库创建 workbook wrapper const wbWrapper = { addSheet: (name: string, rows: any[]) => { @@ -289,11 +353,31 @@ export default function SyncSettingsScreen() { XLSX.utils.book_append_sheet(workbook, ws, name); }, write: (path: string) => { - const base64 = XLSX.write(workbook, { type: 'base64', bookType: 'xlsx' }); - return FileSystem.writeAsStringAsync(path, base64, { encoding: FileSystem.EncodingType.Base64 }); + base64Content = XLSX.write(workbook, { type: 'base64', bookType: 'xlsx' }); + return FileSystem.writeAsStringAsync(path, base64Content, { encoding: FileSystem.EncodingType.Base64 }); }, }; await exportToExcel(transactions, wbWrapper as any, exportPath); + + // Android 特殊处理:使用 SAF 直接保存到本地公开目录 + if (Platform.OS === 'android') { + try { + const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(); + if (permissions.granted) { + const fileUri = await FileSystem.StorageAccessFramework.createFileAsync( + permissions.directoryUri, + `transactions-${timestamp}`, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + await FileSystem.writeAsStringAsync(fileUri, base64Content, { encoding: FileSystem.EncodingType.Base64 }); + Alert.alert(t('sync.exportSuccess'), 'Excel 账单已成功保存到所选文件夹!'); + return; + } + } catch (safError) { + console.warn('SAF Excel export failed, falling back to sharing:', safError); + } + } + if (await Sharing.isAvailableAsync()) { await Sharing.shareAsync(exportPath, { mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); } @@ -313,10 +397,30 @@ export default function SyncSettingsScreen() { const exportPath = FileSystem.documentDirectory + `rules-${timestamp}.json`; const fs = new MockRuleFileSync(); // MockRuleFileSync 是内存的,需要真实写入 - const result = await exportRules(rules, categories, fs, exportPath); + await exportRules(rules, categories, fs, exportPath); // 获取 Mock 写入的内容,用真实 FileSystem 写 const content = await fs.read(exportPath); await FileSystem.writeAsStringAsync(exportPath, content); + + // Android 特殊处理:使用 SAF 直接保存到本地公开目录 + if (Platform.OS === 'android') { + try { + const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync(); + if (permissions.granted) { + const fileUri = await FileSystem.StorageAccessFramework.createFileAsync( + permissions.directoryUri, + `rules-${timestamp}`, + 'application/json', + ); + await FileSystem.writeAsStringAsync(fileUri, content); + Alert.alert(t('sync.exportSuccess'), '匹配规则已成功保存到所选文件夹!'); + return; + } + } catch (safError) { + console.warn('SAF Rules export failed, falling back to sharing:', safError); + } + } + if (await Sharing.isAvailableAsync()) { await Sharing.shareAsync(exportPath, { mimeType: 'application/json' }); } @@ -365,8 +469,12 @@ export default function SyncSettingsScreen() { {/* 云同步配置与云端操作 */} -