diff --git a/docs/superpowers/plans/2026-06-02-classroom-pilot-readiness.md b/docs/superpowers/plans/2026-06-02-classroom-pilot-readiness.md new file mode 100644 index 0000000..ab55d57 --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-classroom-pilot-readiness.md @@ -0,0 +1,1062 @@ +# 暖记课堂试点就绪 — 实施计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 四角色闭环 + 跨角色链路全部打通,达到真实课堂试点就绪 + +**Architecture:** 基于现有 BLoC 模式扩展,LoadJournal 为 EditorBloc 新增 event,评论展示用独立 FutureBuilder widget,家长 PIPL 在现有 parent_page 上验证补全。管理端 CRUD 扩展后端 handler + 前端 DrawerForm。 + +**Tech Stack:** Flutter 3.x / flutter_bloc / Isar / Rust Axum / SeaORM / React + Ant Design + +**Spec:** `docs/superpowers/specs/2026-06-02-classroom-pilot-readiness-design.md` v1.1 + +--- + +## Chunk 1: Phase 1 — 孩子核心闭环 + +### 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| 修改 | `app/lib/features/editor/bloc/editor_bloc.dart` | 添加 `LoadJournal` event + handler | +| 修改 | `app/lib/features/editor/views/editor_page.dart` | initState 触发 LoadJournal | +| 修改 | `test/features/editor/bloc/editor_bloc_test.dart` | LoadJournal 单元测试 | +| 修改 | 8+ 文件 | catch(e) 添加 debugPrint | + +--- + +### Task 1: LoadJournal Event + Handler + +**Files:** +- Modify: `app/lib/features/editor/bloc/editor_bloc.dart` (events 区域 + BLoC handlers) +- Test: `test/features/editor/bloc/editor_bloc_test.dart` + +- [ ] **Step 1: 写 LoadJournal event 的失败测试** + +在 `test/features/editor/bloc/editor_bloc_test.dart` 末尾添加: + +```dart +test('LoadJournal 还原已有日记数据', () async { + // 准备测试数据 + final strokes = [ + Stroke(id: 's1', points: [ + const StrokePoint(x: 10, y: 20), + ]), + ]; + final elements = [ + JournalElement.createSticker( + journalId: 'j1', + emoji: '😊', + position: Offset.zero, + ), + ]; + + // dispatch LoadJournal + bloc.add(LoadJournal( + title: '测试日记', + mood: Mood.happy, + tags: const ['开心', '学校'], + strokes: strokes, + elements: elements, + lastSavedAt: DateTime(2026, 6, 1), + )); + + // 等待状态更新 + await Future.delayed(const Duration(milliseconds: 50)); + + // 验证状态 + expect(bloc.state.title, '测试日记'); + expect(bloc.state.selectedMood, Mood.happy); + expect(bloc.state.tags, ['开心', '学校']); + expect(bloc.state.strokes.length, 1); + expect(bloc.state.elements.length, 1); + expect(bloc.state.lastSavedAt, DateTime(2026, 6, 1)); + expect(bloc.state.isDirty, false); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `cd g:/nj/app && flutter test test/features/editor/bloc/editor_bloc_test.dart` +Expected: FAIL — `LoadJournal` 类不存在 + +- [ ] **Step 3: 添加 LoadJournal event 类** + +在 `app/lib/features/editor/bloc/editor_bloc.dart` 的 events 区域(`TextFormatChanged` 之后)添加: + +```dart +/// 加载已有日记数据(从 JournalRepository 读取后传入) +class LoadJournal extends EditorEvent { + final String title; + final Mood mood; + final List tags; + final List strokes; + final List elements; + final DateTime? lastSavedAt; + + const LoadJournal({ + required this.title, + required this.mood, + required this.tags, + required this.strokes, + required this.elements, + this.lastSavedAt, + }); +} +``` + +- [ ] **Step 4: 添加 _onLoadJournal handler** + +在 EditorBloc 构造函数中注册: + +```dart +on(_onLoadJournal); +``` + +实现 handler: + +```dart +Future _onLoadJournal( + LoadJournal event, + Emitter emit, +) async { + emit(state.copyWith( + title: event.title, + selectedMood: event.mood, + tags: event.tags, + strokes: event.strokes, + elements: event.elements, + lastSavedAt: event.lastSavedAt, + isDirty: false, + )); +} +``` + +> 注意:LoadJournal 不触发 auto-save(isDirty 设为 false),因为这是加载已有数据,不是用户编辑。 + +- [ ] **Step 5: 运行测试确认通过** + +Run: `cd g:/nj/app && flutter test test/features/editor/bloc/editor_bloc_test.dart` +Expected: ALL PASS + +- [ ] **Step 6: 提交** + +```bash +cd g:/nj +git add app/lib/features/editor/bloc/editor_bloc.dart test/features/editor/bloc/editor_bloc_test.dart +git commit -m "feat(app): 添加 EditorBloc.LoadJournal event — 加载已有日记数据" +``` + +--- + +### Task 2: EditorPage 触发 LoadJournal + +**Files:** +- Modify: `app/lib/features/editor/views/editor_page.dart` + +- [ ] **Step 1: 在 _EditorStackState.initState 中加载已有日记** + +修改 `_EditorStackState` 的 `initState`: + +```dart +@override +void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.state.title); + + // 如果打开已有日记,加载其数据 + if (widget.journalId != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _loadExistingJournal(widget.journalId!); + }); + } +} +``` + +- [ ] **Step 2: 实现 _loadExistingJournal 方法** + +在 `_EditorStackState` 中添加: + +```dart +/// 加载已有日记数据并 dispatch LoadJournal +Future _loadExistingJournal(String journalId) async { + try { + final repo = context.read(); + final entry = await repo.getJournal(journalId); + if (entry == null || !mounted) return; + + // 加载元素 + final elements = await repo.getElements(journalId); + + // 从 handwriting_ref 元素中反序列化笔画 + List strokes = []; + final strokesElement = elements.where( + (e) => e.elementType == ElementType.handwritingRef, + ).firstOrNull; + if (strokesElement != null) { + final strokesData = strokesElement.content['strokes']; + if (strokesData is List) { + strokes = strokesData + .map((s) => Stroke.fromJson(s as Map)) + .toList(); + } + } + + // 过滤掉 handwriting_ref 元素(笔画单独管理) + final otherElements = elements + .where((e) => e.elementType != ElementType.handwritingRef) + .toList(); + + context.read().add(LoadJournal( + title: entry.title, + mood: entry.mood, + tags: entry.tags, + strokes: strokes, + elements: otherElements, + lastSavedAt: entry.updatedAt, + )); + + // 同步标题输入框 + _titleController.text = entry.title; + } catch (e) { + debugPrint('加载日记失败: $e'); + } +} +``` + +- [ ] **Step 3: 确认 JournalRepository 有 getElements 方法** + +Run: `cd g:/nj/app && grep -n "getElements" lib/data/repositories/journal_repository.dart` + +如果不存在,需要在 `JournalRepository` 接口和实现中添加: + +```dart +// journal_repository.dart +Future> getElements(String journalId); + +// isar_journal_repository.dart +@override +Future> getElements(String journalId) async { + // 查询 isar 中该 journalId 的所有元素 + final collections = await isar.journalElementCollections + .where() + .filter() + .journalIdEqualTo(journalId) + .findAll(); + return collections.map(_collectionToElement).toList(); +} +``` + +- [ ] **Step 4: 运行 flutter analyze 确认无错误** + +Run: `cd g:/nj/app && flutter analyze` +Expected: No issues found + +- [ ] **Step 5: 提交** + +```bash +cd g:/nj +git add app/lib/features/editor/views/editor_page.dart +git add app/lib/data/repositories/journal_repository.dart +git add app/lib/data/repositories/isar_journal_repository.dart +git commit -m "feat(app): EditorPage 加载已有日记 — initState 触发 LoadJournal" +``` + +--- + +### Task 3: catch(e) 异常处理修复(18 处添加 debugPrint) + +**Files:** +- Modify: `app/lib/features/parent/bloc/parent_bloc.dart` (6 处) +- Modify: `app/lib/features/search/bloc/search_bloc.dart` (3 处) +- Modify: `app/lib/features/achievement/bloc/achievement_bloc.dart` (1 处) +- Modify: `app/lib/features/stickers/bloc/sticker_bloc.dart` (2 处) +- Modify: `app/lib/features/templates/bloc/template_bloc.dart` (1 处) +- Modify: `app/lib/features/mood/bloc/mood_bloc.dart` (1 处) +- Modify: `app/lib/features/home/bloc/home_bloc.dart` (1 处) +- Modify: `app/lib/features/calendar/bloc/calendar_bloc.dart` (1 处) +- Modify: `app/lib/features/calendar/views/weekly_page.dart` (1 处) +- Modify: `app/lib/data/services/sync_engine.dart` (1 处, line 198) + +**修复模式(所有文件统一):** + +```dart +// Before: +} catch (e) { + emit(SomeError('操作失败')); +} + +// After: +} catch (e) { + debugPrint('ParentBloc._onLoadChildren 失败: $e'); + emit(SomeError('操作失败')); +} +``` + +- [ ] **Step 1: 修复 parent_bloc.dart (6 处)** + +在 `_onLoadChildren`, `_onBindChild`, `_onViewJournals`, `_onExportData`, `_onDeleteData`, `_onUnbindChild` 的 catch 块中添加 `debugPrint('ParentBloc._onXxx 失败: $e');`。确保文件头部有 `import 'package:flutter/foundation.dart';`。 + +- [ ] **Step 2: 修复 search_bloc.dart (3 处)** + +同上模式。 + +- [ ] **Step 3: 修复其余 ChangeNotifier BLoC (5 处)** + +achievement_bloc.dart, sticker_bloc.dart, template_bloc.dart, mood_bloc.dart — 这些使用 `_state = _state.copyWith(...)` 而非 emit,但同样需要 debugPrint。 + +- [ ] **Step 4: 修复 flutter_bloc 文件 (3 处)** + +home_bloc.dart, calendar_bloc.dart, weekly_page.dart。 + +- [ ] **Step 5: 修复 sync_engine.dart (1 处)** + +line 198 的 catch 块添加 `debugPrint('SyncEngine._trySync 操作失败: $e');`。 + +- [ ] **Step 6: 运行 flutter analyze 确认无错误** + +Run: `cd g:/nj/app && flutter analyze` +Expected: No issues found + +- [ ] **Step 7: 提交** + +```bash +cd g:/nj +git add app/lib/features/parent/bloc/parent_bloc.dart \ + app/lib/features/search/bloc/search_bloc.dart \ + app/lib/features/achievement/bloc/achievement_bloc.dart \ + app/lib/features/stickers/bloc/sticker_bloc.dart \ + app/lib/features/templates/bloc/template_bloc.dart \ + app/lib/features/mood/bloc/mood_bloc.dart \ + app/lib/features/home/bloc/home_bloc.dart \ + app/lib/features/calendar/bloc/calendar_bloc.dart \ + app/lib/features/calendar/views/weekly_page.dart \ + app/lib/data/services/sync_engine.dart +git commit -m "fix(app): 18 处 catch(e) 添加 debugPrint 异常日志" +``` + +--- + +### Task 4: Phase 1 端到端验证 + +- [ ] **Step 1: 运行全部 Flutter 测试** + +Run: `cd g:/nj/app && flutter test` +Expected: ALL PASS + +- [ ] **Step 2: 运行 flutter analyze** + +Run: `cd g:/nj/app && flutter analyze` +Expected: 0 errors + +- [ ] **Step 3: 手动验证 — 创建并重新编辑日记** + +1. `./scripts/dev.sh app` 启动 Flutter Web +2. 创建日记:手写 + 贴纸 + 选择心情 → 点击"完成" +3. 返回首页 → 点击刚创建的日记 +4. **验证:编辑器正确加载标题、笔画、贴纸、心情** +5. 继续编辑 → 添加新内容 → 再次保存 → 验证更新成功 + +--- + +## Chunk 2: Phase 2 — 家长合规闭环 + +### 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| 验证 | `app/lib/features/parent/bloc/parent_bloc.dart` | API 调用正确性 | +| 验证+修改 | `app/lib/features/parent/views/parent_page.dart` | PIPL 数据权利 UI | +| 修改 | `app/lib/features/home/views/home_page.dart` | 添加家长入口(如缺失) | + +--- + +### Task 5: 家长端数据流端到端验证 + +**Files:** +- Verify: `app/lib/features/parent/bloc/parent_bloc.dart` +- Verify: `app/lib/features/parent/views/parent_page.dart` + +- [ ] **Step 1: 验证后端 API 可用** + +Run: `curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/diary/parent/children` + +Expected: 200 + JSON array(可能为空,因为未绑定孩子) + +- [ ] **Step 2: 手动验证家长端完整流程** + +1. 启动后端 + Flutter: `./scripts/dev.sh` +2. 家长账号登录 → 进入家长中心 +3. 绑定孩子(输入孩子 ID) +4. 查看孩子日记列表 → **验证列表非空,内容正确** +5. 点击"数据导出" → **验证导出结果显示(JSON 内容)** +6. 点击"数据删除" → **验证确认对话框弹出,包含 PIPL 法律文本** +7. 取消删除(不实际执行) → 验证数据未丢失 + +- [ ] **Step 3: 如果发现数据流问题,修复后提交** + +常见问题可能包括:API 返回格式不匹配、状态转换不正确等。根据具体问题修复。 + +```bash +git add app/lib/features/parent/ +git commit -m "fix(app): 修复家长端数据流 — [具体问题描述]" +``` + +--- + +### Task 6: 导出功能补全(文件下载) + +**Files:** +- Modify: `app/lib/features/parent/views/parent_page.dart` (导出结果区域) + +- [ ] **Step 1: 验证当前导出行为** + +检查 `parent_page.dart` 中 `ParentDataExported` state 的处理 — 是否只显示 JSON 预览,还是有文件下载功能。 + +- [ ] **Step 2: 如果缺少文件下载,添加下载按钮** + +在 `_ExportDataView` widget 中添加下载操作。Flutter Web 可使用 `html.AnchorElement` 触发下载: + +```dart +import 'dart:convert'; +// 仅 Web 平台 +import 'package:flutter/foundation.dart' show kIsWeb; + +void _downloadJson(Map data, String filename) { + if (kIsWeb) { + // Web: 使用 AnchorElement 下载 + final blob = html.Blob([jsonEncode(data)]); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute('download', filename) + ..click(); + html.Url.revokeObjectUrl(url); + } + // 非 Web 平台可用 path_provider + File +} +``` + +> 注意:Flutter Web 下载需要 `dart:html`,通过 conditional import 处理。 + +- [ ] **Step 3: 运行 flutter analyze** + +Run: `cd g:/nj/app && flutter analyze` +Expected: No issues found + +- [ ] **Step 4: 提交** + +```bash +git add app/lib/features/parent/views/parent_page.dart +git commit -m "feat(app): 家长端数据导出 — 添加 JSON 文件下载" +``` + +--- + +### Task 7: Phase 2 端到端验证 + +- [ ] **Step 1: 完整家长流程** + +1. 家长登录 → 绑定孩子 → 查看日记列表 +2. 导出数据 → **验证 JSON 文件可下载、内容正确** +3. 删除单篇日记 → 确认对话框 → 取消(不实际删除) +4. PIPL 法律说明 → **验证文本完整显示** + +--- + +## Chunk 3: Phase 3 — 师生互动闭环 + +### 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| 新建 | `app/lib/features/editor/widgets/comment_list_sheet.dart` | 评论列表 FutureBuilder | +| 修改 | `app/lib/features/editor/views/editor_page.dart` | 顶栏添加评语图标 | +| 验证 | `app/lib/features/class_/bloc/class_bloc.dart` | _onCommentCreate 数据流 | + +--- + +### Task 8: 评论列表展示 Widget + +**Files:** +- Create: `app/lib/features/editor/widgets/comment_list_sheet.dart` + +- [ ] **Step 1: 创建 comment_list_sheet.dart** + +```dart +// 评论列表底部面板 — FutureBuilder 轮询模式 +// +// 独立 widget,不纳入 EditorBloc。 +// 打开日记时从后端拉取评论列表展示。 + +import 'package:flutter/material.dart'; + +import '../../../data/remote/api_client.dart'; +import '../../class_/bloc/class_bloc.dart' show Comment; + +/// 评论列表底部面板 +class CommentListSheet extends StatelessWidget { + final String journalId; + final ApiClient apiClient; + + const CommentListSheet({ + super.key, + required this.journalId, + required this.apiClient, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return DraggableScrollableSheet( + initialChildSize: 0.4, + minChildSize: 0.2, + maxChildSize: 0.7, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(22)), + ), + child: Column( + children: [ + // 拖拽指示条 + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 8), + child: Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + // 标题 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Text( + '老师评语', + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + // 评论数量 badge + _CommentCountBadge(journalId: journalId, apiClient: apiClient), + ], + ), + ), + const Divider(), + // 评论列表 + Expanded( + child: _CommentListFuture( + journalId: journalId, + apiClient: apiClient, + scrollController: scrollController, + ), + ), + ], + ), + ); + }, + ); + } +} + +/// 评论数量 Badge +class _CommentCountBadge extends StatelessWidget { + final String journalId; + final ApiClient apiClient; + + const _CommentCountBadge({ + required this.journalId, + required this.apiClient, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _fetchComments(), + builder: (context, snapshot) { + if (!snapshot.hasData) return const SizedBox.shrink(); + final count = snapshot.data!.length; + if (count == 0) return const SizedBox.shrink(); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$count', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ); + }, + ); + } + + Future> _fetchComments() async { + try { + final response = await apiClient.get('/diary/journals/$journalId/comments'); + return response as List; + } catch (_) { + return []; + } + } +} + +/// 评论列表 — FutureBuilder 轮询拉取 +class _CommentListFuture extends StatelessWidget { + final String journalId; + final ApiClient apiClient; + final ScrollController scrollController; + + const _CommentListFuture({ + required this.journalId, + required this.apiClient, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _fetchComments(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center( + child: Text('加载评语失败', style: TextStyle(color: Colors.grey[500])), + ); + } + final comments = snapshot.data ?? []; + if (comments.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.chat_bubble_outline, size: 36, color: Colors.grey[300]), + const SizedBox(height: 8), + Text( + '还没有评语哦', + style: TextStyle(color: Colors.grey[400]), + ), + ], + ), + ); + } + return ListView.builder( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: comments.length, + itemBuilder: (context, index) { + return _CommentTile(comment: comments[index]); + }, + ); + }, + ); + } + + Future> _fetchComments() async { + try { + final response = await apiClient.get('/diary/journals/$journalId/comments'); + final list = response as List; + return list.map((json) => Comment( + id: json['id'] as String, + journalId: json['journal_id'] as String, + authorId: json['author_id'] as String, + content: json['content'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + )).toList(); + } catch (e) { + debugPrint('加载评论失败: $e'); + return []; + } + } +} + +/// 单条评论卡片 +class _CommentTile extends StatelessWidget { + final Comment comment; + + const _CommentTile({required this.comment}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 老师标签 + 时间 + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFE07A5F).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '老师', + style: TextStyle(fontSize: 11, color: Color(0xFFE07A5F)), + ), + ), + const SizedBox(width: 8), + Text( + _formatTime(comment.createdAt), + style: TextStyle(fontSize: 11, color: Colors.grey[500]), + ), + ], + ), + const SizedBox(height: 8), + // 评语内容 + Text( + comment.content, + style: const TextStyle(fontSize: 14, height: 1.5), + ), + ], + ), + ), + ); + } + + String _formatTime(DateTime dt) { + return '${dt.month}/${dt.day} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + } +} +``` + +> 注意:Comment 类定义在 class_bloc.dart 中。如果引用路径有问题,可将 Comment 提取为独立 model 文件。 + +- [ ] **Step 2: 运行 flutter analyze** + +Run: `cd g:/nj/app && flutter analyze` +Expected: No issues found + +- [ ] **Step 3: 提交** + +```bash +git add app/lib/features/editor/widgets/comment_list_sheet.dart +git commit -m "feat(app): 添加评论列表展示组件 — FutureBuilder 轮询模式" +``` + +--- + +### Task 9: EditorPage 顶栏添加评语入口 + +**Files:** +- Modify: `app/lib/features/editor/views/editor_page.dart` + +- [ ] **Step 1: 在 _buildTopBar 的按钮行中添加评语图标** + +在"标签按钮"(`Icons.sell_rounded`)之后、"完成"按钮之前添加: + +```dart +// 评语按钮(仅已有日记显示) +if (widget.journalId != null) + IconButton( + icon: const Icon(Icons.chat_bubble_outline_rounded, size: 18), + onPressed: () => _showComments(context), + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + ), +``` + +- [ ] **Step 2: 实现 _showComments 方法** + +在 `_EditorView` 中添加: + +```dart +/// 显示评论列表 +void _showComments(BuildContext context) { + final journalId = widget.journalId; + if (journalId == null) return; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => CommentListSheet( + journalId: journalId, + apiClient: context.read(), + ), + ); +} +``` + +- [ ] **Step 3: 确认 import 完整** + +在 editor_page.dart 头部确认有: +```dart +import '../../../data/remote/api_client.dart'; +import '../widgets/comment_list_sheet.dart'; +``` + +- [ ] **Step 4: 运行 flutter analyze** + +Run: `cd g:/nj/app && flutter analyze` +Expected: No issues found + +- [ ] **Step 5: 提交** + +```bash +git add app/lib/features/editor/views/editor_page.dart +git commit -m "feat(app): EditorPage 顶栏添加评语入口 — 仅已有日记显示" +``` + +--- + +### Task 10: 班级日记墙评语按钮验证 + +**Files:** +- Verify: `app/lib/features/class_/bloc/class_bloc.dart` +- Verify: `app/lib/features/class_/views/` 日记墙相关页面 + +- [ ] **Step 1: 验证 ClassBloc._onCommentCreate 流程** + +检查 `_onCommentCreate` (line 406-424): +1. Guard: `state is ClassDetailLoaded` ✅ +2. 调用 `_classRepo.createComment()` ✅ +3. 成功后 `add(ClassLoadComments())` 刷新 ✅ +4. 失败时 `debugPrint` + `emit error` ✅ + +- [ ] **Step 2: 验证日记墙 UI 有评语入口** + +在班级详情/日记墙页面中,确认: +- 学生日记卡片上有评语按钮或评语输入框 +- 如果缺失,需要在日记墙页面添加评语按钮 + 底部输入框 + +> 具体修改取决于当前日记墙的实现。如果 ClassBloc 的 CommentCreate 事件在 UI 中有对应触发点(评语输入框),则无需修改。 + +- [ ] **Step 3: 手动验证 — 老师写评语** + +1. 老师账号 → 进入班级 → 查看日记墙 +2. 找到学生日记 → 点击评语按钮 → 输入评语 → 提交 +3. **验证:评语提交成功,日记墙上显示评语数量更新** + +如有问题,修复后提交: + +```bash +git add app/lib/features/class_/ +git commit -m "fix(app): 修复班级日记墙评语 UI — [具体问题]" +``` + +--- + +### Task 11: Phase 3 端到端验证 + +- [ ] **Step 1: 完整师生互动流程** + +1. **老师**:创建班级 → 布置主题 +2. **学生**:看到主题 → 写日记 → 分享到班级 +3. **老师**:查看日记墙 → 写评语 → **验证提交成功** +4. **学生**:打开该日记 → 点击评语图标 → **验证看到老师评语** + +--- + +## Chunk 4: Phase 4 — 管理端补全 + 全系统验证 + +### 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| 修改 | `crates/erp-diary/src/handler/sticker_handler.rs` | 贴纸 CRUD handler | +| 修改 | `crates/erp-diary/src/handler/topic_handler.rs` | 主题编辑/停用 | +| 修改 | `crates/erp-diary/src/service/sticker_service.rs` | 贴纸 CRUD service | +| 修改 | `crates/erp-diary/src/service/topic_service.rs` | 主题编辑/停用 service | +| 修改 | `crates/erp-diary/src/lib.rs` | 注册新路由 | +| 修改 | `apps/web/src/api/diary/stickers.ts` | 贴纸 CRUD API 调用 | +| 修改 | `apps/web/src/pages/diary/StickerPackList.tsx` | 贴纸 CRUD UI | +| 修改 | `apps/web/src/pages/diary/TopicList.tsx` | 主题编辑/停用 UI | +| 验证 | 后端菜单 seed 迁移 | 确认所有暖记菜单已注册 | + +--- + +### Task 12: 验证管理端菜单完整性 + +**Files:** +- Verify: `crates/erp-server/migration/src/` 暖记菜单 seed + +- [ ] **Step 1: 查询数据库确认菜单** + +```bash +psql -U postgres -d nuanji -c "SELECT id, name, path, parent_id FROM sys_menu WHERE path LIKE '/diary%' OR name LIKE '%暖记%' OR name LIKE '%日记%' ORDER BY sort_order;" +``` + +Expected: 至少包含以下菜单: +- 暖记管理(目录) + - 班级管理 `/diary/classes` + - 日记审阅 `/diary/journals` + - 主题管理 `/diary/topics` + - 贴纸管理 `/diary/stickers` + +- [ ] **Step 2: 如果菜单缺失,添加 seed 迁移** + +在 `crates/erp-server/migration/src/` 中修改最新的 diary seed 迁移,补充缺失菜单。 + +```bash +git commit -m "fix(server): 补充暖记管理端菜单 seed — 贴纸/主题/统计入口" +``` + +--- + +### Task 13: 贴纸 CRUD 后端 + +**Files:** +- Modify: `crates/erp-diary/src/service/sticker_service.rs` +- Modify: `crates/erp-diary/src/handler/sticker_handler.rs` +- Modify: `crates/erp-diary/src/lib.rs` + +- [ ] **Step 1: 在 sticker_service.rs 添加 CRUD 方法** + +```rust +// 创建贴纸包 +pub async fn create_sticker_pack(&self, req: CreateStickerPackReq) -> Result { ... } +// 更新贴纸包 +pub async fn update_sticker_pack(&self, id: &str, req: UpdateStickerPackReq) -> Result { ... } +// 删除贴纸包(软删除) +pub async fn delete_sticker_pack(&self, id: &str) -> Result<()> { ... } +``` + +- [ ] **Step 2: 在 sticker_handler.rs 添加 handler** + +```rust +// POST /diary/sticker-packs — 需 diary.sticker.manage 权限 +pub async fn create_sticker_pack(...) { ... } + +// PUT /diary/sticker-packs/{id} — 需 diary.sticker.manage 权限 +pub async fn update_sticker_pack(...) { ... } + +// DELETE /diary/sticker-packs/{id} — 需 diary.sticker.manage 权限 +pub async fn delete_sticker_pack(...) { ... } +``` + +- [ ] **Step 3: 在 lib.rs 注册路由** + +- [ ] **Step 4: 运行 cargo check + cargo test** + +Run: `cd g:/nj && cargo check && cargo test` +Expected: PASS + +- [ ] **Step 5: 提交** + +```bash +git add crates/erp-diary/ +git commit -m "feat(diary): 贴纸包 CRUD API — 创建/更新/删除" +``` + +--- + +### Task 14: 贴纸管理端 UI + +**Files:** +- Modify: `apps/web/src/api/diary/stickers.ts` +- Modify: `apps/web/src/pages/diary/StickerPackList.tsx` + +- [ ] **Step 1: 在 stickers.ts 添加 CRUD API 调用** + +```typescript +createPack: (data: CreatePackReq) => api.post('/diary/sticker-packs', data), +updatePack: (id: string, data: UpdatePackReq) => api.put(`/diary/sticker-packs/${id}`, data), +deletePack: (id: string) => api.delete(`/diary/sticker-packs/${id}`), +``` + +- [ ] **Step 2: 在 StickerPackList.tsx 添加 CRUD 操作** + +参考 `ClassList.tsx` 的 `DrawerForm` 模式: +- 添加"新建贴纸包"按钮 +- DrawerForm 创建/编辑表单 +- 删除确认对话框 + +- [ ] **Step 3: 验证 pnpm build** + +Run: `cd g:/nj/apps/web && pnpm build` +Expected: 无错误 + +- [ ] **Step 4: 提交** + +```bash +git add apps/web/src/ +git commit -m "feat(web): 贴纸包管理 CRUD UI — 创建/编辑/删除" +``` + +--- + +### Task 15: 主题编辑/停用 + +**Files:** +- Modify: `crates/erp-diary/src/service/topic_service.rs` +- Modify: `crates/erp-diary/src/handler/topic_handler.rs` +- Modify: `crates/erp-diary/src/lib.rs` +- Modify: `apps/web/src/api/diary/topics.ts` +- Modify: `apps/web/src/pages/diary/TopicList.tsx` + +- [ ] **Step 1: 后端添加主题更新/停用** + +```rust +// PUT /diary/topics/{id} — 编辑主题(标题/描述/截止日期) +// PATCH /diary/topics/{id}/deactivate — 停用主题 +``` + +- [ ] **Step 2: 管理端 TopicList.tsx 添加编辑/停用按钮** + +- [ ] **Step 3: 提交** + +```bash +git add crates/erp-diary/ apps/web/src/ +git commit -m "feat(diary): 主题编辑/停用 — 后端 API + 管理端 UI" +``` + +--- + +### Task 16: 全系统端到端验证 + +- [ ] **Step 1: 编译全部三端** + +```bash +cd g:/nj && cargo check && cargo test # 后端 +cd g:/nj/app && flutter analyze && flutter test # Flutter +cd g:/nj/apps/web && pnpm build # 管理端 +``` + +Expected: 全部通过 + +- [ ] **Step 2: 四角色 × 跨角色 完整场景** + +| # | 角色 | 场景 | 预期 | +|---|------|------|------| +| 1 | 学生 | 注册 → 写日记(手写+贴纸+心情)→ 保存 → 返回 → 重新打开编辑 | 笔画+贴纸+心情正确还原 | +| 2 | 学生 | 搜索历史日记 | 搜索结果正确 | +| 3 | 学生 | 加入班级 → 分享日记到班级 | 分享成功 | +| 4 | 老师 | 创建班级 → 布置主题 | 主题出现在学生端 | +| 5 | 老师 | 查看日记墙 → 写评语 | 评语提交成功 | +| 6 | 学生 | 打开已分享日记 → 查看老师评语 | 评语正确显示 | +| 7 | 家长 | 绑定孩子 → 查看日记 → 导出数据 | JSON 文件可下载 | +| 8 | 家长 | PIPL 法律说明 | 文本完整显示 | +| 9 | 管理员 | 班级列表(全部班级) | 显示所有班级 | +| 10 | 管理员 | 贴纸管理 CRUD | 创建/编辑/删除正常 | +| 11 | 管理员 | 主题管理 → 停用 | 学生端不再看到已停用主题 | + +- [ ] **Step 3: 最终提交(如有修复)** + +```bash +git add -A +git commit -m "fix: 全系统端到端验证修复 — [具体问题列表]" +``` + +--- + +*计划版本 1.0 | 2026-06-02 | 基于 spec v1.1 (commit 860844a)*