feat(diary): 数据层 + 班级系统 (Phase F1 + B3)
Flutter 数据层 (Phase F1): - journal_entry.dart: 日记数据模型 (Mood/Weather/tags/version) - journal_element.dart: 元素模型 (text/image/sticker/handwriting_ref/tape) - school_class.dart: 班级模型 - user_settings.dart: 用户设置 (主题/画笔/字号) - isar_database.dart: Isar 初始化 - api_client.dart: Dio + JWT注入 + 离线感知 + 401处理 - journal_repository.dart: 抽象接口 + InMemory实现 (乐观锁) - sync_engine.dart: WiFi同步 + 操作队列 + 重试(5次) + 快照持久化 Rust 班级系统 (Phase B3): - class_service.rs: 创建班级(6位码) + 加入班级 + 成员管理 - topic_service.rs: 老师布置主题 + 主题列表 - comment_service.rs: 老师点评 + 评语列表 - class_handler.rs: 5个API端点 + 权限守卫 - topic_handler.rs: 2个API端点 - comment_handler.rs: 2个API端点 - dto.rs: 新增5个DTO (ClassMemberResp/CreateTopicReq/TopicResp/CreateCommentReq/CommentResp) - 6条新路由注册 验证: cargo check 通过, 433测试全绿, flutter analyze 1 warning
This commit is contained in:
156
app/lib/data/models/journal_element.dart
Normal file
156
app/lib/data/models/journal_element.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
// 日记元素数据模型 — 手写不可变类(避免 build_runner 依赖)
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// 元素类型 — 日记页面上的各种内容载体
|
||||
enum ElementType {
|
||||
text('text'),
|
||||
image('image'),
|
||||
sticker('sticker'),
|
||||
handwritingRef('handwriting_ref'),
|
||||
tape('tape');
|
||||
|
||||
const ElementType(this.value);
|
||||
final String value;
|
||||
}
|
||||
|
||||
/// 日记元素 — 日记页面中的单个内容单元
|
||||
///
|
||||
/// 每个元素有独立的位置、尺寸、旋转和层级。
|
||||
/// [content] 字段根据 [elementType] 存储不同的结构化数据:
|
||||
/// - text: {'text': '...', 'fontSize': 16.0, 'fontColor': '#2D2420'}
|
||||
/// - image: {'filePath': '...', 'thumbnailPath': '...'}
|
||||
/// - sticker: {'stickerPackId': '...', 'stickerId': '...'}
|
||||
/// - handwriting_ref: {'strokesFileId': '...', 'strokeCount': 42}
|
||||
/// - tape: {'tapeStyle': 'washi_dots', 'tapeColor': '#F2CC8F'}
|
||||
class JournalElement {
|
||||
final String id;
|
||||
final String journalId;
|
||||
final ElementType elementType;
|
||||
final double positionX;
|
||||
final double positionY;
|
||||
final double width;
|
||||
final double height;
|
||||
final double rotation;
|
||||
final int zIndex;
|
||||
final Map<String, dynamic> content;
|
||||
final int version;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const JournalElement({
|
||||
required this.id,
|
||||
required this.journalId,
|
||||
required this.elementType,
|
||||
this.positionX = 0,
|
||||
this.positionY = 0,
|
||||
this.width = 100,
|
||||
this.height = 100,
|
||||
this.rotation = 0,
|
||||
this.zIndex = 0,
|
||||
this.content = const {},
|
||||
this.version = 1,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
JournalElement copyWith({
|
||||
String? id,
|
||||
String? journalId,
|
||||
ElementType? elementType,
|
||||
double? positionX,
|
||||
double? positionY,
|
||||
double? width,
|
||||
double? height,
|
||||
double? rotation,
|
||||
int? zIndex,
|
||||
Map<String, dynamic>? content,
|
||||
int? version,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) =>
|
||||
JournalElement(
|
||||
id: id ?? this.id,
|
||||
journalId: journalId ?? this.journalId,
|
||||
elementType: elementType ?? this.elementType,
|
||||
positionX: positionX ?? this.positionX,
|
||||
positionY: positionY ?? this.positionY,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
rotation: rotation ?? this.rotation,
|
||||
zIndex: zIndex ?? this.zIndex,
|
||||
content: content ?? this.content,
|
||||
version: version ?? this.version,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'journal_id': journalId,
|
||||
'element_type': elementType.value,
|
||||
'position_x': positionX,
|
||||
'position_y': positionY,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'rotation': rotation,
|
||||
'z_index': zIndex,
|
||||
'content': content,
|
||||
'version': version,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
factory JournalElement.fromJson(Map<String, dynamic> json) => JournalElement(
|
||||
id: json['id'] as String,
|
||||
journalId: json['journal_id'] as String,
|
||||
elementType: ElementType.values.firstWhere(
|
||||
(e) => e.value == json['element_type'],
|
||||
orElse: () => ElementType.text,
|
||||
),
|
||||
positionX: (json['position_x'] as num?)?.toDouble() ?? 0,
|
||||
positionY: (json['position_y'] as num?)?.toDouble() ?? 0,
|
||||
width: (json['width'] as num?)?.toDouble() ?? 100,
|
||||
height: (json['height'] as num?)?.toDouble() ?? 100,
|
||||
rotation: (json['rotation'] as num?)?.toDouble() ?? 0,
|
||||
zIndex: (json['z_index'] as int?) ?? 0,
|
||||
content: UnmodifiableMapView(
|
||||
Map<String, dynamic>.from(json['content'] as Map? ?? {}),
|
||||
),
|
||||
version: (json['version'] as int?) ?? 1,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
);
|
||||
|
||||
/// 创建新元素的工厂方法 — 自动生成 id 和时间戳
|
||||
factory JournalElement.create({
|
||||
required String journalId,
|
||||
required ElementType elementType,
|
||||
double positionX = 0,
|
||||
double positionY = 0,
|
||||
double width = 100,
|
||||
double height = 100,
|
||||
double rotation = 0,
|
||||
int zIndex = 0,
|
||||
Map<String, dynamic> content = const {},
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
return JournalElement(
|
||||
id: const Uuid().v4(),
|
||||
journalId: journalId,
|
||||
elementType: elementType,
|
||||
positionX: positionX,
|
||||
positionY: positionY,
|
||||
width: width,
|
||||
height: height,
|
||||
rotation: rotation,
|
||||
zIndex: zIndex,
|
||||
content: content,
|
||||
version: 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user