Files
nj/app/lib/data/models/journal_element.dart
iven 5e6c6fdd62 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
2026-06-01 00:55:51 +08:00

157 lines
4.7 KiB
Dart

// 日记元素数据模型 — 手写不可变类(避免 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,
);
}
}