问题修复: 1. Web端保存的日记看不到:createJournal 返回值未捕获,server ID 丢失导致 后续元素保存用错 ID。现在使用 saved.id 贯穿全部操作。 2. 管理端看不到新建日记:后端 list_journals 添加 is_private 过滤,admin/teacher 查看他人日记时排除私密日记。 3. RemoteJournalRepository 添加 onJournalChanged 变更通知流,HomeBloc 可自动刷新。 4. SyncEngine(native + web)enqueue 添加 is_private 防御性检查,私密日记不入队。 5. 编辑器 _persistState 条件入队:仅非私密日记同步到后端。 6. 分享流程改造:首次从私密变为公开时入队 create 操作上传。 7. 日记卡片添加可见性标签(仅自己可见/班级可见/公开),私密日记可点击分享。 8. 首页 _sharePrivateJournal 弹出 ShareBottomSheet 主动分享。
172 lines
5.8 KiB
Dart
172 lines
5.8 KiB
Dart
// 远程日记仓库 — 通过 API 客户端连接后端
|
||
|
||
import 'dart:async';
|
||
|
||
import '../models/journal_element.dart';
|
||
import '../models/journal_entry.dart';
|
||
import '../remote/api_client.dart';
|
||
import 'journal_repository.dart';
|
||
|
||
/// 远程日记仓库 — 通过 HTTP API 操作后端数据
|
||
///
|
||
/// 所有操作需要网络连接。离线场景由 SyncEngine 协调 Isar 本地仓库处理。
|
||
class RemoteJournalRepository implements JournalRepository {
|
||
final ApiClient _api;
|
||
|
||
/// 变更通知流控制器 — 写操作成功后通知 UI 刷新
|
||
final StreamController<void> _changeController =
|
||
StreamController<void>.broadcast();
|
||
|
||
RemoteJournalRepository({required ApiClient api}) : _api = api;
|
||
|
||
@override
|
||
Future<List<JournalEntry>> getJournals({
|
||
DateTime? dateFrom,
|
||
DateTime? dateTo,
|
||
int? page,
|
||
int? pageSize,
|
||
String? mood,
|
||
String? tag,
|
||
String? classId,
|
||
}) async {
|
||
final queryParams = <String, dynamic>{};
|
||
// 后端 NaiveDateTime 格式: "2026-06-01T00:00:00"(不带毫秒)
|
||
if (dateFrom != null) {
|
||
queryParams['date_from'] = dateFrom.toIso8601String().replaceFirst(RegExp(r'\.\d+'), '');
|
||
}
|
||
if (dateTo != null) {
|
||
queryParams['date_to'] = dateTo.toIso8601String().replaceFirst(RegExp(r'\.\d+'), '');
|
||
}
|
||
if (page != null) queryParams['page'] = page;
|
||
if (pageSize != null) queryParams['page_size'] = pageSize;
|
||
if (mood != null) queryParams['mood'] = mood;
|
||
if (tag != null) queryParams['tag'] = tag;
|
||
if (classId != null) queryParams['class_id'] = classId;
|
||
|
||
final response = await _api.get('/diary/journals', queryParams: queryParams);
|
||
final body = response.data as Map<String, dynamic>;
|
||
// 后端信封格式: { success, data: { data: [...], total, page, ... }, message }
|
||
final envelope = body['data'] as Map<String, dynamic>? ?? {};
|
||
final items = envelope['data'] as List? ?? [];
|
||
return items
|
||
.map((json) => JournalEntry.fromJson(json as Map<String, dynamic>))
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<int> getJournalCount() async {
|
||
final response = await _api.get('/diary/journals', queryParams: {
|
||
'page': 1,
|
||
'page_size': 1,
|
||
});
|
||
final body = response.data as Map<String, dynamic>;
|
||
// 后端信封格式: { success, data: { data: [...], total, ... }, message }
|
||
final envelope = body['data'] as Map<String, dynamic>? ?? {};
|
||
return (envelope['total'] as int?) ?? 0;
|
||
}
|
||
|
||
@override
|
||
Future<JournalEntry?> getJournal(String id) async {
|
||
try {
|
||
final response = await _api.get('/diary/journals/$id');
|
||
final body = response.data as Map<String, dynamic>;
|
||
return JournalEntry.fromJson(body['data'] as Map<String, dynamic>);
|
||
} on ApiException catch (e) {
|
||
if (e.statusCode == 404) return null;
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<JournalEntry> createJournal(JournalEntry entry) async {
|
||
// 后端 CreateJournalReq.date 是 NaiveDate(只有日期),需转换格式
|
||
final json = entry.toJson();
|
||
json['date'] = entry.date.toIso8601String().substring(0, 10);
|
||
final response = await _api.post('/diary/journals', data: json);
|
||
final body = response.data as Map<String, dynamic>;
|
||
final created = JournalEntry.fromJson(body['data'] as Map<String, dynamic>);
|
||
_changeController.add(null); // 通知 UI 刷新列表
|
||
return created;
|
||
}
|
||
|
||
@override
|
||
Future<JournalEntry> updateJournal(JournalEntry entry) async {
|
||
final response = await _api.put(
|
||
'/diary/journals/${entry.id}',
|
||
data: {
|
||
'title': entry.title,
|
||
'mood': entry.mood.value,
|
||
'weather': entry.weather.value,
|
||
'tags': entry.tags,
|
||
'is_private': entry.isPrivate,
|
||
'shared_to_class': entry.sharedToClass,
|
||
'version': entry.version,
|
||
},
|
||
);
|
||
final body = response.data as Map<String, dynamic>;
|
||
_changeController.add(null); // 通知 UI 刷新列表
|
||
return JournalEntry.fromJson(body['data'] as Map<String, dynamic>);
|
||
}
|
||
|
||
@override
|
||
Future<void> deleteJournal(String id) async {
|
||
await _api.delete('/diary/journals/$id');
|
||
_changeController.add(null); // 通知 UI 刷新列表
|
||
}
|
||
|
||
@override
|
||
Future<List<JournalElement>> getElements(String journalId) async {
|
||
final response = await _api.get('/diary/journals/$journalId/elements');
|
||
final body = response.data as Map<String, dynamic>;
|
||
final items = body['data'] as List? ?? [];
|
||
return items
|
||
.map((json) => JournalElement.fromJson(json as Map<String, dynamic>))
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<JournalElement> addElement(JournalElement element) async {
|
||
final response = await _api.post(
|
||
'/diary/journals/${element.journalId}/elements',
|
||
data: element.toJson(),
|
||
);
|
||
final body = response.data as Map<String, dynamic>;
|
||
return JournalElement.fromJson(body['data'] as Map<String, dynamic>);
|
||
}
|
||
|
||
@override
|
||
Future<JournalElement> updateElement(JournalElement element) async {
|
||
final response = await _api.put(
|
||
'/diary/journals/${element.journalId}/elements/${element.id}',
|
||
data: element.toJson(),
|
||
);
|
||
final body = response.data as Map<String, dynamic>;
|
||
return JournalElement.fromJson(body['data'] as Map<String, dynamic>);
|
||
}
|
||
|
||
@override
|
||
Future<void> removeElement(String elementId) async {
|
||
await _api.delete('/diary/elements/$elementId');
|
||
}
|
||
|
||
/// 变更通知流 — 写操作后广播,供 HomeBloc 自动刷新
|
||
@override
|
||
Stream<void> get onJournalChanged => _changeController.stream;
|
||
}
|
||
|
||
/// API 异常封装 — 后端返回非 2xx 状态码时抛出
|
||
class ApiException implements Exception {
|
||
final String message;
|
||
final int statusCode;
|
||
final dynamic responseBody;
|
||
|
||
const ApiException({
|
||
required this.message,
|
||
required this.statusCode,
|
||
this.responseBody,
|
||
});
|
||
|
||
@override
|
||
String toString() => 'ApiException($statusCode): $message';
|
||
}
|