架构治理: - Feature Flag 落地: Cargo.toml [features] default=["diary"] + main.rs cfg 条件编译 - 环境配置统一: AppConfig 类 + --dart-define 注入 + SSE 端口 8080→3000 修复 搜索替代方案 (无 FTS): - SearchBloc + 标签/心情筛选接入后端 API - JournalRepository 扩展 mood/tag 筛选参数 - 搜索页 UI 接入实际数据(替换占位文本) 家长中心最小集 (PIPL 合规): - 后端: parent_service (绑定/查看/导出/删除/解绑) + parent_handler (6 个 API 端点) - 前端: ParentBloc + ParentPage 功能完整实现 - 绑定孩子、只读查看日记、导出数据、删除数据、解绑 Docker 部署: - verify.sh 健康检查脚本 (Axum/PG/Redis/OpenAPI 四项检查) 测试修复: - home_bloc_test / calendar_bloc_test 适配 JournalRepository 新参数 验证: flutter test 84/84 pass, cargo test 76/76 pass, cargo check pass
141 lines
3.9 KiB
Dart
141 lines
3.9 KiB
Dart
// 家长中心 BLoC — 管理家长-孩子绑定和数据操作
|
||
//
|
||
// 状态机: ParentInitial → ParentLoading → ParentChildrenLoaded / ParentJournalsLoaded / ParentDataExported / ParentDataDeleted / ParentError
|
||
// API: /diary/parent/* 端点
|
||
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
|
||
import '../../../data/remote/api_client.dart';
|
||
|
||
part 'parent_event.dart';
|
||
part 'parent_state.dart';
|
||
|
||
/// 家长中心 BLoC — 处理孩子绑定、日记查看、数据导出/删除
|
||
class ParentBloc extends Bloc<ParentEvent, ParentState> {
|
||
final ApiClient _api;
|
||
|
||
ParentBloc({required ApiClient api})
|
||
: _api = api,
|
||
super(const ParentInitial()) {
|
||
on<ParentLoadChildren>(_onLoadChildren);
|
||
on<ParentBindChild>(_onBindChild);
|
||
on<ParentViewJournals>(_onViewJournals);
|
||
on<ParentExportData>(_onExportData);
|
||
on<ParentDeleteData>(_onDeleteData);
|
||
on<ParentUnbindChild>(_onUnbindChild);
|
||
}
|
||
|
||
/// 加载已绑定的孩子列表
|
||
Future<void> _onLoadChildren(
|
||
ParentLoadChildren event,
|
||
Emitter<ParentState> emit,
|
||
) async {
|
||
emit(const ParentLoading());
|
||
try {
|
||
final response = await _api.get('/diary/parent/children');
|
||
final body = response.data as Map<String, dynamic>;
|
||
final items = body['data'] as List? ?? [];
|
||
final children = items
|
||
.map((j) => ChildBinding.fromJson(j as Map<String, dynamic>))
|
||
.toList();
|
||
emit(ParentChildrenLoaded(children));
|
||
} catch (e) {
|
||
emit(const ParentError('加载孩子列表失败'));
|
||
}
|
||
}
|
||
|
||
/// 绑定孩子(输入孩子 ID)
|
||
Future<void> _onBindChild(
|
||
ParentBindChild event,
|
||
Emitter<ParentState> emit,
|
||
) async {
|
||
emit(const ParentLoading());
|
||
try {
|
||
await _api.post('/diary/parent/bind', data: {
|
||
'child_id': event.childId,
|
||
});
|
||
// 绑定成功后重新加载列表
|
||
add(const ParentLoadChildren());
|
||
} catch (e) {
|
||
emit(const ParentError('绑定失败,请检查孩子 ID'));
|
||
}
|
||
}
|
||
|
||
/// 查看孩子日记(只读)
|
||
Future<void> _onViewJournals(
|
||
ParentViewJournals event,
|
||
Emitter<ParentState> emit,
|
||
) async {
|
||
emit(const ParentLoading());
|
||
try {
|
||
final response = await _api.get(
|
||
'/diary/parent/journals',
|
||
queryParams: {
|
||
'child_id': event.childId,
|
||
'page': 1,
|
||
'page_size': 50,
|
||
},
|
||
);
|
||
final body = response.data as Map<String, dynamic>;
|
||
final items = body['data'] as List? ?? [];
|
||
emit(ParentJournalsLoaded(
|
||
childId: event.childId,
|
||
journals: items.cast<Map<String, dynamic>>(),
|
||
));
|
||
} catch (e) {
|
||
emit(const ParentError('加载日记失败'));
|
||
}
|
||
}
|
||
|
||
/// 导出孩子数据(PIPL 合规)
|
||
Future<void> _onExportData(
|
||
ParentExportData event,
|
||
Emitter<ParentState> emit,
|
||
) async {
|
||
emit(const ParentLoading());
|
||
try {
|
||
final response = await _api.get(
|
||
'/diary/parent/export',
|
||
queryParams: {'child_id': event.childId},
|
||
);
|
||
emit(ParentDataExported(
|
||
childId: event.childId,
|
||
data: response.data as Map<String, dynamic>,
|
||
));
|
||
} catch (e) {
|
||
emit(const ParentError('导出失败'));
|
||
}
|
||
}
|
||
|
||
/// 删除孩子数据(PIPL 合规,需二次确认)
|
||
Future<void> _onDeleteData(
|
||
ParentDeleteData event,
|
||
Emitter<ParentState> emit,
|
||
) async {
|
||
emit(const ParentLoading());
|
||
try {
|
||
await _api.delete('/diary/parent/data', data: {
|
||
'child_id': event.childId,
|
||
});
|
||
emit(ParentDataDeleted(event.childId));
|
||
} catch (e) {
|
||
emit(const ParentError('删除失败'));
|
||
}
|
||
}
|
||
|
||
/// 解绑孩子
|
||
Future<void> _onUnbindChild(
|
||
ParentUnbindChild event,
|
||
Emitter<ParentState> emit,
|
||
) async {
|
||
try {
|
||
await _api.delete('/diary/parent/bind', data: {
|
||
'child_id': event.childId,
|
||
});
|
||
add(const ParentLoadChildren());
|
||
} catch (e) {
|
||
emit(const ParentError('解绑失败'));
|
||
}
|
||
}
|
||
}
|