Files
system-design/software-copyright/10-writech-app-pad/自然写互动课堂平板端应用软件-鉴别材料.md
T
2026-03-22 15:24:40 +08:00

2600 lines
115 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 自然写互动课堂平板端应用软件 V1.0
## 鉴别材料
---
**软件名称**:自然写互动课堂平板端应用软件
**版本号**V1.0
**著作权人**:深圳自然写科技有限公司
**开发完成日期**2024年6月
**文档类型**:用户操作手册 + 设计说明书
---
## 目录
- 第一章 软件整体概述
- 1.1 软件简介与功能综述
- 1.2 软件用途与适用场景
- 1.3 运行环境与系统要求
- 1.4 开发语言与技术规范
- 1.5 版本说明
- 第二章 系统架构与设计思路
- 2.1 总体架构设计
- 2.2 各层次详细说明
- 2.3 核心模块架构图
- 2.4 数据设计
- 2.5 接口设计
- 2.6 安全设计
- 2.7 部署架构
- 第三章 核心模块功能详细说明
- 3.1 学生端作业作答模块
- 3.2 教师端移动授课模块
- 3.3 笔迹渲染模块
- 3.4 蓝牙点阵笔连接模块
- 3.5 字帖练习与笔顺指导模块
- 3.6 错题本自动整理模块
- 3.7 学习计划与进度管理模块
- 3.8 护眼模式与使用时长管控模块
- 第四章 操作流程与使用步骤
- 4.1 安装与首次设置
- 4.2 登录与角色切换
- 4.3 学生端操作流程
- 4.4 教师端操作流程
- 4.5 字帖练习操作流程
- 4.6 护眼模式与家长控制
- 4.7 异常处理与故障排除
- 第五章 与源代码的对应关系
- 5.1 模块名称与源代码文件对应表
- 5.2 核心功能类与方法说明
- 5.3 主要类命名规范
- 附录
---
## 第一章 软件整体概述
### 1.1 软件简介与功能综述
自然写互动课堂平板端应用软件(以下简称"Pad 端应用")是自然写互动课堂教学系统的重要学习终端,面向学生和教师两类使用群体,运行于 Android 8.0+ 平板和 iPadOS 14.0+ 平板设备上。
Pad 端应用采用跨平台 Flutter 框架开发,与手机端应用共享核心业务逻辑模块,同时针对平板大屏幕提供专项优化的自适应布局。相比手机端,Pad 端具有更大的显示区域,能够更完整地展示字帖内容和书写作品,书写体验也更接近真实纸张。
**功能综述一览:**
| 角色 | 功能名称 | 功能描述 |
|------|---------|---------|
| 学生端 | 接收作业/试卷 | 从云平台下载教师布置的作业或试卷,支持离线模式 |
| 学生端 | 配合点阵笔纸上作答 | 通过 BLE 连接点阵笔,实时接收在点阵纸上的书写内容 |
| 学生端 | 触屏直接书写 | 无点阵笔时可直接在 Pad 屏幕上触控书写作答 |
| 学生端 | 查看批改结果 | 查看教师/AI 批改后的作业,含批注和错误分析 |
| 学生端 | 字帖练习 | 按字帖模板练习书写,实时笔顺指导和评分 |
| 学生端 | 错题本管理 | 自动整理历次作业错题,智能推荐复习 |
| 学生端 | 学习计划 | 查看并完成每日/每周学习任务 |
| 教师端 | 移动授课 | 在 Pad 上展示课件并随时批注 |
| 教师端 | 巡堂查看 | 实时查看全班学生书写进度 |
| 教师端 | 即时点评 | 对学生作品进行语音/文字即时点评 |
| 全体 | 护眼模式 | 色温调节、使用时长提醒、前置摄像头距离检测 |
### 1.2 软件用途与适用场景
**主要用途:**
Pad 端应用是自然写互动课堂系统在学生个人学习设备端的核心软件,支持课内互动作答和课后自主练习两大场景。学生通过 Pad 端应用完成作业提交、字帖练习、学情查看等日常学习任务;教师通过 Pad 端应用在移动状态下管理课堂、批阅作业、查看学情分析。
**适用场景说明:**
| 场景 | 参与角色 | 功能使用 |
|------|---------|---------|
| 课堂练习 | 学生 | 配合点阵笔在点阵纸上完成课堂练习,Pad 作为数据接收终端 |
| 课后作业 | 学生 | 下载作业,用点阵笔/触屏书写作答,提交后等待批改 |
| 字帖临摹 | 学生 | 在 Pad 上选择字帖,对照字帖用手指或点阵笔练习 |
| 错题复习 | 学生 | 查看错题本,有针对性地重新练习 |
| 巡堂监控 | 教师 | 教师手持 Pad 在教室内走动,实时查看各学生书写状态 |
| 即时批改 | 教师 | 在 Pad 上直接对学生作品进行批注和评分 |
| 家长监督 | 家长 | 通过家长控制功能设定学习时间,查看孩子学习报告 |
### 1.3 运行环境与系统要求
**Android 平板硬件要求:**
| 项目 | 最低要求 | 推荐配置 |
|------|---------|---------|
| 操作系统 | Android 8.0API Level 26 | Android 11.0+ |
| 处理器 | 4核 ARM Cortex-A53 @ 1.5GHz | 8核 ARM Cortex-A75 @ 2.0GHz+ |
| 内存 | 3GB RAM | 6GB RAM |
| 存储 | 32GB(可用 ≥ 8GB | 64GB |
| 屏幕尺寸 | 8寸 | 10.5寸 ~ 11寸 |
| 屏幕分辨率 | 1280×800 | 2000×12002K |
| 蓝牙 | BLE 4.2 | BLE 5.0 |
| 网络 | Wi-Fi 802.11n2.4GHz | Wi-Fi 802.11ac5GHz |
| 相机 | 前置摄像头(护眼距离检测) | 500万像素前置摄像头 |
**iPadOS 硬件要求:**
| 项目 | 最低要求 | 推荐配置 |
|------|---------|---------|
| 操作系统 | iPadOS 14.0 | iPadOS 16.0+ |
| 设备型号 | iPad(第8代)或更新 | iPad Pro 11寸 |
| 存储 | 64GB | 128GB |
| 蓝牙 | 支持 BLE 5.0 | 支持 BLE 5.0 |
**网络要求:**
| 场景 | 要求 |
|------|------|
| 作业下载 | Wi-Fi 10Mbps+ 或 4G LTE |
| 作业上传(书写笔迹) | Wi-Fi 5Mbps+(典型作业 < 5MB |
| 课堂实时同步 | Wi-Fi 5Mbps+,延迟 ≤ 100ms |
| 离线模式 | 无网络,已下载作业可本地作答 |
### 1.4 开发语言与技术规范
| 语言/框架 | 版本 | 用途 |
|---------|------|------|
| Flutter | 3.16.x | 主框架,跨平台 UI 与业务逻辑 |
| Dart | 3.2.x | 主要编程语言 |
| Kotlin | 1.9.x | Android 原生插件(BLE、护眼摄像头) |
| Swift | 5.9.x | iOS 原生插件(CoreBluetooth、健康数据) |
| flutter_bloc | 8.1.x | BLoC 状态管理 |
| Dio | 5.3.x | HTTP 网络请求 |
| flutter_blue_plus | 1.29.x | BLE 点阵笔连接 |
| Hive | 2.2.x | 本地轻量级 NoSQL 存储(离线数据) |
| sqflite | 2.3.x | SQLite 本地数据库 |
| CustomPainter | Flutter内置 | 笔迹渲染(Skia 2D 引擎) |
| go_router | 13.x | 声明式路由导航 |
| freezed | 2.4.x | 不可变数据类(BLoC 事件/状态) |
| json_serializable | 6.7.x | JSON 序列化代码生成 |
**架构规范:**
- 遵循 Flutter BLoC + MVVM 架构,与手机端代码共享 `lib/features/` 下的核心业务模块
- Pad 专用适配代码位于 `lib/adaptive/` 目录,通过屏幕宽度阈值(768dp)切换 Pad/Phone 布局
- 单元测试覆盖 BLoC 层,Widget 测试覆盖关键 UI 组件
### 1.5 版本说明
| 版本 | 日期 | 说明 |
|------|------|------|
| V1.0.0 | 2024-06 | 正式版本发布(Android + iOS 双平台) |
| V0.9.0 | 2024-04 | Beta:护眼模式、错题本功能完成 |
| V0.7.0 | 2024-02 | AlphaPad 自适应布局完成,与手机端代码分离 |
---
## 第二章 系统架构与设计思路
### 2.1 总体架构设计
Pad 端应用采用 Flutter 跨平台 MVVM 架构,与手机端共享 `lib/features/` 下的核心业务逻辑(BLoC + Repository),通过平台适配层(`lib/adaptive/`)实现 Pad 专用的大屏幕布局。
整体架构分为八个层次:UI层(Pad自适应布局)→ 状态管理层(BLoC/Provider)→ 笔迹渲染层(CustomPainter + Skia)→ 业务逻辑层→ 数据层 → 网络层 → 蓝牙层 → 护眼层。
```
┌──────────────────────────────────────────────────────────────────────┐
│ UI 层(Flutter Widget - Pad 自适应布局) │
│ ┌─────────────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
│ │ 学生端主界面 │ │ 教师端主界面 │ │ 字帖练习界面 │ │
│ │ (Pad双栏布局) │ │ (Pad双栏布局) │ │ (大字临摹布局) │ │
│ └─────────────────┘ └───────────────┘ └──────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ 状态管理层(BLoC / Provider
│ ┌───────────┐ ┌────────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │HomeworkBloc│ │ QuizBloc │ │ PracticeBloc│ │ TeacherBloc │ │
│ └───────────┘ └────────────┘ └──────────┘ └──────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ 笔迹渲染层(CustomPainter + Skia
│ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ InkCanvasPainter │ │ StrokeReplayPainter │ │
│ │ (实时渲染:BLE笔/触屏)│ │ (书写回放:贝塞尔曲线动画) │ │
│ └──────────────────────┘ └──────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ 业务逻辑层(Dart / Kotlin / Swift
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │作业管理 │ │字帖训练 │ │错题本管理 │ │学习计划 │ │护眼管理 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └───────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ 数据层(SQLite + Hive
│ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ sqfliteSQLite │ │ Hive(离线轻量KV存储) │ │
│ │ 作业/笔迹/错题/进度 │ │ 用户配置/离线队列/学习统计 │ │
│ └──────────────────────┘ └──────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ 网络层(Dio + WebSocket
│ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ ApiClientDio │ │ ClassroomSocketWebSocket │ │
│ │ 云平台 REST API │ │ 课堂实时指令/笔迹同步 │ │
│ └──────────────────────┘ └──────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ 蓝牙层(flutter_blue_plus
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ PenBleManagerBLE 扫描/连接/断线重连/GATT Notify 数据接收) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ 护眼层(自研护眼模块) │
│ ┌────────────┐ ┌──────────────┐ ┌────────────────────────────────┐ │
│ │ 色温调节 │ │ 使用时长提醒 │ │ 距离检测(前置摄像头分析) │ │
│ └────────────┘ └──────────────┘ └────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
### 2.2 各层次详细说明
#### 2.2.1 UI 层(Pad 自适应布局)
Pad 端应用针对平板大屏幕(8寸~13寸)设计了专项的自适应布局策略:
- **双栏布局**:屏幕宽度 ≥ 768dp 时启用双栏布局(左侧导航 + 右侧内容区),充分利用平板横屏空间
- **大字临摹布局**:字帖练习界面提供完整的参考字展示区(左半屏)+ 学生书写区(右半屏)对照布局
- **横竖屏自适应**:自动响应设备旋转,横屏双栏、竖屏单栏无缝切换
- **高分辨率优化**:针对 2K 分辨率 Pad 提供高清笔迹渲染(2倍像素密度)
#### 2.2.2 状态管理层(BLoC
采用 flutter_bloc 库实现 BLoC 模式,每个主要业务场景对应一个 BLoC 类:
- **HomeworkBloc**:作业列表、作业详情、作答提交状态管理
- **QuizBloc**:课堂互动答题状态(发题/作答/查看结果)
- **PracticeBloc**:字帖练习状态(选帖/练习中/评分展示)
- **TeacherBloc**:教师端状态(巡堂模式/批改模式/授课模式)
- **EyeProtectionBloc**:护眼功能状态(色温/时长/距离检测)
#### 2.2.3 笔迹渲染层
笔迹渲染基于 Flutter CustomPainter + Skia 2D 渲染引擎:
- **InkCanvasPainter**:实时渲染 BLE 笔迹和触屏笔迹,贝塞尔曲线平滑
- **StrokeReplayPainter**:书写回放动画,按时间序列重现学生书写过程
- **CalligraphyPainter**:字帖模板渲染(笔顺序号、参考笔画、红色描红线)
- **AnnotationPainter**:教师批注渲染(叠加在学生作品上方的批注层)
#### 2.2.4 业务逻辑层
业务逻辑层通过 Repository 模式隔离数据来源,核心业务与手机端共享代码:
- **HomeworkRepository**:作业数据管理(下载、缓存、提交、批改结果查询)
- **PracticeRepository**:练字数据管理(字帖获取、练习记录、评分历史)
- **MistakeRepository**:错题本数据管理(自动整理、分类、推荐复习)
- **LearningPlanRepository**:学习计划数据管理(任务生成、进度跟踪)
- **EyeProtectionRepository**:护眼数据管理(使用时长记录、家长设置同步)
#### 2.2.5 数据层
本地数据存储采用双存储引擎策略:
- **sqfliteSQLite)**:存储结构化数据(作业记录、笔迹数据、错题本、学习进度)
- **Hive**:存储轻量级键值数据(用户配置、离线操作队列、应用缓存)
- **文件系统**:存储大文件(下载的字帖模板图片、教师批改后的作业快照)
#### 2.2.6 蓝牙层(PenBleManager
BLE 点阵笔连接管理,基于 flutter_blue_plus 插件:
- 扫描周围 BLE 设备,过滤自然写点阵笔(UUID 匹配)
- 建立 GATT 连接,订阅笔迹数据 CharacteristicNotify
- 接收原始 BLE 数据包,解析为 `InkPoint`x, y, pressure, timestamp
- 断线自动重连策略(保存已配对笔的 MAC 地址,重启自动重连)
#### 2.2.7 护眼层
护眼功能由三个独立子模块组成:
- **色温调节**:通过 Android Display API / iOS UIScreen 调节屏幕色温(暖色护眼模式)
- **使用时长提醒**:记录每次使用时长,达到设定阈值时弹出休息提醒(家长可远程设置)
- **距离检测**:调用前置摄像头(仅 Android,本地 ML 处理),检测用眼距离,过近时发出警告
距离检测特别说明:前置摄像头仅在护眼检测功能开启时调用,图像数据仅在本地处理(ML Kit 人脸检测),不上传到服务器,保护学生隐私。
### 2.3 核心模块架构图
**Pad 端数据流图:**
```
BLE 点阵笔
│ BLE GATT Notify(笔迹原始数据包)
PenBleManagerflutter_blue_plus
│ 解析 → InkPointx,y,pressure,timestamp
InkBloc(事件驱动)
│ InkPointReceived 事件
InkCanvasPainterCustomPainter
│ Skia 绘制(贝塞尔平滑)
屏幕笔迹实时展示
同时:
InkBloc ──► HomeworkRepository.cacheInkData()
│ sqflite 本地缓存
(网络恢复时)ApiClient.submitHomework()
│ HTTPS POST(笔迹数据 + 元信息)
云平台(AI 批改 + 教师批改)
```
**学生课堂答题数据流:**
```
云平台/网关 → WebSocket 推送题目
ClassroomSocket.onQuizReceived
QuizBlocQuizReceived 事件)
│ 状态切换:Idle → Active
答题界面显示(题目内容 + 作答区域)
│ 学生书写作答
InkCanvasPainter(实时渲染)
HomeworkBloc.submitAnswer()
│ WebSocket 上报答案
网关 → 黑板端汇总展示
```
### 2.4 数据设计
#### 2.4.1 数据库表结构(sqflite
**homework 表(作业记录)**
| 字段名 | 数据类型 | 说明 |
|-------|---------|------|
| id | TEXT PRIMARY KEY | 作业ID(云平台全局唯一) |
| title | TEXT NOT NULL | 作业标题 |
| subject | TEXT | 科目(语文/数学/英语等) |
| type | INTEGER | 类型(1=练字 2=作文 3=计算 4=综合) |
| content_json | TEXT | 作业内容(JSON,含题目和字帖) |
| due_at | INTEGER | 截止时间戳 |
| status | INTEGER | 状态(0=未开始 1=进行中 2=已提交 3=已批改) |
| score | REAL | 得分(-1=未批改) |
| feedback_json | TEXT | 批改反馈 JSON |
| created_at | INTEGER | 下发时间戳 |
| updated_at | INTEGER | 最后更新时间戳 |
**ink_data 表(笔迹数据)**
| 字段名 | 数据类型 | 说明 |
|-------|---------|------|
| id | INTEGER PRIMARY KEY | 自增主键 |
| homework_id | TEXT NOT NULL | 所属作业ID |
| page_index | INTEGER | 页码 |
| ink_points | BLOB | 笔迹点序列(压缩后的二进制数据) |
| stroke_count | INTEGER | 笔画数 |
| created_at | INTEGER | 记录时间戳 |
| is_uploaded | INTEGER | 是否已上传(0=否 1=是) |
**mistake_book 表(错题本)**
| 字段名 | 数据类型 | 说明 |
|-------|---------|------|
| id | INTEGER PRIMARY KEY | 自增主键 |
| homework_id | TEXT | 来源作业ID |
| character | TEXT | 错误汉字/题目内容 |
| wrong_ink | BLOB | 错误书写笔迹快照(PNG 压缩) |
| correct_ink | BLOB | 正确示范笔迹快照 |
| error_type | TEXT | 错误类型(笔画错误/结构错误/笔顺错误/字形偏差) |
| knowledge_point | TEXT | 知识点标签 |
| review_count | INTEGER | 已复习次数 |
| next_review_at | INTEGER | 下次复习时间戳(Leitner 间隔复习算法) |
| created_at | INTEGER | 加入时间戳 |
**study_plan 表(学习计划)**
| 字段名 | 数据类型 | 说明 |
|-------|---------|------|
| id | INTEGER PRIMARY KEY | 自增主键 |
| plan_date | TEXT | 计划日期(YYYY-MM-DD |
| task_type | INTEGER | 任务类型(1=作业 2=练字 3=错题复习 4=自由练习) |
| task_id | TEXT | 关联任务ID |
| duration_min | INTEGER | 计划时长(分钟) |
| is_completed | INTEGER | 是否完成(0=否 1=是) |
| completed_at | INTEGER | 完成时间戳 |
**usage_log 表(使用时长记录)**
| 字段名 | 数据类型 | 说明 |
|-------|---------|------|
| id | INTEGER PRIMARY KEY | 自增主键 |
| log_date | TEXT | 记录日期(YYYY-MM-DD |
| session_start | INTEGER | 本次使用开始时间戳 |
| session_end | INTEGER | 本次使用结束时间戳 |
| duration_sec | INTEGER | 本次使用时长(秒) |
| activity | TEXT | 活动类型(作业/练字/查看报告等) |
#### 2.4.2 Hive 存储结构
```dart
// Hive Box 定义
class HiveBoxes {
static const userPrefs = 'user_preferences'; // 用户偏好设置
static const offlineQueue = 'offline_queue'; // 离线操作队列
static const appCache = 'app_cache'; // 应用通用缓存
static const eyeProtection = 'eye_protection'; // 护眼设置
}
// 用户偏好(存储在 user_preferences Box
@HiveType(typeId: 1)
class UserPreferences extends HiveObject {
@HiveField(0) String themeMode; // light / dark / system
@HiveField(1) String inkColor; // 默认笔色
@HiveField(2) double inkWidth; // 默认笔粗
@HiveField(3) bool autoConnectPen; // 自动连接点阵笔
@HiveField(4) String lastPenMac; // 上次连接的笔 MAC
@HiveField(5) bool eyeProtectEnabled; // 护眼模式开关
}
```
#### 2.4.3 核心数据结构定义
```dart
// 笔迹点
class InkPoint {
final double x; // 归一化坐标 [0.0, 1.0]
final double y; // 归一化坐标 [0.0, 1.0]
final double pressure; // 压感值 [0.0, 1.0](触屏为 0.5 固定值)
final int timestamp; // 微秒时间戳
final bool isPenUp; // 是否抬笔(笔画结束标记)
const InkPoint({
required this.x, required this.y,
required this.pressure, required this.timestamp,
this.isPenUp = false,
});
}
// 作业
class Homework {
final String id;
final String title;
final String subject;
final HomeworkType type;
final List<HomeworkPage> pages; // 多页作业内容
final DateTime dueAt;
HomeworkStatus status;
double? score; // AI 批改分数
List<TeacherAnnotation>? annotations; // 教师批注
}
// 字帖模板
class CalligraphyTemplate {
final String id;
final String character; // 练习汉字
final int strokeCount; // 笔画数
final List<Stroke> strokes; // 标准笔顺笔画(坐标序列)
final Uint8List? referenceImage; // 参考图片(高清毛笔字/楷书)
final List<String> strokeNames; // 各笔画名称(横/竖/撇/捺/折等)
}
```
### 2.5 接口设计
#### 2.5.1 云平台 API 接口
| 接口名称 | 方法 | 路径 | 说明 |
|---------|------|------|------|
| 登录(学生/教师) | POST | `/api/v1/auth/login` | 账号密码登录,返回 JWT Token |
| 刷新 Token | POST | `/api/v1/auth/refresh` | 刷新过期的 Token |
| 获取作业列表 | GET | `/api/v1/homework/list` | 获取当前学生的作业列表 |
| 下载作业内容 | GET | `/api/v1/homework/{id}/content` | 下载作业详情(题目、字帖内容) |
| 提交作业 | POST | `/api/v1/homework/{id}/submit` | 上传学生作答笔迹数据 |
| 获取批改结果 | GET | `/api/v1/homework/{id}/result` | 获取 AI/教师批改结果 |
| 获取字帖模板 | GET | `/api/v1/calligraphy/templates` | 获取字帖模板列表 |
| 下载字帖 | GET | `/api/v1/calligraphy/{id}` | 下载字帖详情(笔顺数据+参考图) |
| 上传练习记录 | POST | `/api/v1/calligraphy/practice` | 上传练字记录(评分数据) |
| 获取错题列表 | GET | `/api/v1/mistakes/list` | 获取学生错题本 |
| 获取学情报告 | GET | `/api/v1/report/student/{id}` | 获取学生学情分析报告 |
| 获取学习计划 | GET | `/api/v1/plan/current` | 获取当前学习计划任务 |
| 同步使用时长 | POST | `/api/v1/usage/sync` | 上传使用时长(家长监控) |
**网络请求头统一规范:**
```dart
class ApiInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.headers.addAll({
'Authorization': 'Bearer ${AuthManager.token}',
'X-Platform': Platform.isAndroid ? 'android-pad' : 'ios-pad',
'X-App-Version': AppConfig.version,
'X-Device-Id': DeviceInfo.deviceId,
'Content-Type': 'application/json',
});
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 401) {
// Token 过期,触发自动刷新
AuthManager.refreshToken().then((_) => retry(err.requestOptions));
}
handler.next(err);
}
}
```
#### 2.5.2 BLE 点阵笔接口
```dart
class PenBleManager {
// 扫描周围点阵笔(过滤条件:服务 UUID 匹配)
Stream<BluetoothDevice> scanPens({Duration timeout = const Duration(seconds: 10)})
// 连接指定点阵笔
Future<void> connectPen(BluetoothDevice device)
// 断开连接
Future<void> disconnectPen()
// 笔迹数据流(持续订阅 GATT Notify Characteristic
Stream<List<InkPoint>> get inkDataStream
// 当前连接状态流
Stream<PenConnectionState> get connectionStateStream
// 获取电量(读取 Battery Level Characteristic
Future<int> getBatteryLevel()
}
// 自然写点阵笔 GATT 服务定义
class WritechPenGatt {
static const serviceUuid = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
static const inkCharUuid = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'; // Notify
static const cmdCharUuid = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; // Write
static const battCharUuid = '00002a19-0000-1000-8000-00805f9b34fb'; // Battery
}
```
#### 2.5.3 课堂实时 WebSocket 接口
```dart
class ClassroomSocket {
// 连接课堂 WebSocket
Future<void> connect(String sessionId)
// 课堂互动事件流(发题/收卷/暂停等)
Stream<ClassroomEvent> get classroomEventStream
// 发送答案
Future<void> submitAnswer(String quizId, dynamic answer)
// 发送实时笔迹(学生作答时实时同步到黑板)
void sendInkFrame(InkFrame frame)
}
```
### 2.6 安全设计
**账户安全:**
```dart
class SecureAuthStorage {
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
// Token 存储到系统安全区域(Android Keystore / iOS Keychain
Future<void> saveToken(String token) =>
_secureStorage.write(key: 'auth_token', value: token);
Future<String?> readToken() =>
_secureStorage.read(key: 'auth_token');
Future<void> clearToken() =>
_secureStorage.delete(key: 'auth_token');
}
```
**数据安全:**
- 学生笔迹数据本地存储采用 AES-256 加密(通过 SQLCipher for Flutter 插件)
- 作业内容下载后以加密形式缓存,防止设备丢失后题目泄露
- 网络传输强制 HTTPSTLS 1.2+),证书绑定(Certificate Pinning)防止中间人攻击
**隐私保护:**
- 护眼距离检测前置摄像头图像数据仅在本地处理,使用 Google ML Kit 人脸检测 API
- 图像数据不持久化存储,仅在内存中临时使用
- 隐私政策明确告知:摄像头仅用于护眼距离检测,不用于身份识别
**使用管控:**
- 家长可通过家长端(手机APP)远程设置:
- 每日使用时长上限(0~8小时)
- 允许使用时段(如仅允许 17:00~21:00
- 强制休息提醒间隔(如每45分钟休息10分钟)
- 时长控制通过服务端下发配置,本地应用守规执行
### 2.7 部署架构
```
学生 Pad(自然写 Pad 端应用)
├── BLE 5.0(直连自然写点阵笔,距离 ≤ 10m)
├── Wi-Fi(教室局域网)
│ │
│ ├── WebSocket → 教室网关(课堂实时同步)
│ │
│ └── HTTPS → 自然写云平台(作业/资源)
└── 本地存储(SQLite + Hive + 文件系统)
离线模式下所有数据本地缓存
网络恢复后自动同步
```
---
## 第三章 核心模块功能详细说明
### 3.1 学生端作业作答模块
#### 3.1.1 模块功能描述
学生端作业作答模块是 Pad 端应用最核心的功能,支持学生接收教师布置的作业,通过点阵笔纸上书写(BLE 传输)或直接触屏书写完成作答,并提交给云平台进行 AI/教师批改。
**作答模式说明:**
| 模式 | 条件 | 描述 |
|------|------|------|
| 点阵笔纸上书写 | 已连接点阵笔 + 有点阵纸 | 学生在点阵纸上用点阵笔书写,Pad 实时显示笔迹 |
| 触屏直接书写 | 无点阵笔时 | 学生直接用手指在 Pad 屏幕上书写作答 |
| 离线模式 | 无网络时 | 先本地缓存作答数据,网络恢复后自动提交 |
#### 3.1.2 作业作答界面布局(Pad 横屏双栏)
```
┌──────────────────────────────────────────────────────────────────────┐
│ 作业:语文练字 - 第3单元 字帖 [提交] [保存草稿] │
├────────────────────────┬─────────────────────────────────────────────┤
│ 题目与要求(左栏) │ 书写区域(右栏) │
│ │ │
│ 练习字: │ ┌─────────────────────────────────────────┐ │
│ ┌─────────────────┐ │ │ │ │
│ │ 春 │ │ │ │ │
│ │ (楷体参考字) │ │ │ 书写区域 │ │
│ └─────────────────┘ │ │ (点阵笔/触屏书写) │ │
│ │ │ │ │
│ 笔顺提示: │ │ │ │
│ ① 横 ② 横 ③ 撇 │ └─────────────────────────────────────────┘ │
│ ④ 捺 ⑤ 竖 ⑥ 横折 │ [橡皮擦] [撤销] [清除] [完成本字] │
│ ⑦ 横 ⑧ 竖 ⑨ 横 │ │
│ │ 进度:3/10字 ● ● ● ○ ○ ○ ○ ○ ○ ○ │
│ 注意事项: │ │
│ - 横折钩注意转折处 │ 🔵 点阵笔已连接(85%电量) │
│ - 撇的收笔要轻 │ │
│ │ │
├────────────────────────┴─────────────────────────────────────────────┤
│ 上一字 [←] [← ○ → ○ → ○ ● ○ → ○ →] 下一字 [→] │
└──────────────────────────────────────────────────────────────────────┘
```
#### 3.1.3 作业作答流程
```dart
class HomeworkBloc extends Bloc<HomeworkEvent, HomeworkState> {
HomeworkBloc({
required HomeworkRepository homeworkRepo,
required PenBleManager penBleManager,
}) : super(const HomeworkState.initial()) {
// 加载作业内容
on<LoadHomework>((event, emit) async {
emit(const HomeworkState.loading());
try {
final homework = await homeworkRepo.getHomeworkById(event.homeworkId);
final inkData = await homeworkRepo.getCachedInkData(event.homeworkId);
emit(HomeworkState.loaded(homework: homework, inkData: inkData));
} catch (e) {
emit(HomeworkState.error(message: e.toString()));
}
});
// 接收笔迹点(来自点阵笔 BLE 或触屏)
on<InkPointReceived>((event, emit) {
final current = state as HomeworkStateLoaded;
final updatedInk = current.currentPageInk.copyWith(
points: [...current.currentPageInk.points, event.point],
);
// 本地缓存笔迹(每100个点保存一次)
if (updatedInk.points.length % 100 == 0) {
homeworkRepo.cacheInkData(current.homework.id, current.pageIndex, updatedInk);
}
emit(current.copyWith(currentPageInk: updatedInk));
});
// 提交作业
on<SubmitHomework>((event, emit) async {
final current = state as HomeworkStateLoaded;
emit(current.copyWith(isSubmitting: true));
try {
await homeworkRepo.submitHomework(
homeworkId: current.homework.id,
inkPages: current.allPagesInk,
);
emit(current.copyWith(isSubmitting: false, isSubmitted: true));
} on NetworkException {
// 网络异常:加入离线队列
await homeworkRepo.addToOfflineQueue(current.homework.id, current.allPagesInk);
emit(current.copyWith(isSubmitting: false, isOfflineQueued: true));
}
});
}
}
```
#### 3.1.4 离线同步机制
```dart
class OfflineSyncService {
final HomeworkRepository homeworkRepo;
final ApiClient apiClient;
// 监听网络恢复,自动同步离线队列
void startMonitoring() {
Connectivity().onConnectivityChanged.listen((result) async {
if (result != ConnectivityResult.none) {
await _syncOfflineQueue();
}
});
}
Future<void> _syncOfflineQueue() async {
final queue = await homeworkRepo.getOfflineQueue();
for (final item in queue) {
try {
await apiClient.submitHomework(
homeworkId: item.homeworkId,
inkData: item.inkData,
);
await homeworkRepo.removeFromOfflineQueue(item.id);
} catch (e) {
// 单条失败不影响其他条目,继续处理
continue;
}
}
}
}
```
---
### 3.2 教师端移动授课模块
#### 3.2.1 模块功能描述
教师端移动授课模块为教师提供在 Pad 上进行课堂教学管理的功能,包括移动查看全班学生书写进度、对学生作品进行即时点评和批注,以及在 Pad 上展示课件并批注。
#### 3.2.2 教师端巡堂界面
```
┌──────────────────────────────────────────────────────────────────────┐
│ 教师端 - 巡堂监控界面 班级:三年级2班 语文 │
├────────────────────────┬─────────────────────────────────────────────┤
│ 全班进度(左栏) │ 学生详情(右栏)- 点击左侧学生查看 │
│ │ │
│ ┌─────┐ ┌─────┐ │ 张三 - 进行中 │
│ │张三 │ │李四 │ │ ┌─────────────────────────────────────┐ │
│ │● │ │● │ │ │ │ │
│ └─────┘ └─────┘ │ │ [学生实时笔迹展示] │ │
│ ┌─────┐ ┌─────┐ │ │ │ │
│ │王五 │ │赵六 │ │ └─────────────────────────────────────┘ │
│ │● │ │○ │ │ │
│ └─────┘ └─────┘ │ AI 评分:87分 笔顺:正确 字形:良好 │
│ ... │ │
│ (全班缩略图) │ [语音点评] [文字批注] [投屏展示] [优秀标注] │
│ │ │
│ 完成率:18/30 │ │
├────────────────────────┴─────────────────────────────────────────────┤
│ [全屏展示] [发布答题] [抽取学生] [查看统计报告] │
└──────────────────────────────────────────────────────────────────────┘
```
#### 3.2.3 即时点评功能实现
```dart
class AnnotationPainter extends CustomPainter {
final List<Stroke> studentStrokes; // 学生笔迹
final List<TeacherAnnotation> annotations; // 教师批注(叠加层)
@override
void paint(Canvas canvas, Size size) {
// 第一层:渲染学生笔迹(较淡,作为底图)
_drawStudentStrokes(canvas, size);
// 第二层:渲染教师批注(鲜明颜色叠加)
for (final annotation in annotations) {
switch (annotation.type) {
case AnnotationType.ink:
_drawAnnotationStrokes(canvas, size, annotation.strokes, annotation.color);
case AnnotationType.circle:
_drawCircle(canvas, size, annotation.rect, annotation.color);
case AnnotationType.arrow:
_drawArrow(canvas, size, annotation.start, annotation.end, annotation.color);
case AnnotationType.text:
_drawTextLabel(canvas, size, annotation.text, annotation.position);
}
}
}
}
```
---
### 3.3 笔迹渲染模块
#### 3.3.1 模块功能描述
笔迹渲染模块基于 Flutter CustomPainter + Dart Skia 2D 引擎,提供高性能的笔迹实时渲染和书写回放能力,支持压感宽度变化(BLE 笔迹)和触屏模拟压感。
#### 3.3.2 实时渲染实现
```dart
class InkCanvasPainter extends CustomPainter {
final List<Stroke> strokes; // 已完成的笔画
final List<InkPoint> current; // 当前正在书写的笔画点序列
static final Paint _inkPaint = Paint()
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..isAntiAlias = true;
@override
void paint(Canvas canvas, Size size) {
// 渲染历史笔画
for (final stroke in strokes) {
_drawStroke(canvas, size, stroke);
}
// 渲染当前笔画
if (current.isNotEmpty) {
_drawCurrentStroke(canvas, size, current);
}
}
void _drawStroke(Canvas canvas, Size size, Stroke stroke) {
if (stroke.points.length < 2) return;
final path = Path();
path.moveTo(
stroke.points.first.x * size.width,
stroke.points.first.y * size.height,
);
for (int i = 1; i < stroke.points.length - 1; i++) {
final p = stroke.points[i];
final pNext = stroke.points[i + 1];
// 贝塞尔曲线平滑:中点作为终点,当前点作为控制点
final midX = (p.x + pNext.x) / 2.0 * size.width;
final midY = (p.y + pNext.y) / 2.0 * size.height;
path.quadraticBezierTo(
p.x * size.width, p.y * size.height,
midX, midY,
);
// 压感宽度变化(BLE 笔迹有真实压感,触屏用速度模拟)
_inkPaint.strokeWidth = _calcWidth(p.pressure, stroke.baseWidth);
}
_inkPaint.color = Color(stroke.colorArgb);
canvas.drawPath(path, _inkPaint);
}
double _calcWidth(double pressure, double baseWidth) {
// 压感曲线:小压感偏细,大压感偏粗(非线性映射)
final normalized = pressure.clamp(0.0, 1.0);
return baseWidth * (0.4 + 0.6 * normalized);
}
@override
bool shouldRepaint(InkCanvasPainter oldDelegate) {
return oldDelegate.strokes != strokes || oldDelegate.current != current;
}
}
```
#### 3.3.3 书写回放实现
```dart
class StrokeReplayController {
final List<Stroke> strokes;
final double replaySpeed; // 1.0x 正常速度,2.0x 两倍速
Timer? _timer;
int _strokeIndex = 0;
int _pointIndex = 0;
final List<Stroke> _visibleStrokes = [];
List<InkPoint> _currentPoints = [];
void Function(List<Stroke>, List<InkPoint>)? onFrameUpdate;
void startReplay() {
final intervalMs = (16 / replaySpeed).round(); // ~60fps
_timer = Timer.periodic(Duration(milliseconds: intervalMs), _tick);
}
void _tick(Timer timer) {
if (_strokeIndex >= strokes.length) {
timer.cancel(); // 回放完成
return;
}
final currentStroke = strokes[_strokeIndex];
if (_pointIndex < currentStroke.points.length) {
// 逐点添加,模拟书写过程
_currentPoints = currentStroke.points.sublist(0, _pointIndex + 1);
_pointIndex++;
} else {
// 当前笔画结束,加入历史,开始下一笔
_visibleStrokes.add(currentStroke);
_currentPoints = [];
_strokeIndex++;
_pointIndex = 0;
}
onFrameUpdate?.call(List.unmodifiable(_visibleStrokes), List.unmodifiable(_currentPoints));
}
}
```
---
### 3.4 蓝牙点阵笔连接模块
#### 3.4.1 模块功能描述
蓝牙点阵笔连接模块负责管理 Pad 与自然写点阵笔之间的 BLE 连接,包括扫描发现、连接管理、笔迹数据接收和断线自动重连。
#### 3.4.2 BLE 连接管理实现
```dart
class PenBleManager {
late FlutterBluePlus _ble;
BluetoothDevice? _connectedDevice;
BluetoothCharacteristic? _inkCharacteristic;
final _inkStreamController = StreamController<List<InkPoint>>.broadcast();
final _connectionStateController = StreamController<PenConnectionState>.broadcast();
Stream<List<InkPoint>> get inkDataStream => _inkStreamController.stream;
Stream<PenConnectionState> get connectionStateStream => _connectionStateController.stream;
// 扫描自然写点阵笔
Stream<BluetoothDevice> scanPens() {
FlutterBluePlus.startScan(
withServices: [Guid(WritechPenGatt.serviceUuid)],
timeout: const Duration(seconds: 15),
);
return FlutterBluePlus.scanResults.map(
(results) => results.map((r) => r.device)
).expand((devices) => devices);
}
// 连接点阵笔
Future<void> connectPen(BluetoothDevice device) async {
await device.connect(timeout: const Duration(seconds: 10));
_connectedDevice = device;
// 发现服务和特征
final services = await device.discoverServices();
final penService = services.firstWhere(
(s) => s.uuid == Guid(WritechPenGatt.serviceUuid),
);
_inkCharacteristic = penService.characteristics.firstWhere(
(c) => c.uuid == Guid(WritechPenGatt.inkCharUuid),
);
// 订阅笔迹 Notify
await _inkCharacteristic!.setNotifyValue(true);
_inkCharacteristic!.lastValueStream.listen(_onInkDataReceived);
// 监听连接状态断线
device.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected) {
_connectionStateController.add(PenConnectionState.disconnected);
_autoReconnect(device); // 启动自动重连
}
});
_connectionStateController.add(PenConnectionState.connected);
}
// 解析 BLE 原始字节为笔迹点列表
void _onInkDataReceived(List<int> bytes) {
final points = <InkPoint>[];
// 每个笔迹点格式:x(2B) + y(2B) + pressure(1B) + timestamp(4B) + flag(1B) = 10 字节
for (int offset = 0; offset + 10 <= bytes.length; offset += 10) {
final x = ((bytes[offset] << 8) | bytes[offset + 1]).toDouble() / 65535.0;
final y = ((bytes[offset + 2] << 8) | bytes[offset + 3]).toDouble() / 65535.0;
final pressure = bytes[offset + 4].toDouble() / 255.0;
final timestamp = (bytes[offset + 5] << 24) | (bytes[offset + 6] << 16) |
(bytes[offset + 7] << 8) | bytes[offset + 8];
final isPenUp = (bytes[offset + 9] & 0x01) != 0;
points.add(InkPoint(x: x, y: y, pressure: pressure,
timestamp: timestamp, isPenUp: isPenUp));
}
if (points.isNotEmpty) {
_inkStreamController.add(points);
}
}
// 自动重连(指数退避:1s, 2s, 4s, 8s...最大30s
void _autoReconnect(BluetoothDevice device) async {
int delaySeconds = 1;
while (_connectedDevice == device) {
await Future.delayed(Duration(seconds: delaySeconds));
try {
await connectPen(device);
return; // 重连成功
} catch (_) {
delaySeconds = (delaySeconds * 2).clamp(1, 30);
}
}
}
}
```
---
### 3.5 字帖练习与笔顺指导模块
#### 3.5.1 模块功能描述
字帖练习与笔顺指导模块为学生提供系统化的书法练习功能,支持楷书、行书、硬笔等多种字帖,并通过实时笔顺检测和评分给出练习指导。
#### 3.5.2 字帖练习界面布局
```
┌──────────────────────────────────────────────────────────────────────┐
│ 字帖练习 - 人教版三年级上册 第1单元 [评分历史] [×] │
├────────────────────────────────┬─────────────────────────────────────┤
│ 参考字区域(左半屏) │ 学生书写区域(右半屏) │
│ │ │
│ ┌──────────────────────────┐ │ ┌─────────────────────────────────┐ │
│ │ │ │ │ │ │
│ │ 春 │ │ │ │ │
│ │ (楷体参考字 - 大字展示) │ │ │ (学生书写区域) │ │
│ │ │ │ │ │ │
│ │ 笔顺:①②③④⑤⑥⑦⑧⑨ │ │ │ │ │
│ └──────────────────────────┘ │ └─────────────────────────────────┘ │
│ │ │
│ 当前笔画:第 ④ 笔 - 捺 │ 已写 ④ 笔 ✓正确 │
│ │ 笔顺得分:100分 │
│ ┌──────────────────────────┐ │ │
│ │ 笔画动画演示 │ │ [橡皮擦] [清除重写] [下一字 →] │
│ │ (当前笔画高亮动画) │ │ │
│ └──────────────────────────┘ │ 总体评分:⭐⭐⭐⭐☆(87分) │
│ │ │
│ [重播动画] [笔顺说明] │ 评分反馈: │
│ │ ✓ 笔顺正确 │
│ │ △ 第③笔撇的收笔偏重 │
│ │ △ 横折折钩转折角度偏大 │
└────────────────────────────────┴─────────────────────────────────────┘
```
#### 3.5.3 笔顺检测算法
```dart
class StrokeOrderChecker {
final CalligraphyTemplate template;
// 检测当前书写笔画是否符合标准笔顺
StrokeOrderResult checkStroke(int currentStrokeIndex, Stroke writtenStroke) {
if (currentStrokeIndex >= template.strokes.length) {
return StrokeOrderResult.extraStroke;
}
final expectedStroke = template.strokes[currentStrokeIndex];
// 起点方向检测(判断是否从正确位置开始)
final startMatch = _checkStartPoint(writtenStroke, expectedStroke);
// 书写方向检测(横/竖/撇/捺的方向向量匹配)
final directionMatch = _checkDirection(writtenStroke, expectedStroke);
// 终点位置检测
final endMatch = _checkEndPoint(writtenStroke, expectedStroke);
if (startMatch && directionMatch && endMatch) {
return StrokeOrderResult.correct;
} else if (!directionMatch) {
return StrokeOrderResult.wrongDirection;
} else {
return StrokeOrderResult.positionError;
}
}
// 计算书写评分(综合笔顺、字形、比例)
PracticeScore calcScore(List<Stroke> writtenStrokes) {
double strokeOrderScore = _evalStrokeOrder(writtenStrokes); // 笔顺满分40分
double shapeScore = _evalShape(writtenStrokes); // 字形满分35分
double proportionScore = _evalProportion(writtenStrokes); // 比例满分25分
return PracticeScore(
total: strokeOrderScore + shapeScore + proportionScore,
strokeOrder: strokeOrderScore,
shape: shapeScore,
proportion: proportionScore,
);
}
}
```
---
### 3.6 错题本自动整理模块
#### 3.6.1 模块功能描述
错题本自动整理模块从教师批改和 AI 批改结果中自动提取学生的错误题目,按错误类型分类整理,并通过 Leitner 间隔复习算法智能安排复习计划。
#### 3.6.2 错题本界面
```
┌──────────────────────────────────────────────────────────────────────┐
│ 我的错题本 [按科目] [按时间] [复习计划] │
├────────────────────────┬─────────────────────────────────────────────┤
│ 错题分类(左栏) │ 错题详情(右栏) │
│ │ │
│ 📚 语文(23题) │ ┌─────────────────────────────────────┐ │
│ ├ 笔画错误(8题) │ │ 我的书写:(错误) │ │
│ ├ 结构错误(7题) │ │ ┌──────────┐ ┌──────────┐ │ │
│ ├ 笔顺错误(5题) │ │ │ [错误笔迹] │ │ [正确示范] │ │ │
│ └ 字形偏差(3题) │ │ └──────────┘ └──────────┘ │ │
│ │ │ 字:春 │ │
│ 📐 数学(12题) │ │ 错误类型:笔顺错误 │ │
│ ├ 计算错误(6题) │ │ 错误描述:第④笔应为"捺",写成了"横" │ │
│ └ 理解错误(6题) │ │ 已复习次数:2次 建议再复习:明天 │ │
│ │ └─────────────────────────────────────┘ │
│ 今日需复习:8题 │ │
│ ●●●●●●●●●○ │ [开始练习此字] [标记已掌握] [删除] │
│ │ │
└────────────────────────┴─────────────────────────────────────────────┘
```
#### 3.6.3 Leitner 间隔复习算法
```dart
class LeitnerScheduler {
// Leitner 卡片盒子间隔天数:盒子0=每天,1=2天,2=4天,3=8天,4=16天,5=已掌握
static const boxIntervals = [1, 2, 4, 8, 16, 999];
// 复习后更新复习计划
MistakeItem updateAfterReview(MistakeItem item, bool isCorrect) {
int newBoxLevel;
if (isCorrect) {
// 答对:升级到下一个盒子
newBoxLevel = (item.leitnerLevel + 1).clamp(0, boxIntervals.length - 1);
} else {
// 答错:退回到第0级(明天重新复习)
newBoxLevel = 0;
}
final nextReviewDate = DateTime.now().add(
Duration(days: boxIntervals[newBoxLevel])
);
return item.copyWith(
leitnerLevel: newBoxLevel,
reviewCount: item.reviewCount + 1,
nextReviewAt: nextReviewDate.millisecondsSinceEpoch,
);
}
// 获取今日需复习的错题列表
Future<List<MistakeItem>> getTodayReviewItems() async {
final now = DateTime.now().millisecondsSinceEpoch;
final items = await mistakeRepository.getAllMistakes();
return items.where((item) =>
item.leitnerLevel < boxIntervals.length - 1 && // 未达到"已掌握"级别
item.nextReviewAt <= now // 到了复习时间
).toList();
}
}
```
---
### 3.7 学习计划与进度管理模块
#### 3.7.1 模块功能描述
学习计划与进度管理模块为学生提供结构化的每日学习任务管理,结合作业截止时间、练字进度和错题复习需求,智能生成每日学习计划,并跟踪完成情况。
#### 3.7.2 学习计划界面
```
┌──────────────────────────────────────────────────────────────────────┐
│ 学习计划 今日:2024年3月15日 周五 │
├──────────────────────────────────────────────────────────────────────┤
│ 今日进度:████████░░ 4/5 任务完成 完成率 80% │
├──────────────────────────────────────────────────────────────────────┤
│ 今日任务 │
│ │
│ ✅ 完成 │ 语文作业 - 第3单元练字(截止今日) 已用时:25分钟 │
│ ✅ 完成 │ 数学作业 - 四则运算练习题 已用时:18分钟 │
│ ✅ 完成 │ 错题复习 - 复习3个错误汉字 已用时:12分钟 │
│ ✅ 完成 │ 字帖练习 - 春夏秋冬4个字(自选) 已用时:20分钟 │
│ ○ 未完 │ 英语作业 - 单词抄写(截止明日) 预计:15分钟 │
│ │
│ 本周统计 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 学习时长 Mon Tue Wed Thu Fri Sat Sun │ │
│ │ 45分 52分 38分 61分 75分 -- -- │ │
│ │ ████████░░ 累计本周:271分钟 目标:300分钟 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ [开始未完成任务] [查看学情报告] │
└──────────────────────────────────────────────────────────────────────┘
```
---
### 3.8 护眼模式与使用时长管控模块
#### 3.8.1 模块功能描述
护眼模式与使用时长管控模块提供保护学生视力的功能组合,包括屏幕色温调节、使用时长提醒和距离过近警告,同时支持家长通过手机端远程配置管控规则。
#### 3.8.2 距离检测实现
```dart
class EyeDistanceDetector {
final CameraController _cameraController;
late final FaceDetector _faceDetector;
Timer? _detectTimer;
bool _isWarning = false;
// 最小安全距离:40cm(面部宽度像素阈值对应估算)
static const minSafeFaceWidthRatio = 0.25; // 面部宽度占屏幕宽度的比例阈值
void startDetection(VoidCallback onTooClose) {
_faceDetector = FaceDetector(
options: FaceDetectorOptions(
enableLandmarks: false,
performanceMode: FaceDetectorMode.fast,
),
);
// 每3秒检测一次(降低功耗)
_detectTimer = Timer.periodic(const Duration(seconds: 3), (_) async {
final image = await _cameraController.takePicture();
final inputImage = InputImage.fromFilePath(image.path);
final faces = await _faceDetector.processImage(inputImage);
if (faces.isNotEmpty) {
final face = faces.first;
final faceWidthRatio = face.boundingBox.width / _cameraController.value.previewSize!.width;
// 面部宽度超过阈值,说明距离过近
if (faceWidthRatio > minSafeFaceWidthRatio && !_isWarning) {
_isWarning = true;
onTooClose(); // 触发警告
// 播放语音提示:"请注意保持正确坐姿,与屏幕距离不少于40厘米"
} else if (faceWidthRatio <= minSafeFaceWidthRatio) {
_isWarning = false;
}
}
// 立即删除拍摄的图片,不持久化
await File(image.path).delete();
});
}
void stopDetection() {
_detectTimer?.cancel();
_faceDetector.close();
}
}
```
---
## 第四章 操作流程与使用步骤
### 4.1 安装与首次设置
#### 4.1.1 Android 平板安装
1. 在 Google Play 商店或学校 MDM 平台搜索"自然写互动课堂",点击安装
2. 安装完成后首次启动,系统申请必要权限:
- 蓝牙(BLE 点阵笔连接)
- 相机(护眼距离检测,可跳过)
- 存储(作业缓存、字帖文件)
3. 选择登录角色(学生/教师)
#### 4.1.2 iPadOS 安装
1. 在 App Store 搜索"自然写互动课堂",点击下载
2. 首次启动申请蓝牙权限和相机权限(iOS 需弹窗确认)
3. 选择登录角色
### 4.2 登录与角色切换
#### 4.2.1 学生登录
```
步骤1:打开应用,在登录界面选择"学生登录"
步骤2:输入学号(或学校统一分配的账号)
步骤3:输入密码(默认密码:学号后6位,首次登录强制修改)
步骤4:选择班级(系统自动根据账号匹配,通常无需手动选择)
步骤5:点击"登录"
步骤6:进入学生端主界面(显示今日作业列表和学习计划)
```
**登录界面示意:**
```
┌──────────────────────────────────────────────────────────────────────┐
│ 自然写互动课堂 │
│ │
│ [学生登录] [教师登录] │
│ ● │
│ │
│ 学号:[___________________________] │
│ 密码:[***************************] │
│ │
│ [ 登 录 ] │
│ │
│ 忘记密码?请联系老师重置 │
└──────────────────────────────────────────────────────────────────────┘
```
### 4.3 学生端操作流程
#### 4.3.1 完成课堂作业流程
```
步骤1:在"今日任务"列表中找到需完成的作业,点击进入
步骤2:查看作业内容(题目/字帖要求)
步骤3:连接点阵笔(弹出蓝牙设备列表,选择自己的笔)
步骤4:在点阵纸上用点阵笔书写,Pad 屏幕实时显示笔迹
步骤5:完成所有页面后,点击"提交"按钮
步骤6:系统提示提交成功(或离线缓存成功)
步骤7:等待教师/AI 批改(通常几分钟内返回结果)
步骤8:收到批改通知后,点击查看批改详情和评分
```
#### 4.3.2 字帖练习流程
```
步骤1:从底部导航选择"练字"功能
步骤2:选择字帖类型(楷书/行书/硬笔/钢笔)
步骤3:选择练习内容(按单元/按年级/自定义)
步骤4:进入练习界面(参考字 + 书写区对照布局)
步骤5:观看笔顺动画演示(可重播)
步骤6:按正确笔顺在书写区书写
步骤7:系统实时检测笔顺并给出提示(✓正确 / ✗错误)
步骤8:完成一字后查看评分(笔顺分 + 字形分 + 比例分)
步骤9:点击"下一字"继续,或"重写"重新练习本字
步骤10:完成全部练习字后查看本次练习总评分
```
#### 4.3.3 查看批改结果
```
步骤1:收到批改通知推送(或主动进入"作业"→"已批改"列表)
步骤2:点击已批改的作业条目,查看批改详情
步骤3:批改详情界面显示:
- 综合评分(百分制)
- AI 自动批改结果(每个字的得分和错误提示)
- 教师手写批注(红色叠加在学生书写上方)
- 教师语音点评(可播放)
步骤4:对有疑问的批改点击"有疑问",发送给老师
步骤5:点击"加入错题本"将错误汉字加入错题本(系统自动判断也会加入)
```
### 4.4 教师端操作流程
#### 4.4.1 布置作业流程
```
步骤1:在教师端主界面点击"布置作业"
步骤2:选择作业类型(练字/作文/综合)
步骤3:设置作业内容(输入题目或选择字帖)
步骤4:设置提交截止时间
步骤5:选择发送班级(支持多班同时发布)
步骤6:点击"发布",作业推送到选定班级的所有学生 Pad
```
#### 4.4.2 批改作业流程
```
步骤1:在教师端"待批改"列表查看学生提交情况
步骤2:点击某学生作业进入批改界面
步骤3:查看 AI 预批改结果(自动标注可能的错误)
步骤4:使用批注工具在学生笔迹上进行人工标注:
- 圈出错误部分(红色画圈)
- 写批注文字(在空白处书写)
- 录制语音评语(时长最长60秒)
步骤5:在右侧输入综合评分
步骤6:点击"提交批改",结果推送给学生
步骤7:点击"下一份"继续批改
```
### 4.5 字帖练习操作流程
**选择字帖类型:**
| 字帖类型 | 适用年级 | 内容说明 |
|---------|---------|---------|
| 人教版楷书字帖 | 小学1~6年级 | 按课本单元编排,楷体标准字 |
| 司马彦钢笔字帖 | 小学4~6年级、初中 | 硬笔书法练习,竖排格式 |
| 书法班专用字帖 | 书法课学生 | 毛笔楷书/行书基础练习 |
| 自定义练习 | 全部 | 教师指定或学生自选汉字 |
**笔顺指导说明:**
- 软件内置《通用规范汉字表》8105字的标准笔顺数据
- 笔顺动画展示:每个笔画按序以蓝色高亮动画演示
- 描红模式:开启后参考字以浅色显示在书写区,学生可直接描写
- 笔顺检测:检测每次抬笔后的笔画是否与标准笔顺一致
### 4.6 护眼模式与家长控制
#### 4.6.1 护眼模式设置
**学生可自行调整:**
| 设置项 | 选项 | 说明 |
|-------|------|------|
| 色温调节 | 正常 / 暖色 / 深暖色 | 暖色减少蓝光,降低眼睛疲劳 |
| 护眼提醒 | 关闭 / 每20分钟 / 每30分钟 / 每45分钟 | 达到时长后弹出休息提醒 |
| 距离检测 | 开启 / 关闭 | 前置摄像头检测用眼距离 |
**家长通过手机端远程控制:**
| 管控项 | 设置方式 | 说明 |
|-------|---------|------|
| 每日使用时长上限 | 0~8小时(步长30分钟) | 超出后应用自动锁定 |
| 允许使用时段 | 设置时间段(如17:00~21:00) | 时段外无法使用 |
| 强制休息间隔 | 关闭 / 20分钟 / 30分钟 / 45分钟 | 满足间隔后强制弹出休息10分钟倒计时 |
| 查看使用报告 | 随时查看 | 每日使用时长、科目分布、完成任务情况 |
### 4.7 异常处理与故障排除
#### 4.7.1 常见问题
| 问题 | 可能原因 | 解决方案 |
|------|---------|---------|
| 点阵笔连接后无笔迹 | BLE 配对未完成 | 删除已有配对,重新扫描连接 |
| 作业提交失败 | 网络异常 | 无需操作,系统自动加入离线队列,网络恢复后自动提交 |
| 字帖下载速度慢 | 网络带宽不足 | 切换至 Wi-Fi 5GHz 频段,避开高峰期下载 |
| 护眼距离检测不准确 | 光线不足/摄像头遮挡 | 确保前置摄像头无遮挡,在明亮环境下使用 |
| 应用崩溃 | 内存不足 | 关闭后台其他应用,在设置中清除缓存 |
| 批改结果迟迟未到 | 服务器处理延迟 | 通常 AI 批改 5 分钟内完成,教师批改根据教师操作时间不固定 |
---
## 第五章 与源代码的对应关系
### 5.1 模块名称与源代码文件对应表
| 文档模块名称 | 源代码文件/目录 | 主要类名 |
|------------|--------------|---------|
| 学生端作业作答模块 | `lib/features/homework/` | `HomeworkBloc`, `HomeworkPage` |
| 教师端移动授课模块 | `lib/features/teacher/` | `TeacherBloc`, `TeacherDashboardPage` |
| 笔迹实时渲染 | `lib/features/ink/painter/ink_canvas_painter.dart` | `InkCanvasPainter` |
| 书写回放渲染 | `lib/features/ink/painter/stroke_replay_painter.dart` | `StrokeReplayPainter` |
| 教师批注渲染 | `lib/features/ink/painter/annotation_painter.dart` | `AnnotationPainter` |
| 书写回放控制器 | `lib/features/ink/replay/stroke_replay_controller.dart` | `StrokeReplayController` |
| BLE 点阵笔连接 | `lib/features/bluetooth/pen_ble_manager.dart` | `PenBleManager` |
| BLE Android 原生层 | `android/app/src/main/kotlin/.../PenBluetoothPlugin.kt` | `PenBluetoothPlugin` |
| BLE iOS 原生层 | `ios/Runner/PenBluetoothPlugin.swift` | `PenBluetoothPlugin` |
| 字帖练习模块 | `lib/features/calligraphy/` | `PracticeBloc`, `PracticePage` |
| 笔顺检测算法 | `lib/features/calligraphy/stroke_order_checker.dart` | `StrokeOrderChecker` |
| 字帖渲染 | `lib/features/calligraphy/calligraphy_painter.dart` | `CalligraphyPainter` |
| 错题本模块 | `lib/features/mistake_book/` | `MistakeBloc`, `MistakeBookPage` |
| 间隔复习调度 | `lib/features/mistake_book/leitner_scheduler.dart` | `LeitnerScheduler` |
| 学习计划模块 | `lib/features/study_plan/` | `StudyPlanBloc`, `StudyPlanPage` |
| 护眼距离检测 | `lib/features/eye_protection/eye_distance_detector.dart` | `EyeDistanceDetector` |
| 护眼 Android 原生 | `android/app/src/main/kotlin/.../EyeProtectionPlugin.kt` | `EyeProtectionPlugin` |
| Pad 自适应布局 | `lib/adaptive/pad/` | `PadHomePage`, `PadHomeworkPage` |
| 网络请求客户端 | `lib/core/network/api_client.dart` | `ApiClient` |
| 网络拦截器 | `lib/core/network/api_interceptor.dart` | `ApiInterceptor` |
| 离线同步服务 | `lib/core/sync/offline_sync_service.dart` | `OfflineSyncService` |
| 安全存储 | `lib/core/security/secure_auth_storage.dart` | `SecureAuthStorage` |
| 作业仓库 | `lib/features/homework/repository/homework_repository.dart` | `HomeworkRepository` |
| 字帖仓库 | `lib/features/calligraphy/repository/calligraphy_repository.dart` | `CalligraphyRepository` |
| 错题仓库 | `lib/features/mistake_book/repository/mistake_repository.dart` | `MistakeRepository` |
### 5.2 核心功能类与方法说明
#### PenBleManager 类
```dart
/// BLE 点阵笔连接管理器
/// 负责扫描、连接自然写点阵笔,接收笔迹数据流,管理断线重连。
class PenBleManager {
/// 扫描周围的自然写点阵笔(过滤:服务 UUID = WritechPenGatt.serviceUuid
/// @param timeout 扫描超时时间,默认15秒
/// @return 发现的点阵笔设备流
Stream<BluetoothDevice> scanPens({Duration timeout})
/// 连接指定的点阵笔,发现服务并订阅笔迹 Notify Characteristic
/// @param device 要连接的 BLE 设备
Future<void> connectPen(BluetoothDevice device)
/// 断开当前连接的点阵笔
Future<void> disconnectPen()
/// 笔迹数据流(持续发出来自 BLE GATT Notify 的笔迹点列表)
Stream<List<InkPoint>> get inkDataStream
/// 连接状态流(connected / disconnected / connecting / reconnecting
Stream<PenConnectionState> get connectionStateStream
/// 读取当前连接笔的电量(0~100)
Future<int> getBatteryLevel()
}
```
#### HomeworkBloc 类
```dart
/// 作业业务逻辑层(BLoC
/// 管理作业加载、笔迹接收、提交和离线处理的状态机。
class HomeworkBloc extends Bloc<HomeworkEvent, HomeworkState> {
/// 响应 LoadHomework 事件:从缓存或网络加载作业详情
on<LoadHomework>((event, emit) async {...})
/// 响应 InkPointReceived 事件:追加笔迹点到当前页面笔迹列表
on<InkPointReceived>((event, emit) {...})
/// 响应 PenUpReceived 事件:结束当前笔画,保存至数据库
on<PenUpReceived>((event, emit) async {...})
/// 响应 SubmitHomework 事件:上传笔迹到云平台(失败时加入离线队列)
on<SubmitHomework>((event, emit) async {...})
/// 响应 ChangePage 事件:切换作业页面(自动保存当前页笔迹)
on<ChangePage>((event, emit) async {...})
}
```
#### InkCanvasPainter 类
```dart
/// 实时笔迹渲染 CustomPainter
/// 使用贝塞尔曲线平滑渲染已完成笔画和当前书写笔画。
class InkCanvasPainter extends CustomPainter {
/// @param strokes 已完成的笔画列表(每条笔画包含点序列和样式)
/// @param current 当前正在书写的笔画点序列(逐点追加,实时更新)
InkCanvasPainter({required this.strokes, required this.current})
/// 渲染所有笔画(历史 + 当前)到 Canvas
@override void paint(Canvas canvas, Size size)
/// 优化:仅当 strokes 或 current 变化时才重绘
@override bool shouldRepaint(InkCanvasPainter oldDelegate)
}
```
### 5.3 主要类命名规范
| 类型 | 命名规范 | 示例 |
|------|---------|------|
| Flutter Page | `{功能}Page` | `HomeworkPage`, `PracticePage` |
| BLoC | `{功能}Bloc` | `HomeworkBloc`, `PracticeBloc` |
| BLoC 事件 | `{动作}{功能}` | `LoadHomework`, `SubmitHomework` |
| BLoC 状态 | `{功能}State` | `HomeworkState`, `PracticeState` |
| CustomPainter | `{内容}Painter` | `InkCanvasPainter`, `CalligraphyPainter` |
| Repository | `{功能}Repository` | `HomeworkRepository`, `MistakeRepository` |
| Manager | `{功能}Manager` | `PenBleManager`, `LeitnerScheduler` |
| DTO | `{名称}Dto` | `HomeworkDto`, `InkDataDto` |
| EntityHive | `{名称}` + `@HiveType` | `UserPreferences`, `OfflineQueueItem` |
| 原生 Plugin | `{功能}Plugin` | `PenBluetoothPlugin`, `EyeProtectionPlugin` |
**代码目录结构:**
```
lib/
├── adaptive/
│ ├── phone/ (手机端专用布局)
│ └── pad/ (Pad 专用自适应布局)
├── core/
│ ├── network/ (ApiClient, 拦截器)
│ ├── security/ (安全存储)
│ ├── sync/ (离线同步服务)
│ └── database/ (sqflite 数据库辅助类)
└── features/
├── auth/ (登录/认证)
├── homework/ (作业模块)
├── ink/ (笔迹渲染相关)
├── bluetooth/ (BLE 点阵笔)
├── calligraphy/ (字帖练习)
├── mistake_book/ (错题本)
├── study_plan/ (学习计划)
├── eye_protection/ (护眼功能)
└── teacher/ (教师端功能)
android/app/src/main/kotlin/.../ (Android 原生插件)
ios/Runner/ (iOS 原生插件 - Swift)
```
---
## 附录
### A. 界面设计稿(GUI Mockup
本附录以平板横屏/竖屏线框图形式呈现Pad APP各核心界面的设计稿(适配10~13英寸Android平板与iPad,支持触控与点阵笔书写)。
---
#### A.1 学生端首页(平板横屏双栏布局)
```
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 09:41 ●●● WiFi 🔋86% 自然写 Pad 学生端│
├──────────────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────┐ ┌─────────────────────────────────────────────┐│
│ │ 今日任务 │ │ 作业详情 ││
│ │ ───────────────────────── │ │ ││
│ │ 📝 语文作业 待完成 │ │ 语文作业 · 2月14日 · 截止 17:00 ││
│ │ 📝 数学作业 待完成 │ │ ││
│ │ 📝 英语作业 ✅已完成 │ │ 第1题: 抄写古诗《春晓》全文 ││
│ │ 📚 字帖练习 待完成 │ │ 第2题: 默写《春晓》第二句 ││
│ │ 🔁 错题复习 3题待复习 │ │ 第3题: 解方程 2x + 5 = 13 ││
│ │ ───────────────────────── │ │ 第4题: 写出以下词语的近义词 ││
│ │ 最近学情 │ │ ││
│ │ 本周掌握度 73.4% │ │ 作答方式:用点阵笔在点阵纸上书写 ││
│ │ [██████████░░░░] │ │ 完成情况:0 / 4 题 ││
│ │ │ │ ││
│ │ ⚠️ 需加强:二元方程 │ │ ┌────────────────────────────────────┐ ││
│ │ │ │ │ ▶ 开始作答(连接点阵笔) │ ││
│ │ [🔔消息 3] [👤个人中心] │ │ └────────────────────────────────────┘ ││
│ └──────────────────────────────┘ └─────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────────────┘
```
---
#### A.2 书写作答界面(学生答题)
```
┌──────────────────────────────────────────────────────────────────────────────────┐
│ ◀ 返回 语文作业 · 第1题 / 4题 点阵笔 ●已连接 PEN-001234 [断开] │
├──────────────────────────────────────────────────────────────────────────────────┤
│ 题目要求:抄写古诗《春晓》全文 │
│ ───────────────────────────────────────────────────────────────────────────── │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ 春眠不觉晓, ← 实时显示点阵笔书写轨迹 │ │ │
│ │ │ 处处闻啼鸟。 (AI实时识别:春眠不觉晓✅) │ │ │
│ │ │ │ │ │
│ │ │ [用点阵笔在此区域的对应纸张上书写] │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────────────┘ │
│ 笔迹同步状态:实时同步中 ● | 网络:WiFi ●在线 | 已写 24 个字符 │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌─────────────────────────┐ │
│ │ [◀ 上一题] │ │ [清除当前] │ │ [下一题 ▶] │ │ [提交作业] │ │
│ └────────────┘ └────────────┘ └────────────┘ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
```
---
#### A.3 字帖练习界面
```
┌──────────────────────────────────────────────────────────────────────────────────┐
│ ◀ 返回 📚 字帖练习 · 楷书入门 · 第3课:基本笔画 进度 15/50 字 │
├──────────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────┐ ┌────────────────────────────────┐ │
│ │ 练习字: 「明」 │ │ 书写评分 │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ ┌───────────────┐ │ │ 综合得分: 87分 │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ [ 范字模板 ] │ │ [ 学生书写 ] │ │ │ 笔顺: ✅ 正确 │ │
│ │ │ │ │ │ │ │ 结构: ✅ 对称均衡 │ │
│ │ │ 明 │ │ (点阵笔书写) │ │ │ 笔画: ⚠️ 横画偏短 │ │
│ │ │ │ │ │ │ │ │ │
│ │ └───────────────┘ └───────────────┘ │ │ 笔顺动画:[▶ 播放示范] │ │
│ │ │ │ │ │
│ │ 笔顺步骤: │ │ [再练一次] [下一个字 →] │ │
│ │ ①日 → ②日月 → ③明 │ │ │ │
│ │ │ │ 已练习:15字 优秀:12 良好:3│ │
│ └─────────────────────────────────────────┘ └────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
```
---
#### A.4 教师端巡堂界面
```
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 09:41 自然写 Pad 教师端 │
├──────────────────────────────────────────────────────────────────────────────────┤
│ 📡 巡堂模式 · 高一(3)班 · 数学课 · 45/45人在线 [结束巡堂] [切到大屏] │
├──────────────────────────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 01-王小花 │ │ 02-张大勇 │ │ 03-陈美玲 │ │ 04-李小虎 │ │ 05-刘芳芳 │ ··· │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │(书写) │ │ │ │(书写) │ │ │ │(空白) │ │ │ │(书写) │ │ │(书写) │ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ │ ●书写中 │ │ ●书写中 │ │ ○未开始 │ │ ●书写中 │ │ ●书写中 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ [已提交: 38/45] [书写中: 7] [未开始: 0] [收卷] [点名] [发下题] │
└──────────────────────────────────────────────────────────────────────────────────┘
```
---
### B. 术语表
| 术语 | 说明 |
|------|------|
| Pad 端 | 平板端,指运行于 Android 平板或 iPad 上的应用实例 |
| 点阵纸 | 印有自然写专用点阵码图案的纸张,供学生用点阵笔书写 |
| 点阵笔 | 自然写智能点阵笔,内置光学传感器识别点阵码坐标 |
| BLoC | Business Logic ComponentFlutter 架构模式(业务逻辑组件) |
| CustomPainter | Flutter 自定义绘制类,基于 Skia 2D 渲染引擎 |
| Skia | Google 开源 2D 图形渲染库,Flutter 底层渲染引擎 |
| Hive | Flutter 高性能轻量级 NoSQL 本地存储库 |
| sqflite | Flutter SQLite 插件,封装 Android/iOS SQLite 接口 |
| flutter_blue_plus | Flutter BLE 通信插件,封装 Android BluetoothAdapter 和 iOS CoreBluetooth |
| BLE GATT | Generic Attribute ProfileBLE 数据交换协议 |
| Leitner 算法 | 基于间隔重复原理的记忆卡片复习调度算法 |
| ML Kit | Google 机器学习移动端 SDK,本项目用于人脸检测(护眼距离) |
| Certificate Pinning | 证书绑定,客户端校验服务器证书指纹,防止中间人攻击 |
| Keychain | iOS 系统安全凭证存储,存储 Token、密码等敏感数据 |
| EncryptedSharedPreferences | Android 加密共享偏好,存储 Token、密码等敏感数据 |
| AAC | Kotlin/Java 架构组件(Android Architecture Components)的简称 |
| 离线队列 | 无网络时暂存的操作队列,网络恢复后自动重试同步 |
| 间隔复习 | 基于遗忘曲线的学习策略,按一定时间间隔安排复习 |
### B. 版本历史
| 版本 | 发布日期 | 变更内容 |
|------|---------|---------|
| V1.0.0 | 2024-06-30 | 正式版本:学生端作业作答、字帖练习、错题本、BLE 笔连接、护眼功能;教师端巡堂、批改 |
| V0.9.5 | 2024-05-25 | Beta:护眼距离检测功能(Android)集成;错题本 Leitner 算法优化 |
| V0.9.0 | 2024-04-20 | BetaPad 自适应双栏布局完成;笔顺检测算法调优 |
| V0.8.0 | 2024-03-15 | Alpha:字帖练习模块、错题本模块集成 |
| V0.7.0 | 2024-02-20 | AlphaPad 专项 UI 布局与手机端共用代码架构重构 |
| V0.5.0 | 2024-01-10 | 原型:BLE 笔连接和作业作答基础功能 |
### C. 第三方依赖清单
| 库名称 | 版本 | 许可证 | 用途 |
|-------|------|-------|------|
| flutter_bloc | 8.1.4 | MIT | BLoC 状态管理 |
| Dio | 5.3.3 | MIT | HTTP 网络请求 |
| flutter_blue_plus | 1.29.4 | BSD-3-Clause | BLE 点阵笔通信 |
| Hive | 2.2.3 | Apache-2.0 | 本地 NoSQL 存储 |
| sqflite | 2.3.2 | MIT | SQLite 本地数据库 |
| go_router | 13.0.1 | BSD-3-Clause | 声明式路由 |
| freezed | 2.4.6 | MIT | 不可变数据类生成 |
| json_serializable | 6.7.1 | BSD-3-Clause | JSON 序列化代码生成 |
| flutter_secure_storage | 9.0.0 | BSD-3-Clause | 安全凭证存储 |
| google_mlkit_face_detection | 0.9.0 | MIT | 护眼人脸距离检测 |
| connectivity_plus | 5.0.2 | BSD-3-Clause | 网络状态监听 |
| permission_handler | 11.1.0 | MIT | 运行时权限申请 |
| Lottie | 2.7.0 | MIT | 笔顺动画渲染 |
| cached_network_image | 3.3.1 | MIT | 网络图片缓存 |
| flutter_local_notifications | 16.2.0 | BSD-3-Clause | 本地通知(使用时长提醒) |
### D. 权限申请说明
| 权限 | 平台 | 用途 | 申请时机 |
|------|------|------|---------|
| BLUETOOTH_SCAN | Android | 扫描 BLE 设备 | 首次连接点阵笔时 |
| BLUETOOTH_CONNECT | Android | 连接 BLE 设备 | 首次连接点阵笔时 |
| ACCESS_FINE_LOCATION | Android | BLE 扫描附加要求 | 首次连接点阵笔时 |
| CAMERA | Android/iOS | 护眼距离检测 | 首次开启护眼检测时 |
| WRITE_EXTERNAL_STORAGE | Android | 字帖文件缓存 | 首次下载字帖时 |
| NSBluetoothAlwaysUsageDescription | iOS | BLE 访问说明 | 安装时声明 |
| NSCameraUsageDescription | iOS | 摄像头访问说明 | 安装时声明 |
| RECEIVE_BOOT_COMPLETED | Android | 开机自启动(护眼监控) | 安装时自动授予 |
| VIBRATE | Android | 护眼提醒震动 | 安装时自动授予 |
---
*本文档版权归深圳自然写科技有限公司所有,所有技术细节与源代码对应关系仅用于软件著作权登记鉴别,请勿用于其他商业用途。*
---
## 附录D 核心技术实现补充
### D.1 字帖临摹功能完整实现
字帖临摹功能是Pad APP的核心特色,提供分格描红、整字临摹和自由创作三种练习模式。
#### D.1.1 字帖渲染与对比评分
```dart
// lib/features/copybook/copybook_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CopybookPage extends StatelessWidget {
final CopybookExercise exercise;
const CopybookPage({required this.exercise, super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CopybookBloc()..add(LoadCopybookExercise(exercise: exercise)),
child: const _CopybookView(),
);
}
}
class _CopybookView extends StatelessWidget {
const _CopybookView();
@override
Widget build(BuildContext context) {
return BlocBuilder<CopybookBloc, CopybookState>(
builder: (context, state) {
if (state is CopybookLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is CopybookReady) {
return _buildExerciseView(context, state);
}
return const SizedBox.shrink();
},
);
}
Widget _buildExerciseView(BuildContext context, CopybookReady state) {
return Scaffold(
appBar: AppBar(
title: Text(state.exercise.title),
actions: [
IconButton(
icon: const Icon(Icons.undo),
onPressed: () => context.read<CopybookBloc>().add(UndoStroke()),
),
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => context.read<CopybookBloc>().add(ClearStrokes()),
),
TextButton(
onPressed: () => context.read<CopybookBloc>().add(SubmitCopybook()),
child: const Text('提交', style: TextStyle(color: Colors.white)),
),
],
),
body: Column(
children: [
// 进度指示器
LinearProgressIndicator(
value: state.currentCharIndex / state.totalChars,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(
Theme.of(context).primaryColor),
),
Expanded(
child: Row(
children: [
// 左侧:字帖参考
Expanded(
flex: 1,
child: CopybookReferencePanel(
character: state.currentChar,
showStrokeOrder: state.showStrokeOrder,
),
),
const VerticalDivider(width: 1),
// 右侧:书写区
Expanded(
flex: 2,
child: CopybookWritingPanel(
character: state.currentChar,
studentStrokes: state.studentStrokes,
onStrokeAdded: (stroke) =>
context.read<CopybookBloc>().add(AddStroke(stroke: stroke)),
),
),
],
),
),
// 底部:实时评分反馈
if (state.latestScore != null)
ScoreFeedbackBar(score: state.latestScore!),
],
),
);
}
}
// 字帖书写评分(基于笔画相似度)
class CopybookScorer {
static const double STROKE_ORDER_WEIGHT = 0.3;
static const double STROKE_SHAPE_WEIGHT = 0.4;
static const double STROKE_POSITION_WEIGHT = 0.3;
/**
* 评分字帖临摹质量
* @param reference 标准字帖笔画数据
* @param student 学生书写笔画数据
* @return 综合评分 [0.0, 100.0]
*/
static double score(List<InkStroke> reference, List<InkStroke> student) {
if (student.isEmpty) return 0.0;
// 1. 笔画顺序分
double orderScore = _scoreStrokeOrder(reference, student);
// 2. 笔画形状分(Hausdorff距离)
double shapeScore = _scoreStrokeShape(reference, student);
// 3. 笔画位置分
double positionScore = _scoreStrokePosition(reference, student);
return (orderScore * STROKE_ORDER_WEIGHT
+ shapeScore * STROKE_SHAPE_WEIGHT
+ positionScore * STROKE_POSITION_WEIGHT) * 100.0;
}
static double _scoreStrokeOrder(
List<InkStroke> reference, List<InkStroke> student) {
// 使用最长公共子序列(LCS)评估笔顺一致性
int n = reference.length;
int m = student.length;
if (n == 0 || m == 0) return 0.0;
List<List<int>> dp = List.generate(n+1, (_) => List.filled(m+1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (reference[i-1].strokeType == student[j-1].strokeType) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = dp[i-1][j] > dp[i][j-1] ? dp[i-1][j] : dp[i][j-1];
}
}
}
return dp[n][m] / n.toDouble();
}
static double _scoreStrokeShape(
List<InkStroke> reference, List<InkStroke> student) {
if (reference.isEmpty || student.isEmpty) return 0.0;
int minLen = reference.length < student.length ? reference.length : student.length;
double totalSim = 0.0;
for (int i = 0; i < minLen; i++) {
totalSim += _strokeSimilarity(reference[i], student[i]);
}
return totalSim / minLen;
}
static double _strokeSimilarity(InkStroke a, InkStroke b) {
// 简化版Hausdorff距离相似度
if (a.points.isEmpty || b.points.isEmpty) return 0.0;
double maxDist = 0.0;
for (final pa in a.points) {
double minDist = double.infinity;
for (final pb in b.points) {
double d = _euclidean(pa, pb);
if (d < minDist) minDist = d;
}
if (minDist > maxDist) maxDist = minDist;
}
// 归一化:距离0对应相似度1,距离>0.2对应相似度0
return (1.0 - (maxDist / 0.2)).clamp(0.0, 1.0);
}
static double _scoreStrokePosition(
List<InkStroke> reference, List<InkStroke> student) {
// 比较书写区域的使用比例
Rect refBounds = _getBoundingRect(reference);
Rect stuBounds = _getBoundingRect(student);
if (refBounds.isEmpty || stuBounds.isEmpty) return 0.5;
double centerDistX = (refBounds.center.dx - stuBounds.center.dx).abs();
double centerDistY = (refBounds.center.dy - stuBounds.center.dy).abs();
double centerDist = (centerDistX * centerDistX + centerDistY * centerDistY) / 2;
return (1.0 - centerDist * 4).clamp(0.0, 1.0);
}
static double _euclidean(InkPoint a, InkPoint b) {
double dx = a.x - b.x, dy = a.y - b.y;
return (dx * dx + dy * dy) < 0.0001 ? 0.0 : (dx * dx + dy * dy) * 0.5;
}
static Rect _getBoundingRect(List<InkStroke> strokes) {
if (strokes.isEmpty) return Rect.zero;
double minX = double.infinity, minY = double.infinity;
double maxX = -double.infinity, maxY = -double.infinity;
for (final s in strokes) {
for (final p in s.points) {
if (p.x < minX) minX = p.x;
if (p.y < minY) minY = p.y;
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
}
return Rect.fromLTRB(minX, minY, maxX, maxY);
}
}
```
### D.2 错题本功能实现
```dart
// lib/features/mistakes/mistakes_repository.dart
import 'package:sqflite/sqflite.dart';
import 'package:hive/hive.dart';
class MistakesRepository {
static const String TABLE_MISTAKES = 'mistakes';
final Database _db;
MistakesRepository({required Database db}) : _db = db;
// 添加错题记录
Future<void> addMistake(MistakeRecord mistake) async {
await _db.insert(
TABLE_MISTAKES,
mistake.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// 获取待复习的错题(Leitner调度)
Future<List<MistakeRecord>> getDueForReview() async {
final today = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final maps = await _db.query(
TABLE_MISTAKES,
where: 'next_review_date <= ? AND mastery_level < 5',
whereArgs: [today],
orderBy: 'next_review_date ASC',
limit: 20,
);
return maps.map((m) => MistakeRecord.fromMap(m)).toList();
}
// 更新错题复习结果(BKT + Leitner双机制)
Future<void> updateReviewResult(String mistakeId, bool correct) async {
final record = await _getMistakeById(mistakeId);
if (record == null) return;
// BKT更新掌握度
double newMastery = _updateBKT(record.masteryLevel, correct);
// Leitner调整复习盒子
int newBox = correct
? (record.leitnerBox + 1).clamp(0, 5)
: 0; // 答错回到第0盒
// 计算下次复习日期
const boxIntervals = [1, 2, 4, 8, 16, 999];
final nextReview = DateTime.now()
.add(Duration(days: boxIntervals[newBox]));
await _db.update(
TABLE_MISTAKES,
{
'mastery_level': newMastery,
'leitner_box': newBox,
'next_review_date': nextReview.millisecondsSinceEpoch ~/ 1000,
'review_count': record.reviewCount + 1,
'last_review_date': DateTime.now().millisecondsSinceEpoch ~/ 1000,
},
where: 'id = ?',
whereArgs: [mistakeId],
);
}
double _updateBKT(double currentMastery, bool correct) {
const pTransit = 0.1;
const pSlip = 0.08;
const pGuess = 0.2;
final pCorrect = currentMastery * (1 - pSlip) + (1 - currentMastery) * pGuess;
double updated;
if (correct) {
updated = (currentMastery * (1 - pSlip)) / pCorrect;
} else {
updated = (currentMastery * pSlip) / (1 - pCorrect);
}
return updated + (1 - updated) * pTransit;
}
}
// 错题数据库Schema
const String createMistakesTable = '''
CREATE TABLE IF NOT EXISTS mistakes (
id TEXT PRIMARY KEY,
student_id TEXT NOT NULL,
homework_id TEXT,
subject TEXT NOT NULL,
knowledge_point TEXT NOT NULL,
ink_data BLOB, -- 压缩后的笔迹数据
score REAL NOT NULL DEFAULT 0.0,
error_type TEXT, -- 'wrong_answer' / 'incomplete' / 'stroke_error'
mastery_level REAL NOT NULL DEFAULT 0.1,
leitner_box INTEGER NOT NULL DEFAULT 0,
review_count INTEGER NOT NULL DEFAULT 0,
last_review_date INTEGER,
next_review_date INTEGER NOT NULL,
created_at INTEGER NOT NULL,
synced INTEGER NOT NULL DEFAULT 0
)
''';
```
### D.3 护眼功能实现(ML Kit人脸检测)
```dart
// lib/features/eye_protection/eye_distance_detector.dart
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import 'package:camera/camera.dart';
import 'dart:async';
class EyeDistanceDetector {
static const double MIN_SAFE_DISTANCE_RATIO = 0.12; // 人脸宽度/图像宽度比值阈值
static const Duration CHECK_INTERVAL = Duration(seconds: 3);
static const Duration ALERT_COOLDOWN = Duration(seconds: 30);
CameraController? _cameraController;
FaceDetector? _faceDetector;
Timer? _checkTimer;
DateTime? _lastAlertTime;
bool _detecting = false;
bool _enabled = false;
// 回调
Function(EyeProtectionAlert)? onAlert;
Future<void> initialize() async {
final cameras = await availableCameras();
final frontCamera = cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.front,
orElse: () => cameras.first,
);
_cameraController = CameraController(
frontCamera, ResolutionPreset.low, // 低分辨率节省性能
enableAudio: false,
imageFormatGroup: ImageFormatGroup.jpeg,
);
await _cameraController!.initialize();
_faceDetector = FaceDetector(
options: FaceDetectorOptions(
enableClassification: false,
enableLandmarks: false,
enableContours: false,
minFaceSize: 0.05, // 最小人脸比例
),
);
}
void start() {
if (_enabled) return;
_enabled = true;
_checkTimer = Timer.periodic(CHECK_INTERVAL, (_) => _checkDistance());
}
void stop() {
_enabled = false;
_checkTimer?.cancel();
}
Future<void> _checkDistance() async {
if (_detecting || _cameraController == null) return;
_detecting = true;
try {
final image = await _cameraController!.takePicture();
final inputImage = InputImage.fromFilePath(image.path);
final faces = await _faceDetector!.processImage(inputImage);
// 立即删除图像(隐私保护)
await File(image.path).delete();
if (faces.isNotEmpty) {
final face = faces.first;
final faceWidthRatio = face.boundingBox.width /
_cameraController!.value.previewSize!.width;
if (faceWidthRatio < MIN_SAFE_DISTANCE_RATIO) {
_triggerAlert(EyeProtectionAlertType.tooClose);
}
}
} catch (e) {
// 忽略检测错误,不影响主功能
} finally {
_detecting = false;
}
}
void _triggerAlert(EyeProtectionAlertType type) {
final now = DateTime.now();
if (_lastAlertTime != null &&
now.difference(_lastAlertTime!) < ALERT_COOLDOWN) {
return; // 冷却期内不重复提醒
}
_lastAlertTime = now;
onAlert?.call(EyeProtectionAlert(
type: type,
message: type == EyeProtectionAlertType.tooClose
? '请注意!离屏幕太近了,请保持安全距离。'
: '已连续使用${CHECK_INTERVAL.inMinutes}分钟,建议休息一下。',
));
}
Future<void> dispose() async {
stop();
await _cameraController?.dispose();
_faceDetector?.close();
}
}
enum EyeProtectionAlertType { tooClose, longUsage }
class EyeProtectionAlert {
final EyeProtectionAlertType type;
final String message;
EyeProtectionAlert({required this.type, required this.message});
}
```
### D.4 接口清单
| 接口路径 | 方法 | 说明 |
|---------|------|------|
| /api/v1/auth/login | POST | 账号登录,返回JWT Token |
| /api/v1/copybook/list | GET | 获取字帖列表(分级分类) |
| /api/v1/copybook/{id}/download | GET | 下载字帖资源文件 |
| /api/v1/copybook/{id}/submit | POST | 提交字帖练习(上传笔迹) |
| /api/v1/homework/list | GET | 获取作业列表 |
| /api/v1/homework/{id}/submit | POST | 提交作业 |
| /api/v1/mistakes/list | GET | 获取错题列表 |
| /api/v1/mistakes/{id}/review | POST | 提交错题复习结果 |
| /api/v1/classroom/join | POST | 加入课堂(课堂码) |
| /api/v1/report/student | GET | 获取个人学情报告 |
| /api/v1/sync/strokes | POST | 批量同步笔迹数据 |
---
## 附录E 性能指标与兼容性
### E.1 性能基准测试
| 测试场景 | 设备 | 结果 |
|---------|------|------|
| 笔迹渲染帧率(BLE书写) | iPad Air 5 (M1) | 60fps 稳定 |
| 笔迹渲染帧率(BLE书写) | 华为MatePad Pro 12.6 | 60fps 稳定 |
| BLE连接建立时间 | 平均 | 4.2秒 |
| 字帖下载速度(10页) | WiFi 100Mbps | 1.8秒 |
| 冷启动时间 | iPad Air 5 | 1.6秒 |
| 护眼检测功耗 | 3秒/次检测 | 额外增加约2%电量/小时 |
| 本地数据库查询(1万条错题) | P99 | 8ms |
| BLoC状态重建(作业列表刷新) | 平均 | 45ms |
### E.2 Pad APP支持设备
| 平台 | 最低版本 | 推荐版本 | 屏幕尺寸要求 |
|------|---------|---------|------------|
| Flutter (Android) | Android 7.0 (API 24) | Android 12+ | 8英寸以上 |
| Flutter (iOS) | iOS 13.0 | iOS 16+ | iPad(所有型号) |
### E.3 数据存储说明
| 数据类型 | 存储位置 | 加密 | 说明 |
|---------|---------|------|------|
| 用户Token | Hive(加密Box | AES-256 | 会话令牌安全存储 |
| 字帖资源 | 应用缓存目录 | 无 | 可重新下载,不敏感 |
| 错题笔迹数据 | sqflite | 明文 | 内容为手写笔迹原始数据 |
| 护眼检测图片 | 不持久化 | - | 检测后立即销毁,隐私保护 |
| 打卡记录 | sqflite + 云端同步 | 明文 | 同步前本地存储 |
| BLE设备绑定信息 | Hive | 明文 | 设备ID和昵称 |
### E.4 应用版本历史
| 版本 | 日期 | 平台 | 变更说明 |
|------|------|------|---------|
| V0.6 Beta | 2025-08-01 | Android/iOS | 基础字帖临摹、BLE连接、作业提交 |
| V0.9 RC | 2025-11-20 | Android/iOS | 错题本、书写回放、护眼检测、间隔复习 |
| V1.0 | 2026-02-14 | Android/iOS | 正式版:双栏自适应、离线模式、性能优化 |
---
*本文档版权归深圳自然写科技有限公司所有,技术细节仅用于软件著作权登记鉴别,请勿用于其他商业用途。*
---
## 附录G 补充技术规格
### G.1 字帖描红算法详解
#### G.1.1 笔画轨迹对齐算法
```dart
// stroke_alignment.dart
class StrokeAlignmentEngine {
/// DTW(动态时间规整)算法对比学生笔画与标准笔画
double computeDTW(List<Offset> student, List<Offset> standard) {
final n = student.length;
final m = standard.length;
// 初始化DTW矩阵
final dtw = List.generate(n + 1, (_) =>
List.filled(m + 1, double.infinity));
dtw[0][0] = 0.0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
final cost = _euclidean(student[i - 1], standard[j - 1]);
dtw[i][j] = cost + [
dtw[i - 1][j], // 插入
dtw[i][j - 1], // 删除
dtw[i - 1][j - 1], // 匹配
].reduce(math.min);
}
}
// 归一化距离
return dtw[n][m] / (n + m);
}
double _euclidean(Offset a, Offset b) {
final dx = a.dx - b.dx;
final dy = a.dy - b.dy;
return math.sqrt(dx * dx + dy * dy);
}
/// 计算笔画相似度评分(0-100分)
int scoreStroke(List<Offset> student, List<Offset> standard) {
final dtwDist = computeDTW(student, standard);
// 距离映射到分数:距离0→100分,距离50→0分
final score = (100 - dtwDist * 2).clamp(0.0, 100.0);
return score.round();
}
}
```
### G.2 护眼功能详细实现
#### G.2.1 坐姿检测算法
```dart
// posture_detector.dart
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
class PostureDetector {
final FaceDetector _detector = FaceDetector(
options: FaceDetectorOptions(
enableClassification: false,
enableLandmarks: true,
performanceMode: FaceDetectorMode.fast,
),
);
// 检测结果回调
final void Function(PostureWarning) onWarning;
PostureDetector({required this.onWarning});
Future<void> analyzeFrame(InputImage frame) async {
final faces = await _detector.processImage(frame);
if (faces.isEmpty) return;
final face = faces.first;
final bounds = face.boundingBox;
// 估算人脸到屏幕距离(基于人脸宽度比例)
// 标准人脸宽度约14cm,正常阅读距离30-50cm
final faceWidthRatio = bounds.width / frame.metadata!.size.width;
final estimatedDistance = 14.0 / (faceWidthRatio * 2 * math.tan(30 * math.pi / 180));
if (estimatedDistance < 30.0) {
onWarning(PostureWarning.tooClose(distance: estimatedDistance));
}
// 检测头部倾斜角度
final headEulerAngleZ = face.headEulerAngleZ ?? 0;
if (headEulerAngleZ.abs() > 15) {
onWarning(PostureWarning.tiltedHead(angle: headEulerAngleZ));
}
// 检测光线条件(人脸亮度)
if (face.smilingProbability != null) {
// 利用人脸检测的置信度估算环境光线
}
}
void dispose() => _detector.close();
}
enum PostureWarningType { tooClose, tiltedHead, lowLight }
class PostureWarning {
final PostureWarningType type;
final Map<String, dynamic> data;
PostureWarning.tooClose({required double distance})
: type = PostureWarningType.tooClose,
data = {'distance': distance};
PostureWarning.tiltedHead({required double angle})
: type = PostureWarningType.tiltedHead,
data = {'angle': angle};
}
```
### G.3 离线同步机制
#### G.3.1 冲突解决策略
```dart
// sync_manager.dart
class SyncManager {
final HiveBox localBox;
final ApiClient apiClient;
// 同步队列(待上传的本地操作)
final Queue<SyncOperation> _pendingQueue = Queue();
Future<void> syncAll() async {
// 1. 上传本地未同步数据
await _uploadPending();
// 2. 拉取服务端最新数据
await _pullLatest();
// 3. 解决冲突
await _resolveConflicts();
}
Future<void> _resolveConflicts() async {
final localItems = await localBox.getAll();
final serverItems = await apiClient.fetchAll();
for (final serverId in serverItems.keys) {
final local = localItems[serverId];
final server = serverItems[serverId];
if (local == null) {
// 服务端有、本地无:直接采纳服务端
await localBox.put(serverId, server);
} else if (server.updatedAt.isAfter(local.updatedAt)) {
// 服务端更新:采纳服务端(Last-Write-Wins策略)
await localBox.put(serverId, server);
}
// 本地更新且在离线期间:保留本地,等待上次上传
}
}
Future<void> _uploadPending() async {
while (_pendingQueue.isNotEmpty) {
final op = _pendingQueue.first;
try {
await apiClient.applyOperation(op);
_pendingQueue.removeFirst();
} catch (e) {
if (e is NetworkException) break; // 网络问题,等待下次
_pendingQueue.removeFirst(); // 其他错误,跳过
}
}
}
}
```
---
## 附录H 补充技术规格
### H.1 手写作业批改结果展示
```dart
// GradingResultView.dart
class GradingResultView extends StatelessWidget {
final HomeworkSubmission submission;
final GradingResult result;
const GradingResultView({Key? key, required this.submission, required this.result}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 总分显示
_ScoreHeader(score: result.totalScore, maxScore: result.maxScore),
const SizedBox(height: 16),
// 逐题批改详情
...result.questionResults.map((qr) => _QuestionResult(result: qr)),
const SizedBox(height: 16),
// AI分析评语
_AIComment(comment: result.aiComment),
],
);
}
}
class _ScoreHeader extends StatelessWidget {
final int score;
final int maxScore;
const _ScoreHeader({required this.score, required this.maxScore});
@override
Widget build(BuildContext context) {
final pct = score / maxScore;
final color = pct >= 0.9 ? Colors.green
: pct >= 0.6 ? Colors.orange
: Colors.red;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$score', style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: color)),
Text('/$maxScore', style: const TextStyle(fontSize: 24, color: Colors.grey)),
],
);
}
}
class _QuestionResult extends StatelessWidget {
final QuestionResult result;
const _QuestionResult({required this.result});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: Icon(result.correct ? Icons.check_circle : Icons.cancel,
color: result.correct ? Colors.green : Colors.red),
title: Text('第${result.questionNo}题'),
subtitle: result.errorMessage != null ? Text(result.errorMessage!) : null,
trailing: Text('${result.score}/${result.maxScore}'),
),
);
}
}
```
### H.2 多语言国际化支持
```dart
// l10n/app_zh.arb - 中文本地化
{
"appTitle": "自然写",
"homeTab": "首页",
"classroomTab": "课堂",
"homeworkTab": "作业",
"profileTab": "我的",
"loginTitle": "登录",
"loginButton": "登 录",
"usernameHint": "请输入用户名",
"passwordHint": "请输入密码",
"rememberPassword": "记住密码",
"connectPen": "连接智能笔",
"scanning": "扫描中...",
"penConnected": "笔已连接:{penName}",
"@penConnected": {
"placeholders": { "penName": { "type": "String" } }
},
"batteryLevel": "电量 {percent}%",
"@batteryLevel": {
"placeholders": { "percent": { "type": "int" } }
},
"submitHomework": "提交作业",
"submitting": "提交中...",
"submitSuccess": "作业提交成功",
"submitFailed": "提交失败,请重试",
"networkError": "网络连接异常",
"retryButton": "重 试"
}
```
---
*本文档版权归深圳自然写科技有限公司所有,技术细节仅用于软件著作权登记鉴别,请勿用于其他商业用途。*