import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ainoval/models/novel_setting_item.dart'; import 'package:ainoval/models/setting_type.dart'; import 'package:ainoval/models/setting_group.dart'; import 'package:ainoval/blocs/setting/setting_bloc.dart'; import 'package:ainoval/utils/web_theme.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:ainoval/screens/editor/widgets/novel_setting_detail.dart'; /// 设定信息预览卡片组件 /// 显示设定的基本信息(分类、名称、设定组、图片、描述) class SettingPreviewCard extends StatefulWidget { final String settingId; final String novelId; final Offset position; final VoidCallback? onClose; const SettingPreviewCard({ Key? key, required this.settingId, required this.novelId, required this.position, this.onClose, }) : super(key: key); @override State createState() => _SettingPreviewCardState(); } class _SettingPreviewCardState extends State with TickerProviderStateMixin { static const String _tag = 'SettingPreviewCard'; late AnimationController _animationController; late Animation _scaleAnimation; late Animation _opacityAnimation; NovelSettingItem? _settingItem; SettingGroup? _settingGroup; bool _isLoading = true; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); _scaleAnimation = Tween( begin: 0.8, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeOutCubic, )); _opacityAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeOut, )); _loadSettingData(); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } /// 加载设定数据 void _loadSettingData() { try { final settingBloc = context.read(); final state = settingBloc.state; // 查找设定条目 _settingItem = state.items.firstWhere( (item) => item.id == widget.settingId, orElse: () => NovelSettingItem(name: ''), ); if (_settingItem != null) { // 查找设定组 _settingGroup = state.groups.firstWhere( (group) => group.itemIds?.any((item) => item == widget.settingId) == true, orElse: () => SettingGroup(name: ''), ); } setState(() { _isLoading = false; }); AppLogger.d(_tag, '设定数据加载完成: ${_settingItem?.name ?? "未找到"}'); } catch (e) { AppLogger.e(_tag, '加载设定数据失败', e); setState(() { _isLoading = false; }); } } /// 获取设定类型图标 IconData _getTypeIcon() { if (_settingItem?.type == null) return Icons.article; final settingType = SettingType.fromValue(_settingItem!.type!); switch (settingType) { case SettingType.character: return Icons.person; case SettingType.location: return Icons.place; case SettingType.item: return Icons.inventory_2; case SettingType.lore: return Icons.public; case SettingType.event: return Icons.event; case SettingType.concept: return Icons.auto_awesome; case SettingType.faction: return Icons.groups; case SettingType.creature: return Icons.pets; case SettingType.magicSystem: return Icons.auto_fix_high; case SettingType.technology: return Icons.science; case SettingType.culture: return Icons.emoji_people; case SettingType.history: return Icons.history; case SettingType.organization: return Icons.apartment; case SettingType.worldview: return Icons.public; case SettingType.pleasurePoint: return Icons.whatshot; case SettingType.anticipationHook: return Icons.bolt; case SettingType.theme: return Icons.category; case SettingType.tone: return Icons.tonality; case SettingType.style: return Icons.brush; case SettingType.trope: return Icons.theater_comedy; case SettingType.plotDevice: return Icons.schema; case SettingType.powerSystem: return Icons.flash_on; case SettingType.timeline: return Icons.timeline; case SettingType.religion: return Icons.account_balance; case SettingType.politics: return Icons.gavel; case SettingType.economy: return Icons.attach_money; case SettingType.geography: return Icons.map; default: return Icons.article; } } /// 获取设定类型显示名称 String _getTypeDisplayName() { if (_settingItem?.type == null) return '其他'; return SettingType.fromValue(_settingItem!.type!).displayName; } /// 处理标题点击 void _handleTitleTap() { AppLogger.d(_tag, '点击设定标题,打开详情卡片: ${_settingItem?.name}'); // 关闭当前预览卡片 _close(); // 延迟一小段时间后打开详情卡片,确保预览卡片完全关闭 Future.delayed(const Duration(milliseconds: 100), () { if (mounted && _settingItem != null) { FloatingNovelSettingDetail.show( context: context, itemId: _settingItem!.id, novelId: widget.novelId, groupId: _settingGroup?.id, isEditing: false, onSave: (item, groupId) { // 保存成功后可以做一些处理 AppLogger.i(_tag, '设定详情保存成功: ${item.name}'); }, onCancel: () { // 取消操作 AppLogger.d(_tag, '设定详情编辑取消'); }, ); } }); } /// 关闭卡片 void _close() { _animationController.reverse().then((_) { widget.onClose?.call(); }); } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; final isDark = WebTheme.isDarkMode(context); // 计算卡片位置,确保不超出屏幕边界 const cardWidth = 320.0; const cardHeight = 200.0; double left = widget.position.dx; double top = widget.position.dy; // 调整水平位置 if (left + cardWidth > screenSize.width) { left = screenSize.width - cardWidth - 16; } if (left < 16) { left = 16; } // 调整垂直位置 if (top + cardHeight > screenSize.height) { top = widget.position.dy - cardHeight - 10; // 显示在鼠标上方 } if (top < 16) { top = 16; } return Positioned( left: left, top: top, child: AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Opacity( opacity: _opacityAnimation.value, child: Material( elevation: 12, borderRadius: BorderRadius.circular(12), color: Colors.transparent, shadowColor: Theme.of(context).colorScheme.shadow.withOpacity(0.3), child: Container( width: cardWidth, constraints: const BoxConstraints( maxHeight: cardHeight, ), decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(12), border: Border.all( color: isDark ? WebTheme.darkGrey700 : WebTheme.grey300, width: 1.5, ), ), child: _buildCardContent(isDark), ), ), ), ); }, ), ); } /// 构建卡片内容 Widget _buildCardContent(bool isDark) { if (_isLoading) { return const Padding( padding: EdgeInsets.all(16), child: Center( child: CircularProgressIndicator(), ), ); } if (_settingItem == null) { return Padding( padding: const EdgeInsets.all(16), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, size: 32, color: WebTheme.getSecondaryTextColor(context), ), const SizedBox(height: 8), Text( '设定不存在', style: TextStyle( fontSize: 14, color: WebTheme.getSecondaryTextColor(context), ), ), ], ), ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // 头部区域 _buildHeader(isDark), // 分隔线 Container( height: 1, color: isDark ? WebTheme.darkGrey800 : WebTheme.grey200, ), // 内容区域 Flexible( child: _buildContent(isDark), ), ], ); } /// 构建头部区域 Widget _buildHeader(bool isDark) { return Container( padding: const EdgeInsets.all(16), child: Row( children: [ // 设定图片或类型图标 Container( width: 48, height: 48, decoration: BoxDecoration( color: isDark ? WebTheme.darkGrey800 : WebTheme.grey100, borderRadius: BorderRadius.circular(8), border: Border.all( color: isDark ? WebTheme.darkGrey700 : WebTheme.grey300, width: 1, ), ), child: _settingItem!.imageUrl != null && _settingItem!.imageUrl!.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(7), child: Image.network( _settingItem!.imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Icon( _getTypeIcon(), size: 24, color: WebTheme.getTextColor(context), ); }, ), ) : Icon( _getTypeIcon(), size: 24, color: WebTheme.getTextColor(context), ), ), const SizedBox(width: 12), // 设定信息 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 设定名称(可点击) GestureDetector( onTap: _handleTitleTap, child: MouseRegion( cursor: SystemMouseCursors.click, child: Text( _settingItem!.name, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), decoration: TextDecoration.underline, decorationColor: WebTheme.getTextColor(context).withOpacity(0.3), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), const SizedBox(height: 4), // 类型和设定组 Row( children: [ // 设定类型 Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: WebTheme.getTextColor(context).withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( _getTypeDisplayName(), style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), ), if (_settingGroup != null) ...[ const SizedBox(width: 8), // 设定组 Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: WebTheme.getSecondaryTextColor(context).withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( _settingGroup!.name, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: WebTheme.getSecondaryTextColor(context), ), ), ), ], ], ), ], ), ), // 关闭按钮 GestureDetector( onTap: _close, child: MouseRegion( cursor: SystemMouseCursors.click, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(4), ), child: Icon( Icons.close, size: 16, color: WebTheme.getSecondaryTextColor(context), ), ), ), ), ], ), ); } /// 构建内容区域 Widget _buildContent(bool isDark) { return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // 描述内容 if (_settingItem!.description != null && _settingItem!.description!.isNotEmpty) ...[ Text( '描述', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), const SizedBox(height: 6), Flexible( child: Text( _settingItem!.description!, style: TextStyle( fontSize: 13, height: 1.4, color: WebTheme.getSecondaryTextColor(context), ), maxLines: 4, overflow: TextOverflow.ellipsis, ), ), ] else if (_settingItem!.content != null && _settingItem!.content!.isNotEmpty) ...[ Text( '内容', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), const SizedBox(height: 6), Flexible( child: Text( _settingItem!.content!, style: TextStyle( fontSize: 13, height: 1.4, color: WebTheme.getSecondaryTextColor(context), ), maxLines: 4, overflow: TextOverflow.ellipsis, ), ), ] else ...[ Center( child: Text( '暂无描述', style: TextStyle( fontSize: 13, color: WebTheme.getSecondaryTextColor(context).withOpacity(0.6), fontStyle: FontStyle.italic, ), ), ), ], const SizedBox(height: 8), // 提示文本 Text( '点击标题查看详情', style: TextStyle( fontSize: 11, color: WebTheme.getSecondaryTextColor(context).withOpacity(0.7), fontStyle: FontStyle.italic, ), ), ], ), ); } }