32 KiB
暖记课堂试点就绪 — 实施计划
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 末尾添加:
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 之后)添加:
/// 加载已有日记数据(从 JournalRepository 读取后传入)
class LoadJournal extends EditorEvent {
final String title;
final Mood mood;
final List<String> tags;
final List<Stroke> strokes;
final List<JournalElement> 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 构造函数中注册:
on<LoadJournal>(_onLoadJournal);
实现 handler:
Future<void> _onLoadJournal(
LoadJournal event,
Emitter<EditorState> 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: 提交
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 的版本:
/// 加载已有日记数据并 dispatch LoadJournal
Future<void> _loadExistingJournal(String journalId) async {
try {
final repo = context.read<JournalRepository>();
final entry = await repo.getJournal(journalId);
if (entry == null || !mounted) return;
// 加载元素
final elements = await repo.getElements(journalId);
// 从 handwriting_ref 元素中反序列化笔画
List<Stroke> 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<String, dynamic>))
.toList();
}
}
// 过滤掉 handwriting_ref 元素(笔画单独管理)
final otherElements = elements
.where((e) => e.elementType != ElementType.handwritingRef)
.toList();
context.read<EditorBloc>().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: 提交
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)
修复模式(所有文件统一):
// 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: 提交
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: 手动验证 — 创建并重新编辑日记
./scripts/dev.sh app启动 Flutter Web- 创建日记:手写 + 贴纸 + 选择心情 → 点击"完成"
- 返回首页 → 点击刚创建的日记
- 验证:编辑器正确加载标题、笔画、贴纸、心情
- 继续编辑 → 添加新内容 → 再次保存 → 验证更新成功
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: 手动验证家长端完整流程
- 启动后端 + Flutter:
./scripts/dev.sh - 家长账号登录 → 进入家长中心
- 绑定孩子(输入孩子 ID)
- 查看孩子日记列表 → 验证列表非空,内容正确
- 点击"数据导出" → 验证导出结果显示(JSON 内容)
- 点击"数据删除" → 验证确认对话框弹出,包含 PIPL 法律文本
- 取消删除(不实际执行) → 验证数据未丢失
- Step 3: 如果发现数据流问题,修复后提交
常见问题可能包括:API 返回格式不匹配、状态转换不正确等。根据具体问题修复。
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 触发下载:
import 'dart:convert';
// 仅 Web 平台
import 'package:flutter/foundation.dart' show kIsWeb;
void _downloadJson(Map<String, dynamic> 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: 提交
git add app/lib/features/parent/views/parent_page.dart
git commit -m "feat(app): 家长端数据导出 — 添加 JSON 文件下载"
Task 7: Phase 2 端到端验证
- Step 1: 完整家长流程
- 家长登录 → 绑定孩子 → 查看日记列表
- 导出数据 → 验证 JSON 文件可下载、内容正确
- 删除单篇日记 → 确认对话框 → 取消(不实际删除)
- 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
// 评论列表底部面板 — 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<List<dynamic>>(
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<List<dynamic>> _fetchComments() async {
try {
final response = await apiClient.get('/diary/journals/$journalId/comments');
return response.data as List<dynamic>;
} 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<List<Comment>>(
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<List<Comment>> _fetchComments() async {
try {
final response = await apiClient.get('/diary/journals/$journalId/comments');
final list = response.data as List<dynamic>;
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: 提交
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)之后、"完成"按钮之前添加:
// 评语按钮(仅已有日记显示)
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 中添加:
/// 显示评论列表
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<ApiClient>(),
),
);
}
- Step 3: 确认 import 完整
在 editor_page.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: 提交
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):
- Guard:
state is ClassDetailLoaded✅ - 调用
_classRepo.createComment()✅ - 成功后
add(ClassLoadComments())刷新 ✅ - 失败时
debugPrint+emit error✅
- Step 2: 验证日记墙 UI 有评语入口
在班级详情/日记墙页面中,确认:
- 学生日记卡片上有评语按钮或评语输入框
- 如果缺失,需要在日记墙页面添加评语按钮 + 底部输入框
具体修改取决于当前日记墙的实现。如果 ClassBloc 的 CommentCreate 事件在 UI 中有对应触发点(评语输入框),则无需修改。
- Step 3: 手动验证 — 老师写评语
- 老师账号 → 进入班级 → 查看日记墙
- 找到学生日记 → 点击评语按钮 → 输入评语 → 提交
- 验证:评语提交成功,日记墙上显示评语数量更新
如有问题,修复后提交:
git add app/lib/features/class_/
git commit -m "fix(app): 修复班级日记墙评语 UI — [具体问题]"
Task 11: Phase 3 端到端验证
- Step 1: 完整师生互动流程
- 老师:创建班级 → 布置主题
- 学生:看到主题 → 写日记 → 分享到班级
- 老师:查看日记墙 → 写评语 → 验证提交成功
- 学生:打开该日记 → 点击评语图标 → 验证看到老师评语
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: 查询数据库确认菜单
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 迁移,补充缺失菜单。
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 中添加:
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct UpdateStickerPackReq {
#[validate(length(min = 1, max = 100))]
pub name: Option<String>,
pub description: Option<String>,
pub category: Option<String>,
pub is_free: Option<bool>,
}
- Step 2: 在 sticker_service.rs 添加 CRUD 方法
// 创建贴纸包
pub async fn create_sticker_pack(&self, req: CreateStickerPackReq) -> Result<StickerPack> { ... }
// 更新贴纸包
pub async fn update_sticker_pack(&self, id: &str, req: UpdateStickerPackReq) -> Result<StickerPack> { ... }
// 删除贴纸包(软删除)
pub async fn delete_sticker_pack(&self, id: &str) -> Result<()> { ... }
- Step 3: 在 sticker_handler.rs 添加 handler
// 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: 提交
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 调用
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: 提交
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 调用
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: 提交
git add crates/erp-diary/ apps/web/src/
git commit -m "feat(diary): 主题编辑/停用 — 后端 API + 管理端 UI"
Task 16: 全系统端到端验证
- Step 1: 编译全部三端
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: 最终提交(如有修复)
git add -A
git commit -m "fix: 全系统端到端验证修复 — [具体问题列表]"
计划版本 1.0 | 2026-06-02 | 基于 spec v1.1 (commit 860844a)