# 暖记课堂试点就绪 — 实施计划 > **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` > ⚠️ **重要:** `_loadExistingJournal` 已存在于 `_EditorViewState`(line ~278-331),使用多个细粒度事件(TitleChanged, MoodChanged, TagsLoaded, StrokesLoaded, ElementsLoaded)。本任务将其替换为单一的原子 `LoadJournal` 事件。修改目标是 **`_EditorViewState`**,不是 `_EditorStackState`。 - [ ] **Step 1: 替换 `_EditorViewState._loadExistingJournal` 方法** 找到 `_EditorViewState._loadExistingJournal`(约 line 287-331),替换为使用 LoadJournal 的版本: ```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, )); // 同步标题输入框(_EditorStackState 中的 controller) } catch (e) { debugPrint('加载日记失败: $e'); } } ``` - [ ] **Step 2: 验证 JournalRepository.getElements 已存在(完整性检查,预计已存在)** Run: `cd g:/nj/app && grep -n "getElements" lib/data/repositories/journal_repository.dart` Expected: 找到方法声明,无需添加。 - [ ] **Step 3: 运行 flutter analyze 确认无错误** Run: `cd g:/nj/app && flutter analyze` Expected: No issues found - [ ] **Step 4: 提交** ```bash cd g:/nj git add app/lib/features/editor/views/editor_page.dart git commit -m "feat(app): EditorPage 加载已有日记 — 替换为 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.data 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.data 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: 在 dto.rs 添加 UpdateStickerPackReq DTO** 参考已有的 `CreateStickerPackReq`,在 `crates/erp-diary/src/dto.rs` 中添加: ```rust #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateStickerPackReq { #[validate(length(min = 1, max = 100))] pub name: Option, pub description: Option, pub category: Option, pub is_free: Option, } ``` - [ ] **Step 2: 在 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 3: 在 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 4: 在 lib.rs 注册路由** - [ ] **Step 5: 运行 cargo check + cargo test** Run: `cd g:/nj && cargo check && cargo test` Expected: PASS - [ ] **Step 6: 提交** ```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:** - Verify: `crates/erp-diary/src/handler/topic_handler.rs` (update_topic + deactivate_topic 已存在) - Modify: `apps/web/src/api/diary/topics.ts` - Modify: `apps/web/src/pages/diary/TopicList.tsx` > ⚠️ **后端已实现** — `topic_handler.rs` 已有 `update_topic` (PUT) 和 `deactivate_topic` (PATCH) handler,`lib.rs` 已注册路由。只需补全管理端 UI。 - [ ] **Step 1: 验证后端 API 可用** Run: `cd g:/nj && grep -n "update_topic\|deactivate_topic" crates/erp-diary/src/handler/topic_handler.rs` Expected: 找到两个方法 - [ ] **Step 2: 在 topics.ts 添加 update/deactivate API 调用** ```typescript update: (id: string, data: UpdateTopicReq) => api.put(`/diary/topics/${id}`, data), deactivate: (id: string) => api.patch(`/diary/topics/${id}/deactivate`), ``` - [ ] **Step 3: 管理端 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)*