// 日记元素数据模型 — 手写不可变类(避免 build_runner 依赖) import 'dart:collection'; import 'dart:ui'; 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 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? 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 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 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.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 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, ); } /// 文字元素便利工厂 factory JournalElement.createText({ required String journalId, required String text, required Offset position, double fontSize = 18.0, String fontColor = '#2D2420', }) { return JournalElement.create( journalId: journalId, elementType: ElementType.text, positionX: position.dx, positionY: position.dy, content: { 'text': text, 'fontSize': fontSize, 'fontColor': fontColor, }, ); } /// 图片元素便利工厂 factory JournalElement.createImage({ required String journalId, required String filePath, required Offset position, String? thumbnailPath, }) { return JournalElement.create( journalId: journalId, elementType: ElementType.image, positionX: position.dx, positionY: position.dy, content: { 'filePath': filePath, if (thumbnailPath != null) 'thumbnailPath': thumbnailPath, }, ); } /// 贴纸元素便利工厂 factory JournalElement.createSticker({ required String journalId, required String emoji, required Offset position, String? stickerPackId, String? stickerId, }) { return JournalElement.create( journalId: journalId, elementType: ElementType.sticker, positionX: position.dx, positionY: position.dy, content: { 'emoji': emoji, if (stickerPackId != null) 'stickerPackId': stickerPackId, if (stickerId != null) 'stickerId': stickerId, }, ); } }