import 'package:ainoval/blocs/next_outline/next_outline_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../blocs/next_outline/next_outline_bloc.dart'; import '../../../models/user_ai_model_config_model.dart'; import 'package:ainoval/screens/next_outline/widgets/modern_result_card.dart'; import 'package:ainoval/widgets/common/loading_indicator.dart'; import 'package:ainoval/utils/web_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; /// 剧情推演结果网格 - 全局通用组件 /// /// 此组件负责展示和管理剧情推演的生成结果: /// 1. 结果展示 - 以网格形式展示多个剧情选项 /// 2. 交互操作 - 支持选择、重新生成、保存等操作 /// 3. 状态管理 - 处理加载、空状态、错误状态 /// 4. 响应式布局 - 适配不同屏幕尺寸 /// /// 设计特点: /// - 采用纯黑白配色方案,保持视觉一致性 /// - 现代化的卡片设计和交互反馈 /// - 清晰的信息层次和操作引导 /// - 优化的间距和组件尺寸 class ResultsGrid extends StatefulWidget { /// 剧情选项列表 - 生成的剧情推演结果 final List outlineOptions; /// 当前选中的剧情选项ID final String? selectedOptionId; /// AI模型配置列表 - 用于重新生成操作 final List aiModelConfigs; /// 是否正在生成 - 控制全局生成状态 final bool isGenerating; /// 是否正在保存 - 控制保存操作状态 final bool isSaving; /// 选项选中回调 - 用户选择特定剧情选项 final Function(String optionId) onOptionSelected; /// 重新生成单个选项回调 - 重新生成特定选项 final Function(String optionId, String configId, String? hint) onRegenerateSingle; /// 重新生成全部选项回调 - 批量重新生成 final Function(String? hint) onRegenerateAll; /// 保存大纲回调 - 保存选中的剧情到小说结构 final Function(String optionId, String insertType) onSaveOutline; const ResultsGrid({ Key? key, required this.outlineOptions, this.selectedOptionId, required this.aiModelConfigs, this.isGenerating = false, this.isSaving = false, required this.onOptionSelected, required this.onRegenerateSingle, required this.onRegenerateAll, required this.onSaveOutline, }) : super(key: key); @override State createState() => _ResultsGridState(); } /// 结果网格状态管理 /// /// 负责: /// 1. 本地状态管理(重新生成提示等) /// 2. 响应式布局计算 /// 3. 用户交互处理 /// 4. 对话框和弹窗管理 class _ResultsGridState extends State { final TextEditingController _regenerateHintController = TextEditingController(); @override void dispose() { _regenerateHintController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题区域 - 统一的视觉标识 _buildSectionHeader(context), const SizedBox(height: 24), // 标题与内容的间距 // 主内容区域 - 根据状态显示不同内容 _buildMainContent(context), ], ); } /// 构建区域标题 /// 提供清晰的功能标识和视觉层次 Widget _buildSectionHeader(BuildContext context) { return Row( children: [ Icon( LucideIcons.layout_grid, size: 24, color: WebTheme.getTextColor(context), ), const SizedBox(width: 12), Text( '生成结果', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: WebTheme.getTextColor(context), fontWeight: FontWeight.w600, height: 1.2, ), ), const Spacer(), // 结果数量指示器 if (widget.outlineOptions.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: WebTheme.getEmptyStateColor(context), borderRadius: BorderRadius.circular(16), border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), ), child: Text( '${widget.outlineOptions.length} 个选项', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: WebTheme.getSecondaryTextColor(context), ), ), ), ], ); } /// 构建主内容区域 /// 根据不同状态显示相应的内容 Widget _buildMainContent(BuildContext context) { // 全局加载状态 - 首次生成时的加载指示 if (widget.isGenerating && widget.outlineOptions.isEmpty) { return _buildLoadingState(); } // 空状态 - 尚未生成任何结果 if (widget.outlineOptions.isEmpty) { return _buildEmptyState(); } // 有结果状态 - 显示结果网格和操作区域 return _buildResultsContent(context); } /// 构建加载状态 /// 现代化的加载指示器 Widget _buildLoadingState() { return Container( padding: const EdgeInsets.all(40), child: Center( child: Column( children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: WebTheme.getCardColor(context), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.5), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: const LoadingIndicator(message: '正在生成剧情选项...'), ), ], ), ), ); } /// 构建空状态 /// 引导用户进行首次生成 Widget _buildEmptyState() { return Container( padding: const EdgeInsets.all(40), child: Center( child: Column( children: [ Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: WebTheme.getEmptyStateColor(context), borderRadius: BorderRadius.circular(12), border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), ), child: Column( children: [ Icon( LucideIcons.sparkles, size: 48, color: WebTheme.getSecondaryTextColor(context), ), const SizedBox(height: 16), Text( '尚未生成剧情', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: WebTheme.getTextColor(context), fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( '请在上方配置参数后点击"生成剧情大纲"', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: WebTheme.getSecondaryTextColor(context), ), textAlign: TextAlign.center, ), ], ), ), ], ), ), ); } /// 构建结果内容 /// 包含结果网格和操作按钮 Widget _buildResultsContent(BuildContext context) { return Column( children: [ // 结果卡片网格 - 响应式布局 LayoutBuilder( builder: (context, constraints) { final crossAxisCount = _calculateCrossAxisCount(constraints.maxWidth); return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: 0.62, // 调高卡片高度 crossAxisSpacing: 24, // 增加间距 mainAxisSpacing: 24, // 增加间距 ), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: widget.outlineOptions.length, itemBuilder: (context, index) { final option = widget.outlineOptions[index]; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.5), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: ModernResultCard( option: option, isSelected: widget.selectedOptionId == option.optionId, aiModelConfigs: widget.aiModelConfigs, onSelected: () => widget.onOptionSelected(option.optionId), onRegenerateSingle: (configId, hint) => widget.onRegenerateSingle(option.optionId, configId, hint), onSave: (insertType) => widget.onSaveOutline(option.optionId, insertType), ), ); }, ); }, ), const SizedBox(height: 32), // 网格与操作按钮的间距 // 全局操作按钮区域 if (widget.outlineOptions.isNotEmpty && !widget.isGenerating) _buildGlobalActionButtons(context), ], ); } /// 构建全局操作按钮 /// 提供批量操作和主要功能入口 Widget _buildGlobalActionButtons(BuildContext context) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: WebTheme.getEmptyStateColor(context), borderRadius: BorderRadius.circular(12), border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), ), child: Row( children: [ // 重新生成按钮 - 次要操作 OutlinedButton.icon( onPressed: widget.isGenerating || widget.isSaving ? null : () => widget.onRegenerateAll(null), icon: Icon( LucideIcons.refresh_cw, size: 18, color: WebTheme.getTextColor(context), ), label: Text( '重新生成全部', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), style: WebTheme.getSecondaryButtonStyle(context).copyWith( padding: MaterialStateProperty.all( const EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), ), ), const Spacer(), // 保存按钮 - 主要操作 if (widget.selectedOptionId != null) ElevatedButton.icon( onPressed: widget.isGenerating || widget.isSaving ? null : () => _showSaveOptionsDialog(context), icon: widget.isSaving ? SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: WebTheme.getCardColor(context), ), ) : Icon( LucideIcons.save, size: 18, color: WebTheme.getCardColor(context), ), label: Text( widget.isSaving ? '保存中...' : '保存选中的大纲', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: WebTheme.getCardColor(context), ), ), style: WebTheme.getPrimaryButtonStyle(context).copyWith( padding: MaterialStateProperty.all( const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ), ], ), ); } /// 显示保存选项对话框 /// 提供不同的保存方式选择 void _showSaveOptionsDialog(BuildContext context) { showDialog( context: context, builder: (dialogContext) { return AlertDialog( backgroundColor: WebTheme.getCardColor(context), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: Row( children: [ Icon( LucideIcons.save, size: 24, color: WebTheme.getTextColor(context), ), const SizedBox(width: 12), Text( '保存大纲', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '选择保存方式:', style: TextStyle( fontSize: 14, color: WebTheme.getSecondaryTextColor(context), ), ), const SizedBox(height: 16), // 保存选项列表 _buildSaveOption( context, icon: LucideIcons.folder_plus, title: '添加为新章节', subtitle: '在小说末尾添加新章节', onTap: () { Navigator.of(dialogContext).pop(); widget.onSaveOutline(widget.selectedOptionId!, 'NEW_CHAPTER'); }, ), const SizedBox(height: 12), _buildSaveOption( context, icon: LucideIcons.list_plus, title: '插入到现有章节', subtitle: '选择插入位置', onTap: () { Navigator.of(dialogContext).pop(); _showChapterInsertDialog(context); }, ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), style: WebTheme.getSecondaryButtonStyle(context), child: Text( '取消', style: TextStyle( color: WebTheme.getTextColor(context), ), ), ), ], ); }, ); } /// 构建保存选项项目 Widget _buildSaveOption( BuildContext context, { required IconData icon, required String title, required String subtitle, required VoidCallback onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( icon, size: 24, color: WebTheme.getTextColor(context), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), const SizedBox(height: 4), Text( subtitle, style: TextStyle( fontSize: 12, color: WebTheme.getSecondaryTextColor(context), ), ), ], ), ), Icon( LucideIcons.chevron_right, size: 18, color: WebTheme.getSecondaryTextColor(context), ), ], ), ), ); } /// 显示章节插入对话框 /// 选择具体的插入位置 void _showChapterInsertDialog(BuildContext context) { // 获取章节列表 final chapters = context.read().state.chapters; showDialog( context: context, builder: (dialogContext) { return AlertDialog( backgroundColor: WebTheme.getCardColor(context), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), title: Text( '选择插入位置', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), content: SizedBox( width: double.maxFinite, child: ListView.separated( shrinkWrap: true, itemCount: chapters.length, separatorBuilder: (context, index) => Divider( color: WebTheme.getBorderColor(context), height: 1, ), itemBuilder: (context, index) { final chapter = chapters[index]; return ListTile( title: Text( chapter.title, style: TextStyle( fontSize: 14, color: WebTheme.getTextColor(context), ), overflow: TextOverflow.ellipsis, ), subtitle: Text( '插入到此章节后', style: TextStyle( fontSize: 12, color: WebTheme.getSecondaryTextColor(context), ), ), onTap: () { Navigator.of(dialogContext).pop(); widget.onSaveOutline(widget.selectedOptionId!, 'CHAPTER_END'); }, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), style: WebTheme.getSecondaryButtonStyle(context), child: Text( '取消', style: TextStyle( color: WebTheme.getTextColor(context), ), ), ), ], ); }, ); } /// 计算网格列数 /// 基于屏幕宽度的响应式计算 int _calculateCrossAxisCount(double width) { if (width < 600) return 1; // 移动设备:单列 if (width < 900) return 2; // 平板:双列 if (width < 1200) return 3; // 小桌面:三列 return 3; // 大桌面:最多三列,保持卡片适当大小 } }