fix(app): Phase 1.1 紧急修复 — SyncEngine 接入 + authorId + catch 异常处理
- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync - fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local' - fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint - feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter) - feat(editor): EditorBloc 扩展 + EditorPage 增强 - feat(search): SearchBloc 扩展搜索功能 - feat(home): HomeBloc/HomePage 增强 - feat(auth): LoginPage 增强 - feat(templates): TemplateGalleryPage 重构 - fix(web): 管理端班级/日记页面修复 - fix(server): comment_service + theme_handler 修复 - docs: 添加全链路审计报告和验证截图
This commit is contained in:
@@ -16,7 +16,7 @@ extension GetJournalElementCollectionCollection on Isar {
|
||||
|
||||
const JournalElementCollectionSchema = CollectionSchema(
|
||||
name: r'JournalElementCollection',
|
||||
id: 5678901234567001,
|
||||
id: -1002,
|
||||
properties: {
|
||||
r'contentJson': PropertySchema(
|
||||
id: 0,
|
||||
@@ -96,7 +96,7 @@ const JournalElementCollectionSchema = CollectionSchema(
|
||||
idName: r'isarId',
|
||||
indexes: {
|
||||
r'id': IndexSchema(
|
||||
id: 5678901234567002,
|
||||
id: -2002,
|
||||
name: r'id',
|
||||
unique: false,
|
||||
replace: false,
|
||||
@@ -109,7 +109,7 @@ const JournalElementCollectionSchema = CollectionSchema(
|
||||
],
|
||||
),
|
||||
r'journalId': IndexSchema(
|
||||
id: 5678901234567003,
|
||||
id: 3001,
|
||||
name: r'journalId',
|
||||
unique: false,
|
||||
replace: false,
|
||||
|
||||
@@ -46,6 +46,9 @@ class JournalEntryCollection {
|
||||
/// 关联主题 ID(可选)
|
||||
String? assignedTopicId;
|
||||
|
||||
/// 内容摘要(自动从文本元素提取)
|
||||
String? contentExcerpt;
|
||||
|
||||
/// 版本号(乐观锁)
|
||||
int version = 1;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ extension GetJournalEntryCollectionCollection on Isar {
|
||||
|
||||
const JournalEntryCollectionSchema = CollectionSchema(
|
||||
name: r'JournalEntryCollection',
|
||||
id: 5678901234567004,
|
||||
id: -1001,
|
||||
properties: {
|
||||
r'assignedTopicId': PropertySchema(
|
||||
id: 0,
|
||||
@@ -33,63 +33,68 @@ const JournalEntryCollectionSchema = CollectionSchema(
|
||||
name: r'classId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'createdAtEpoch': PropertySchema(
|
||||
r'contentExcerpt': PropertySchema(
|
||||
id: 3,
|
||||
name: r'contentExcerpt',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'createdAtEpoch': PropertySchema(
|
||||
id: 4,
|
||||
name: r'createdAtEpoch',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'dateEpoch': PropertySchema(
|
||||
id: 4,
|
||||
id: 5,
|
||||
name: r'dateEpoch',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'id': PropertySchema(
|
||||
id: 5,
|
||||
id: 6,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'isDeleted': PropertySchema(
|
||||
id: 6,
|
||||
id: 7,
|
||||
name: r'isDeleted',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isPrivate': PropertySchema(
|
||||
id: 7,
|
||||
id: 8,
|
||||
name: r'isPrivate',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'mood': PropertySchema(
|
||||
id: 8,
|
||||
id: 9,
|
||||
name: r'mood',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'sharedToClass': PropertySchema(
|
||||
id: 9,
|
||||
id: 10,
|
||||
name: r'sharedToClass',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'tagsJson': PropertySchema(
|
||||
id: 10,
|
||||
id: 11,
|
||||
name: r'tagsJson',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'title': PropertySchema(
|
||||
id: 11,
|
||||
id: 12,
|
||||
name: r'title',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'updatedAtEpoch': PropertySchema(
|
||||
id: 12,
|
||||
id: 13,
|
||||
name: r'updatedAtEpoch',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'version': PropertySchema(
|
||||
id: 13,
|
||||
id: 14,
|
||||
name: r'version',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'weather': PropertySchema(
|
||||
id: 14,
|
||||
id: 15,
|
||||
name: r'weather',
|
||||
type: IsarType.string,
|
||||
)
|
||||
@@ -101,7 +106,7 @@ const JournalEntryCollectionSchema = CollectionSchema(
|
||||
idName: r'isarId',
|
||||
indexes: {
|
||||
r'id': IndexSchema(
|
||||
id: 5678901234567002,
|
||||
id: -2001,
|
||||
name: r'id',
|
||||
unique: false,
|
||||
replace: false,
|
||||
@@ -141,6 +146,12 @@ int _journalEntryCollectionEstimateSize(
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
{
|
||||
final value = object.contentExcerpt;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
bytesCount += 3 + object.id.length * 3;
|
||||
bytesCount += 3 + object.mood.length * 3;
|
||||
bytesCount += 3 + object.tagsJson.length * 3;
|
||||
@@ -158,18 +169,19 @@ void _journalEntryCollectionSerialize(
|
||||
writer.writeString(offsets[0], object.assignedTopicId);
|
||||
writer.writeString(offsets[1], object.authorId);
|
||||
writer.writeString(offsets[2], object.classId);
|
||||
writer.writeLong(offsets[3], object.createdAtEpoch);
|
||||
writer.writeLong(offsets[4], object.dateEpoch);
|
||||
writer.writeString(offsets[5], object.id);
|
||||
writer.writeBool(offsets[6], object.isDeleted);
|
||||
writer.writeBool(offsets[7], object.isPrivate);
|
||||
writer.writeString(offsets[8], object.mood);
|
||||
writer.writeBool(offsets[9], object.sharedToClass);
|
||||
writer.writeString(offsets[10], object.tagsJson);
|
||||
writer.writeString(offsets[11], object.title);
|
||||
writer.writeLong(offsets[12], object.updatedAtEpoch);
|
||||
writer.writeLong(offsets[13], object.version);
|
||||
writer.writeString(offsets[14], object.weather);
|
||||
writer.writeString(offsets[3], object.contentExcerpt);
|
||||
writer.writeLong(offsets[4], object.createdAtEpoch);
|
||||
writer.writeLong(offsets[5], object.dateEpoch);
|
||||
writer.writeString(offsets[6], object.id);
|
||||
writer.writeBool(offsets[7], object.isDeleted);
|
||||
writer.writeBool(offsets[8], object.isPrivate);
|
||||
writer.writeString(offsets[9], object.mood);
|
||||
writer.writeBool(offsets[10], object.sharedToClass);
|
||||
writer.writeString(offsets[11], object.tagsJson);
|
||||
writer.writeString(offsets[12], object.title);
|
||||
writer.writeLong(offsets[13], object.updatedAtEpoch);
|
||||
writer.writeLong(offsets[14], object.version);
|
||||
writer.writeString(offsets[15], object.weather);
|
||||
}
|
||||
|
||||
JournalEntryCollection _journalEntryCollectionDeserialize(
|
||||
@@ -182,19 +194,20 @@ JournalEntryCollection _journalEntryCollectionDeserialize(
|
||||
object.assignedTopicId = reader.readStringOrNull(offsets[0]);
|
||||
object.authorId = reader.readString(offsets[1]);
|
||||
object.classId = reader.readStringOrNull(offsets[2]);
|
||||
object.createdAtEpoch = reader.readLong(offsets[3]);
|
||||
object.dateEpoch = reader.readLong(offsets[4]);
|
||||
object.id = reader.readString(offsets[5]);
|
||||
object.isDeleted = reader.readBool(offsets[6]);
|
||||
object.isPrivate = reader.readBool(offsets[7]);
|
||||
object.contentExcerpt = reader.readStringOrNull(offsets[3]);
|
||||
object.createdAtEpoch = reader.readLong(offsets[4]);
|
||||
object.dateEpoch = reader.readLong(offsets[5]);
|
||||
object.id = reader.readString(offsets[6]);
|
||||
object.isDeleted = reader.readBool(offsets[7]);
|
||||
object.isPrivate = reader.readBool(offsets[8]);
|
||||
object.isarId = id;
|
||||
object.mood = reader.readString(offsets[8]);
|
||||
object.sharedToClass = reader.readBool(offsets[9]);
|
||||
object.tagsJson = reader.readString(offsets[10]);
|
||||
object.title = reader.readString(offsets[11]);
|
||||
object.updatedAtEpoch = reader.readLong(offsets[12]);
|
||||
object.version = reader.readLong(offsets[13]);
|
||||
object.weather = reader.readString(offsets[14]);
|
||||
object.mood = reader.readString(offsets[9]);
|
||||
object.sharedToClass = reader.readBool(offsets[10]);
|
||||
object.tagsJson = reader.readString(offsets[11]);
|
||||
object.title = reader.readString(offsets[12]);
|
||||
object.updatedAtEpoch = reader.readLong(offsets[13]);
|
||||
object.version = reader.readLong(offsets[14]);
|
||||
object.weather = reader.readString(offsets[15]);
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -212,28 +225,30 @@ P _journalEntryCollectionDeserializeProp<P>(
|
||||
case 2:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 3:
|
||||
return (reader.readLong(offset)) as P;
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
case 4:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 5:
|
||||
return (reader.readString(offset)) as P;
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 6:
|
||||
return (reader.readBool(offset)) as P;
|
||||
return (reader.readString(offset)) as P;
|
||||
case 7:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 8:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 10:
|
||||
case 9:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 10:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 11:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 12:
|
||||
return (reader.readLong(offset)) as P;
|
||||
return (reader.readString(offset)) as P;
|
||||
case 13:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 14:
|
||||
return (reader.readLong(offset)) as P;
|
||||
case 15:
|
||||
return (reader.readString(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
@@ -832,6 +847,162 @@ extension JournalEntryCollectionQueryFilter on QueryBuilder<
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'contentExcerpt',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'contentExcerpt',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptEqualTo(
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'contentExcerpt',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptGreaterThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'contentExcerpt',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptLessThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'contentExcerpt',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptBetween(
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'contentExcerpt',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'contentExcerpt',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'contentExcerpt',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition>
|
||||
contentExcerptContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'contentExcerpt',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition>
|
||||
contentExcerptMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'contentExcerpt',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'contentExcerpt',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> contentExcerptIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'contentExcerpt',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection,
|
||||
QAfterFilterCondition> createdAtEpochEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -1883,6 +2054,20 @@ extension JournalEntryCollectionQuerySortBy
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QAfterSortBy>
|
||||
sortByContentExcerpt() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'contentExcerpt', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QAfterSortBy>
|
||||
sortByContentExcerptDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'contentExcerpt', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QAfterSortBy>
|
||||
sortByCreatedAtEpoch() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -2096,6 +2281,20 @@ extension JournalEntryCollectionQuerySortThenBy on QueryBuilder<
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QAfterSortBy>
|
||||
thenByContentExcerpt() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'contentExcerpt', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QAfterSortBy>
|
||||
thenByContentExcerptDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'contentExcerpt', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QAfterSortBy>
|
||||
thenByCreatedAtEpoch() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -2303,6 +2502,14 @@ extension JournalEntryCollectionQueryWhereDistinct
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QDistinct>
|
||||
distinctByContentExcerpt({bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'contentExcerpt',
|
||||
caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, JournalEntryCollection, QDistinct>
|
||||
distinctByCreatedAtEpoch() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
@@ -2417,6 +2624,13 @@ extension JournalEntryCollectionQueryProperty on QueryBuilder<
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, String?, QQueryOperations>
|
||||
contentExcerptProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'contentExcerpt');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<JournalEntryCollection, int, QQueryOperations>
|
||||
createdAtEpochProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
|
||||
@@ -16,7 +16,7 @@ extension GetPendingOperationCollectionCollection on Isar {
|
||||
|
||||
const PendingOperationCollectionSchema = CollectionSchema(
|
||||
name: r'PendingOperationCollection',
|
||||
id: 5678901234567005,
|
||||
id: -1003,
|
||||
properties: {
|
||||
r'createdAtEpoch': PropertySchema(
|
||||
id: 0,
|
||||
@@ -61,7 +61,7 @@ const PendingOperationCollectionSchema = CollectionSchema(
|
||||
idName: r'isarId',
|
||||
indexes: {
|
||||
r'id': IndexSchema(
|
||||
id: 5678901234567002,
|
||||
id: -2003,
|
||||
name: r'id',
|
||||
unique: false,
|
||||
replace: false,
|
||||
|
||||
@@ -42,6 +42,10 @@ class JournalEntry {
|
||||
final bool isPrivate;
|
||||
final bool sharedToClass;
|
||||
final String? assignedTopicId;
|
||||
|
||||
/// 内容摘要 — 自动从文本元素提取,用于列表预览
|
||||
final String? contentExcerpt;
|
||||
|
||||
final int version;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
@@ -58,6 +62,7 @@ class JournalEntry {
|
||||
this.isPrivate = true,
|
||||
this.sharedToClass = false,
|
||||
this.assignedTopicId,
|
||||
this.contentExcerpt,
|
||||
this.version = 1,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@@ -77,6 +82,8 @@ class JournalEntry {
|
||||
bool? sharedToClass,
|
||||
String? assignedTopicId,
|
||||
bool clearAssignedTopicId = false,
|
||||
String? contentExcerpt,
|
||||
bool clearContentExcerpt = false,
|
||||
int? version,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
@@ -94,6 +101,9 @@ class JournalEntry {
|
||||
sharedToClass: sharedToClass ?? this.sharedToClass,
|
||||
assignedTopicId:
|
||||
clearAssignedTopicId ? null : (assignedTopicId ?? this.assignedTopicId),
|
||||
contentExcerpt: clearContentExcerpt
|
||||
? null
|
||||
: (contentExcerpt ?? this.contentExcerpt),
|
||||
version: version ?? this.version,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
@@ -111,6 +121,7 @@ class JournalEntry {
|
||||
'is_private': isPrivate,
|
||||
'shared_to_class': sharedToClass,
|
||||
'assigned_topic_id': assignedTopicId,
|
||||
'content_excerpt': contentExcerpt,
|
||||
'version': version,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
@@ -134,6 +145,7 @@ class JournalEntry {
|
||||
isPrivate: (json['is_private'] as bool?) ?? true,
|
||||
sharedToClass: (json['shared_to_class'] as bool?) ?? false,
|
||||
assignedTopicId: json['assigned_topic_id'] as String?,
|
||||
contentExcerpt: json['content_excerpt'] as String?,
|
||||
version: (json['version'] as int?) ?? 1,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
|
||||
@@ -39,7 +39,7 @@ class SseNotificationService {
|
||||
|
||||
SseNotificationService({
|
||||
required String token,
|
||||
String baseUrl = 'http://localhost:3000/api/v1',
|
||||
required String baseUrl,
|
||||
}) : _token = token,
|
||||
_baseUrl = baseUrl;
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
// - 联网后自动推送待同步操作
|
||||
// - 版本冲突时本地版本覆盖远端(简单策略)
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../local/isar_database.dart';
|
||||
@@ -114,6 +116,7 @@ class PendingOperation {
|
||||
class SyncEngine {
|
||||
final ApiClient _apiClient;
|
||||
final Queue<PendingOperation> _pendingQueue = Queue();
|
||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySub;
|
||||
|
||||
SyncStatus _status = SyncStatus.idle;
|
||||
String? _lastError;
|
||||
@@ -289,6 +292,26 @@ class SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动网络监听 — 网络恢复时自动触发同步
|
||||
///
|
||||
/// 在 app.dart 中创建 SyncEngine 后调用一次。
|
||||
/// 调用 [dispose] 停止监听。
|
||||
void startAutoSync() {
|
||||
_connectivitySub = Connectivity().onConnectivityChanged.listen((result) {
|
||||
final isOnline = result.any((r) => r != ConnectivityResult.none);
|
||||
if (isOnline && _pendingQueue.isNotEmpty && _status != SyncStatus.syncing) {
|
||||
debugPrint('SyncEngine: 网络恢复,开始同步 ${_pendingQueue.length} 个操作');
|
||||
trySync();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止网络监听并清理资源
|
||||
void dispose() {
|
||||
_connectivitySub?.cancel();
|
||||
_connectivitySub = null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 转换函数
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user