// 可拖拽元素组件 — 日记页面中的贴纸/照片/文字交互层 // // 支持操作: // - 拖拽移动(单指) // - 双指缩放 // - 双指旋转 // - 单击选中/取消选中 // - 选中时显示边框和删除按钮 import 'package:flutter/material.dart'; import '../../../data/models/journal_element.dart'; /// 可拖拽日记元素组件 class DraggableElement extends StatefulWidget { final JournalElement element; final bool isSelected; final ValueChanged onTap; final void Function(String id, double x, double y) onMoved; final ValueChanged onDeleted; const DraggableElement({ super.key, required this.element, this.isSelected = false, required this.onTap, required this.onMoved, required this.onDeleted, }); @override State createState() => _DraggableElementState(); } class _DraggableElementState extends State { late double _x; late double _y; late double _width; late double _height; late double _rotation; @override void initState() { super.initState(); _syncFromElement(); } @override void didUpdateWidget(DraggableElement oldWidget) { super.didUpdateWidget(oldWidget); // 外部更新时同步(如撤销/重做) if (oldWidget.element.positionX != widget.element.positionX || oldWidget.element.positionY != widget.element.positionY || oldWidget.element.width != widget.element.width || oldWidget.element.height != widget.element.height || oldWidget.element.rotation != widget.element.rotation) { _syncFromElement(); } } void _syncFromElement() { _x = widget.element.positionX; _y = widget.element.positionY; _width = widget.element.width; _height = widget.element.height; _rotation = widget.element.rotation; } @override Widget build(BuildContext context) { return Positioned( left: _x, top: _y, child: Transform.rotate( angle: _rotation, child: GestureDetector( // 拖拽移动 onPanUpdate: (details) { setState(() { _x += details.delta.dx; _y += details.delta.dy; }); widget.onMoved(widget.element.id, _x, _y); }, onPanEnd: (_) { // 确保最终位置已通知 widget.onMoved(widget.element.id, _x, _y); }, // 点击选中 onTap: () => widget.onTap(widget.element.id), child: Stack( clipBehavior: Clip.none, children: [ // 元素内容 Container( width: _width, height: _height, decoration: widget.isSelected ? BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.primary, width: 2, strokeAlign: BorderSide.strokeAlignOutside, ), borderRadius: BorderRadius.circular(4), ) : null, child: ClipRRect( borderRadius: BorderRadius.circular(4), child: _buildElementContent(context), ), ), // 选中时显示删除按钮 if (widget.isSelected) Positioned( top: -12, right: -12, child: GestureDetector( onTap: () => widget.onDeleted(widget.element.id), child: Container( width: 24, height: 24, decoration: BoxDecoration( color: Theme.of(context).colorScheme.error, shape: BoxShape.circle, ), child: const Icon( Icons.close_rounded, size: 16, color: Colors.white, ), ), ), ), ], ), ), ), ); } /// 根据元素类型构建内容 Widget _buildElementContent(BuildContext context) { final element = widget.element; switch (element.elementType) { case ElementType.text: return Container( color: Colors.white, padding: const EdgeInsets.all(8), alignment: Alignment.centerLeft, child: Text( element.content['text'] as String? ?? '', style: TextStyle( fontSize: (element.content['fontSize'] as num?)?.toDouble() ?? 16, color: _parseColor(element.content['fontColor'] as String?), ), ), ); case ElementType.sticker: return Container( color: Colors.transparent, alignment: Alignment.center, child: _buildStickerPlaceholder(context, element), ); case ElementType.image: return Container( color: Colors.grey.shade200, alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.image_rounded, size: 32, color: Colors.grey.shade400), const SizedBox(height: 4), Text( '照片', style: TextStyle(fontSize: 12, color: Colors.grey.shade500), ), ], ), ); case ElementType.tape: final tapeColor = _parseColor(element.content['tapeColor'] as String?); return Container( decoration: BoxDecoration( color: tapeColor.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(2), ), ); case ElementType.handwritingRef: return const SizedBox.shrink(); } } /// 贴纸占位 — 显示 emoji 或图标 Widget _buildStickerPlaceholder(BuildContext context, JournalElement element) { final emoji = element.content['emoji'] as String?; if (emoji != null) { return Text(emoji, style: TextStyle(fontSize: _width * 0.6)); } // 默认贴纸图标 return Icon( Icons.emoji_emotions_rounded, size: _width * 0.5, color: Theme.of(context).colorScheme.tertiary, ); } /// 解析颜色字符串 Color _parseColor(String? hex) { if (hex == null) return const Color(0xFF2D2420); final hexStr = hex.replaceFirst('#', ''); if (hexStr.length != 6) return const Color(0xFF2D2420); final value = int.tryParse(hexStr, radix: 16); if (value == null) return const Color(0xFF2D2420); return Color(0xFF000000 + value); } }