马良AI写作初始化仓库
This commit is contained in:
1009
AINoval/lib/screens/next_outline/widgets/modern_config_card.dart
Normal file
1009
AINoval/lib/screens/next_outline/widgets/modern_config_card.dart
Normal file
File diff suppressed because it is too large
Load Diff
468
AINoval/lib/screens/next_outline/widgets/modern_result_card.dart
Normal file
468
AINoval/lib/screens/next_outline/widgets/modern_result_card.dart
Normal file
@@ -0,0 +1,468 @@
|
||||
import 'package:ainoval/blocs/next_outline/next_outline_state.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
import '../../../models/user_ai_model_config_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
|
||||
/// 现代化剧情推演结果卡片 - 全局通用组件
|
||||
///
|
||||
/// 此组件负责展示单个剧情推演结果:
|
||||
/// 1. 内容展示 - 显示生成的剧情内容
|
||||
/// 2. 状态指示 - 加载、完成、选中状态
|
||||
/// 3. 操作控件 - 选择、重新生成、保存等
|
||||
/// 4. 交互反馈 - 悬停效果和状态变化
|
||||
///
|
||||
/// 设计特点:
|
||||
/// - 采用纯黑白配色方案,保持视觉一致性
|
||||
/// - 现代化的卡片设计和微交互
|
||||
/// - 清晰的信息层次和操作引导
|
||||
/// - 优化的间距和组件尺寸
|
||||
class ModernResultCard extends StatefulWidget {
|
||||
/// 剧情选项数据
|
||||
final OutlineOptionState option;
|
||||
|
||||
/// 是否被选中
|
||||
final bool isSelected;
|
||||
|
||||
/// AI模型配置列表 - 用于重新生成操作
|
||||
final List<UserAIModelConfigModel> aiModelConfigs;
|
||||
|
||||
/// 选中回调
|
||||
final VoidCallback onSelected;
|
||||
|
||||
/// 重新生成回调
|
||||
final Function(String configId, String? hint) onRegenerateSingle;
|
||||
|
||||
/// 保存回调
|
||||
final Function(String insertType) onSave;
|
||||
|
||||
const ModernResultCard({
|
||||
Key? key,
|
||||
required this.option,
|
||||
this.isSelected = false,
|
||||
required this.aiModelConfigs,
|
||||
required this.onSelected,
|
||||
required this.onRegenerateSingle,
|
||||
required this.onSave,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ModernResultCard> createState() => _ModernResultCardState();
|
||||
}
|
||||
|
||||
/// 结果卡片状态管理
|
||||
///
|
||||
/// 负责:
|
||||
/// 1. 悬停状态管理
|
||||
/// 2. 模型选择状态
|
||||
/// 3. 交互动画控制
|
||||
/// 4. 用户操作处理
|
||||
class _ModernResultCardState extends State<ModernResultCard> {
|
||||
String? _selectedConfigId;
|
||||
bool _isHovering = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 默认选择第一个已验证的模型配置
|
||||
final validatedConfigs = widget.aiModelConfigs
|
||||
.where((config) => config.isValidated)
|
||||
.toList();
|
||||
if (validatedConfigs.isNotEmpty) {
|
||||
_selectedConfigId = validatedConfigs.first.id;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovering = true),
|
||||
onExit: (_) => setState(() => _isHovering = false),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
transform: _isHovering
|
||||
? (Matrix4.identity()..translate(0, -2))
|
||||
: Matrix4.identity(),
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getCardColor(context),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: widget.isSelected
|
||||
? WebTheme.getTextColor(context)
|
||||
: _isHovering
|
||||
? WebTheme.getSecondaryTextColor(context)
|
||||
: WebTheme.getBorderColor(context),
|
||||
width: widget.isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
if (_isHovering || widget.isSelected)
|
||||
BoxShadow(
|
||||
color: WebTheme.getShadowColor(context, opacity: 0.3),
|
||||
blurRadius: widget.isSelected ? 12 : 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题区域
|
||||
_buildTitleSection(context),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: _buildContentSection(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 底部操作区域
|
||||
_buildActionSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标题区域
|
||||
Widget _buildTitleSection(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// 状态指示器
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: widget.option.isGenerating
|
||||
? WebTheme.warning
|
||||
: widget.isSelected
|
||||
? WebTheme.getTextColor(context)
|
||||
: WebTheme.success,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 标题文本
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.option.title ?? '生成中...',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: WebTheme.getTextColor(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 选中指示器
|
||||
if (widget.isSelected)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getTextColor(context),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'已选择',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: WebTheme.getCardColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建内容区域
|
||||
Widget _buildContentSection(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getEmptyStateColor(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: WebTheme.getBorderColor(context),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ValueListenableBuilder<String>(
|
||||
valueListenable: widget.option.contentStreamController,
|
||||
builder: (context, content, child) {
|
||||
// 生成中状态
|
||||
if (content.isEmpty && widget.option.isGenerating) {
|
||||
return _buildLoadingContent(context);
|
||||
}
|
||||
|
||||
// 内容展示
|
||||
return _buildTextContent(context, content);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建加载内容
|
||||
Widget _buildLoadingContent(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'正在生成内容...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建文本内容
|
||||
Widget _buildTextContent(BuildContext context, String content) {
|
||||
return SingleChildScrollView(
|
||||
child: Text(
|
||||
content.isEmpty ? '暂无内容' : content,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 20,
|
||||
height: 2.0,
|
||||
color: content.isEmpty
|
||||
? WebTheme.getSecondaryTextColor(context)
|
||||
: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建操作区域
|
||||
Widget _buildActionSection(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getEmptyStateColor(context),
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: WebTheme.getBorderColor(context),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 模型选择和重新生成
|
||||
Row(
|
||||
children: [
|
||||
// 模型选择下拉框
|
||||
Expanded(
|
||||
child: _buildModelSelector(context),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 重新生成按钮
|
||||
_buildRegenerateButton(context),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 主要操作按钮
|
||||
_buildMainActionButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建模型选择器
|
||||
Widget _buildModelSelector(BuildContext context) {
|
||||
final validatedConfigs = widget.aiModelConfigs
|
||||
.where((config) => config.isValidated)
|
||||
.toList();
|
||||
|
||||
if (validatedConfigs.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getCardColor(context),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: WebTheme.getBorderColor(context),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'无可用模型',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getCardColor(context),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: WebTheme.getBorderColor(context),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedConfigId,
|
||||
items: validatedConfigs.map((config) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: config.id,
|
||||
child: Text(
|
||||
config.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedConfigId = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
isDense: true,
|
||||
isExpanded: true,
|
||||
icon: Icon(
|
||||
LucideIcons.chevron_down,
|
||||
size: 14,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
dropdownColor: WebTheme.getCardColor(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建重新生成按钮
|
||||
Widget _buildRegenerateButton(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: WebTheme.getBorderColor(context),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
LucideIcons.refresh_cw,
|
||||
size: 16,
|
||||
color: widget.option.isGenerating || _selectedConfigId == null
|
||||
? WebTheme.getSecondaryTextColor(context)
|
||||
: WebTheme.getTextColor(context),
|
||||
),
|
||||
tooltip: '重新生成',
|
||||
onPressed: widget.option.isGenerating || _selectedConfigId == null
|
||||
? null
|
||||
: () => widget.onRegenerateSingle(_selectedConfigId!, null),
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建主要操作按钮
|
||||
Widget _buildMainActionButton(BuildContext context) {
|
||||
if (widget.option.isGenerating) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.getEmptyStateColor(context),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'生成中...',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: WebTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: widget.onSelected,
|
||||
style: widget.isSelected
|
||||
? WebTheme.getPrimaryButtonStyle(context).copyWith(
|
||||
backgroundColor: MaterialStateProperty.all(WebTheme.getTextColor(context)),
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
)
|
||||
: WebTheme.getSecondaryButtonStyle(context).copyWith(
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
widget.isSelected ? '已选择' : '选择此大纲',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: widget.isSelected
|
||||
? WebTheme.getCardColor(context)
|
||||
: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,890 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
|
||||
import '../../../models/novel_structure.dart';
|
||||
import '../../../models/user_ai_model_config_model.dart';
|
||||
|
||||
/// 剧情大纲生成配置卡片 - 全局通用组件
|
||||
///
|
||||
/// 此组件负责剧情推演的参数配置功能:
|
||||
/// 1. 章节范围选择 - 确定推演的上下文范围
|
||||
/// 2. AI模型配置 - 选择和管理生成模型
|
||||
/// 3. 生成参数设置 - 选项数量、作者引导等
|
||||
/// 4. 配置验证和错误提示
|
||||
///
|
||||
/// 设计特点:
|
||||
/// - 采用纯黑白配色方案,符合现代简洁审美
|
||||
/// - 响应式布局,适配宽屏和窄屏设备
|
||||
/// - 统一的视觉层次和组件间距
|
||||
/// - 清晰的信息架构和用户引导
|
||||
class OutlineGenerationConfigCard extends StatefulWidget {
|
||||
/// 章节列表
|
||||
final List<Chapter> chapters;
|
||||
|
||||
/// AI模型配置列表
|
||||
final List<UserAIModelConfigModel> aiModelConfigs;
|
||||
|
||||
/// 当前选中的上下文开始章节ID
|
||||
final String? startChapterId;
|
||||
|
||||
/// 当前选中的上下文结束章节ID
|
||||
final String? endChapterId;
|
||||
|
||||
/// 生成选项数量
|
||||
final int numOptions;
|
||||
|
||||
/// 作者引导
|
||||
final String? authorGuidance;
|
||||
|
||||
/// 是否正在生成
|
||||
final bool isGenerating;
|
||||
|
||||
/// 开始章节变更回调
|
||||
final Function(String?) onStartChapterChanged;
|
||||
|
||||
/// 结束章节变更回调
|
||||
final Function(String?) onEndChapterChanged;
|
||||
|
||||
/// 选项数量变更回调
|
||||
final Function(int) onNumOptionsChanged;
|
||||
|
||||
/// 作者引导变更回调
|
||||
final Function(String?) onAuthorGuidanceChanged;
|
||||
|
||||
/// 生成回调
|
||||
final Function(int numOptions, String? authorGuidance, List<String>? selectedConfigIds) onGenerate;
|
||||
|
||||
/// 跳转到添加模型页面的回调
|
||||
final VoidCallback? onNavigateToAddModel;
|
||||
|
||||
/// 跳转到配置特定模型页面的回调
|
||||
final Function(String configId)? onConfigureModel;
|
||||
|
||||
const OutlineGenerationConfigCard({
|
||||
Key? key,
|
||||
required this.chapters,
|
||||
required this.aiModelConfigs,
|
||||
this.startChapterId,
|
||||
this.endChapterId,
|
||||
this.numOptions = 3,
|
||||
this.authorGuidance,
|
||||
this.isGenerating = false,
|
||||
required this.onStartChapterChanged,
|
||||
required this.onEndChapterChanged,
|
||||
required this.onNumOptionsChanged,
|
||||
required this.onAuthorGuidanceChanged,
|
||||
required this.onGenerate,
|
||||
this.onNavigateToAddModel,
|
||||
this.onConfigureModel,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<OutlineGenerationConfigCard> createState() => _OutlineGenerationConfigCardState();
|
||||
}
|
||||
|
||||
class _OutlineGenerationConfigCardState extends State<OutlineGenerationConfigCard> {
|
||||
late int _numOptions;
|
||||
late TextEditingController _authorGuidanceController;
|
||||
List<String> _selectedConfigIds = [];
|
||||
String? _chapterRangeError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_numOptions = widget.numOptions;
|
||||
_authorGuidanceController = TextEditingController(text: widget.authorGuidance);
|
||||
|
||||
// 默认选择第一个模型配置
|
||||
if (widget.aiModelConfigs.isNotEmpty) {
|
||||
_selectedConfigIds = [widget.aiModelConfigs.first.id];
|
||||
}
|
||||
|
||||
// 初始化时验证章节范围
|
||||
_validateChapterRange(widget.startChapterId, widget.endChapterId);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(OutlineGenerationConfigCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.authorGuidance != widget.authorGuidance) {
|
||||
_authorGuidanceController.text = widget.authorGuidance ?? '';
|
||||
}
|
||||
|
||||
if (oldWidget.numOptions != widget.numOptions) {
|
||||
_numOptions = widget.numOptions;
|
||||
}
|
||||
|
||||
// 当起止章节ID变化时验证范围
|
||||
if (oldWidget.startChapterId != widget.startChapterId ||
|
||||
oldWidget.endChapterId != widget.endChapterId) {
|
||||
_validateChapterRange(widget.startChapterId, widget.endChapterId);
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证章节范围,确保开始章节不晚于结束章节
|
||||
void _validateChapterRange(String? startId, String? endId) {
|
||||
setState(() {
|
||||
_chapterRangeError = null;
|
||||
|
||||
if (startId != null && endId != null && widget.chapters.isNotEmpty) {
|
||||
// 查找章节索引
|
||||
int? startIndex;
|
||||
int? endIndex;
|
||||
|
||||
for (int i = 0; i < widget.chapters.length; i++) {
|
||||
if (widget.chapters[i].id == startId) {
|
||||
startIndex = i;
|
||||
}
|
||||
if (widget.chapters[i].id == endId) {
|
||||
endIndex = i;
|
||||
}
|
||||
|
||||
// 如果两个索引都找到了,可以提前结束循环
|
||||
if (startIndex != null && endIndex != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查有效性
|
||||
if (startIndex != null && endIndex != null && startIndex > endIndex) {
|
||||
_chapterRangeError = '起始章节不能晚于结束章节';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_authorGuidanceController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- 新增:根据模型名称获取图标 ---
|
||||
IconData _getIconForModel(String modelName) {
|
||||
final lowerCaseName = modelName.toLowerCase();
|
||||
if (lowerCaseName.contains('gemini')) {
|
||||
return LucideIcons.gem;
|
||||
} else if (lowerCaseName.contains('deepseek')) {
|
||||
return LucideIcons.search_code;
|
||||
} else if (lowerCaseName.contains('gpt') || lowerCaseName.contains('openai')) {
|
||||
return LucideIcons.brain_circuit;
|
||||
} else if (lowerCaseName.contains('beta') || lowerCaseName.contains('test')) {
|
||||
return LucideIcons.flask_conical;
|
||||
} else if (lowerCaseName.contains('flash') || lowerCaseName.contains('fast')) {
|
||||
return LucideIcons.zap;
|
||||
}
|
||||
return LucideIcons.cpu; // 默认图标
|
||||
}
|
||||
// --- 结束新增 ---
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 检查生成按钮是否应该禁用
|
||||
final bool isGenerateButtonDisabled = widget.isGenerating ||
|
||||
_selectedConfigIds.isEmpty ||
|
||||
_chapterRangeError != null;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(32), // 统一的内边距
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 响应式布局判断
|
||||
final isWideScreen = constraints.maxWidth >= 960;
|
||||
|
||||
return isWideScreen
|
||||
? _buildWideLayout(context, isGenerateButtonDisabled, constraints)
|
||||
: _buildNarrowLayout(context, isGenerateButtonDisabled);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 宽屏布局(AI模型配置显示在右侧)
|
||||
/// 充分利用宽屏空间,提供更好的信息组织
|
||||
Widget _buildWideLayout(BuildContext context, bool isGenerateButtonDisabled, BoxConstraints constraints) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 左侧:主要配置区域
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题区域
|
||||
Text(
|
||||
'生成配置',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: WebTheme.getTextColor(context),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 章节配置字段
|
||||
_buildChapterConfigFields(constraints.maxWidth * 0.6, WebTheme.getTextColor(context)),
|
||||
|
||||
// 章节范围验证错误提示
|
||||
if (_chapterRangeError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: WebTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: WebTheme.error.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.circle_alert,
|
||||
size: 16,
|
||||
color: WebTheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_chapterRangeError!,
|
||||
style: TextStyle(
|
||||
color: WebTheme.error,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 作者引导输入区域
|
||||
_buildAuthorGuidanceField(WebTheme.getTextColor(context)),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 生成按钮
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _buildGenerateButton(isGenerateButtonDisabled, WebTheme.getTextColor(context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 40), // 左右区域间距
|
||||
|
||||
// 右侧:AI模型选择区域
|
||||
if (widget.aiModelConfigs.isNotEmpty)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildAIModelSelection(true, WebTheme.getTextColor(context)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 窄屏布局(AI模型配置显示在下方)
|
||||
Widget _buildNarrowLayout(BuildContext context, bool isGenerateButtonDisabled) {
|
||||
final Color primaryColor = Colors.indigo; // 定义主色调为靛蓝色
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'生成选项',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor, // 使用靛蓝色
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildChapterConfigFields(double.infinity, primaryColor),
|
||||
|
||||
if (_chapterRangeError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
_chapterRangeError!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildAuthorGuidanceField(primaryColor),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (widget.aiModelConfigs.isNotEmpty)
|
||||
_buildAIModelSelection(false, primaryColor),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _buildGenerateButton(isGenerateButtonDisabled, primaryColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建章节配置区域
|
||||
Widget _buildChapterConfigFields(double totalWidth, Color primaryColor) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: totalWidth < 600 ? totalWidth : (totalWidth - 32) / 3,
|
||||
child: _buildStartChapterDropdown(primaryColor),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: totalWidth < 600 ? totalWidth : (totalWidth - 32) / 3,
|
||||
child: _buildEndChapterDropdown(primaryColor),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: totalWidth < 600 ? totalWidth : (totalWidth - 32) / 3,
|
||||
child: _buildNumOptionsDropdown(primaryColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建生成按钮
|
||||
Widget _buildGenerateButton(bool isDisabled, Color primaryColor) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: isDisabled
|
||||
? null
|
||||
: () {
|
||||
widget.onGenerate(
|
||||
_numOptions,
|
||||
_authorGuidanceController.text.isEmpty ? null : _authorGuidanceController.text,
|
||||
_selectedConfigIds.isEmpty ? null : _selectedConfigIds,
|
||||
);
|
||||
},
|
||||
icon: widget.isGenerating
|
||||
? SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white, // 加载时图标颜色为白色
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(LucideIcons.brain_circuit, size: 20),
|
||||
label: Text(
|
||||
widget.isGenerating ? '生成中...' : '生成剧情大纲',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor, // 按钮背景色为靛蓝色
|
||||
foregroundColor: Colors.white, // 按钮文字和图标颜色为白色
|
||||
disabledBackgroundColor: primaryColor.withOpacity(0.5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建上下文开始章节下拉框
|
||||
Widget _buildStartChapterDropdown(Color primaryColor) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.book_copy, // 更换图标
|
||||
size: 18,
|
||||
color: primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'上下文开始章节',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: widget.startChapterId,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: primaryColor), // 聚焦时边框颜色
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
items: widget.chapters.map((chapter) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: chapter.id,
|
||||
child: Text(
|
||||
chapter.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
widget.onStartChapterChanged(value);
|
||||
},
|
||||
hint: const Text('选择开始章节'),
|
||||
isExpanded: true,
|
||||
icon: const Icon(LucideIcons.chevron_down, size: 20), // 更换图标
|
||||
dropdownColor: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'选择剧情上下文的起始章节',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建上下文结束章节下拉框
|
||||
Widget _buildEndChapterDropdown(Color primaryColor) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.book_marked, // 更换图标
|
||||
size: 18,
|
||||
color: primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'上下文结束章节',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: widget.endChapterId,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: primaryColor), // 聚焦时边框颜色
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
items: widget.chapters.map((chapter) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: chapter.id,
|
||||
child: Text(
|
||||
chapter.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
widget.onEndChapterChanged(value);
|
||||
},
|
||||
hint: const Text('选择结束章节'),
|
||||
isExpanded: true,
|
||||
icon: const Icon(LucideIcons.chevron_down, size: 20), // 更换图标
|
||||
dropdownColor: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'选择剧情上下文的结束章节',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建生成选项数量下拉框
|
||||
Widget _buildNumOptionsDropdown(Color primaryColor) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.list_ordered, // 更换图标
|
||||
size: 18,
|
||||
color: primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'生成选项数量',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<int>(
|
||||
value: _numOptions,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: primaryColor), // 聚焦时边框颜色
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
items: [2, 3, 4, 5].map((number) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: number,
|
||||
child: Text('$number'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: widget.isGenerating
|
||||
? null
|
||||
: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_numOptions = value;
|
||||
});
|
||||
widget.onNumOptionsChanged(value);
|
||||
}
|
||||
},
|
||||
isExpanded: true,
|
||||
icon: const Icon(LucideIcons.chevron_down, size: 20), // 更换图标
|
||||
dropdownColor: Colors.white,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建作者引导文本框
|
||||
Widget _buildAuthorGuidanceField(Color primaryColor) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.lightbulb, // 更换图标
|
||||
size: 18,
|
||||
color: primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'作者偏好/引导 (可选)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _authorGuidanceController,
|
||||
enabled: !widget.isGenerating,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: primaryColor), // 聚焦时边框颜色
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
hintText: '例如:希望侧重角色A的成长;引入新的反派;避免涉及魔法元素...',
|
||||
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 13),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
style: const TextStyle(height: 1.5),
|
||||
maxLines: 3,
|
||||
onChanged: widget.onAuthorGuidanceChanged,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.info, // 更换图标
|
||||
size: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'告诉 AI 您对下一段剧情的期望或限制',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建AI模型选择器 (列表形式)
|
||||
Widget _buildAIModelSelection(bool isWideScreen, Color primaryColor) {
|
||||
// --- 不再过滤,使用全部模型 ---
|
||||
final allConfigs = widget.aiModelConfigs;
|
||||
|
||||
// 如果模型列表为空
|
||||
if (allConfigs.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.list_checks,
|
||||
size: 18,
|
||||
color: primaryColor.withOpacity(0.6),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'AI 模型选择',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (widget.onNavigateToAddModel != null)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 16),
|
||||
label: const Text('添加模型', style: TextStyle(fontSize: 12)),
|
||||
onPressed: widget.onNavigateToAddModel,
|
||||
style: TextButton.styleFrom(padding: EdgeInsets.zero)
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.info, size: 14, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'没有配置任何模型。请前往设置页面添加模型服务。',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.list_checks,
|
||||
size: 18,
|
||||
color: primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'AI 模型选择',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (widget.onNavigateToAddModel != null)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 16),
|
||||
label: const Text('添加模型', style: TextStyle(fontSize: 12)),
|
||||
onPressed: widget.onNavigateToAddModel,
|
||||
style: TextButton.styleFrom(padding: EdgeInsets.zero)
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 模型列表 - 显示所有,区分已验证和未验证
|
||||
Column(
|
||||
children: allConfigs.map((config) { // <-- 使用全部列表
|
||||
final isSelected = _selectedConfigIds.contains(config.id);
|
||||
final isValidated = config.isValidated;
|
||||
final iconColor = isSelected ? primaryColor : (isValidated ? Colors.grey.shade700 : Colors.grey.shade400);
|
||||
final textColor = isValidated ? Theme.of(context).textTheme.bodyLarge?.color : Colors.grey.shade500;
|
||||
|
||||
// --- 如果已验证 ---
|
||||
if (isValidated) {
|
||||
return CheckboxListTile(
|
||||
title: Text(config.name, style: TextStyle(color: textColor)),
|
||||
value: isSelected,
|
||||
onChanged: widget.isGenerating
|
||||
? null
|
||||
: (selected) {
|
||||
setState(() {
|
||||
if (selected == true) {
|
||||
_selectedConfigIds.add(config.id);
|
||||
} else {
|
||||
_selectedConfigIds.remove(config.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
secondary: Icon(
|
||||
_getIconForModel(config.name),
|
||||
color: iconColor,
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
activeColor: primaryColor,
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
);
|
||||
}
|
||||
// --- 如果未验证 ---
|
||||
else {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
leading: Icon( // 使用 CheckboxListTile 的 secondary 占位,保持对齐
|
||||
_getIconForModel(config.name),
|
||||
color: iconColor,
|
||||
),
|
||||
title: Text(config.name, style: TextStyle(color: textColor, fontStyle: FontStyle.italic)),
|
||||
subtitle: Text('未验证', style: TextStyle(fontSize: 11, color: Colors.orange.shade700)),
|
||||
trailing: OutlinedButton.icon( // 添加配置按钮
|
||||
icon: const Icon(Icons.settings_outlined, size: 14),
|
||||
label: const Text('前往配置', style: TextStyle(fontSize: 11)),
|
||||
onPressed: () {
|
||||
if (widget.onConfigureModel != null) {
|
||||
widget.onConfigureModel!(config.id);
|
||||
}
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
enabled: false, // 整体禁用 ListTile 的交互
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
// 提示信息 (保持不变)
|
||||
if (_selectedConfigIds.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.circle_alert, // 更换图标
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'请至少选择一个 AI 模型',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (_selectedConfigIds.length < _numOptions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.circle_alert, // 更换图标
|
||||
size: 16,
|
||||
color: Colors.amber.shade800,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'注意:部分模型将被重复使用',
|
||||
style: TextStyle(
|
||||
color: Colors.amber.shade800,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
308
AINoval/lib/screens/next_outline/widgets/result_card.dart
Normal file
308
AINoval/lib/screens/next_outline/widgets/result_card.dart
Normal file
@@ -0,0 +1,308 @@
|
||||
import 'package:ainoval/blocs/next_outline/next_outline_state.dart';
|
||||
import '../../../models/novel_structure.dart';
|
||||
import '../../../models/user_ai_model_config_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:animated_text_kit/animated_text_kit.dart';
|
||||
|
||||
/// 结果卡片
|
||||
class ResultCard extends StatefulWidget {
|
||||
/// 剧情选项
|
||||
final OutlineOptionState option;
|
||||
|
||||
/// 是否被选中
|
||||
final bool isSelected;
|
||||
|
||||
/// AI模型配置列表
|
||||
final List<UserAIModelConfigModel> aiModelConfigs;
|
||||
|
||||
/// 选中回调
|
||||
final VoidCallback onSelected;
|
||||
|
||||
/// 重新生成回调
|
||||
final Function(String configId, String? hint) onRegenerateSingle;
|
||||
|
||||
/// 保存回调
|
||||
final Function(String insertType) onSave;
|
||||
|
||||
const ResultCard({
|
||||
Key? key,
|
||||
required this.option,
|
||||
this.isSelected = false,
|
||||
required this.aiModelConfigs,
|
||||
required this.onSelected,
|
||||
required this.onRegenerateSingle,
|
||||
required this.onSave,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ResultCard> createState() => _ResultCardState();
|
||||
}
|
||||
|
||||
class _ResultCardState extends State<ResultCard> {
|
||||
String? _selectedConfigId;
|
||||
bool _isHovering = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 默认选择第一个模型配置
|
||||
if (widget.aiModelConfigs.isNotEmpty) {
|
||||
_selectedConfigId = widget.aiModelConfigs.first.id;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovering = true),
|
||||
onExit: (_) => setState(() => _isHovering = false),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transform: _isHovering
|
||||
? (Matrix4.identity()..translate(0, -4))
|
||||
: Matrix4.identity(),
|
||||
child: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
elevation: _isHovering || widget.isSelected ? 8.0 : 2.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: widget.isSelected
|
||||
? BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)
|
||||
: _isHovering
|
||||
? BorderSide(color: Theme.of(context).colorScheme.primary.withAlpha(128), width: 1.5)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 卡片内容
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_stories,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.option.title ?? '生成中...',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 内容
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: ValueListenableBuilder<String>(
|
||||
valueListenable: widget.option.contentStreamController,
|
||||
builder: (context, content, child) {
|
||||
if (content.isEmpty && widget.option.isGenerating) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'正在生成内容...',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: AnimatedTextKit(
|
||||
animatedTexts: [
|
||||
TypewriterAnimatedText(
|
||||
content,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
height: 1.6,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
speed: const Duration(milliseconds: 40),
|
||||
),
|
||||
],
|
||||
isRepeatingAnimation: false,
|
||||
displayFullTextOnTap: true,
|
||||
key: ValueKey(widget.option.optionId + content),
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 底部操作区
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 模型选择下拉框
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedConfigId,
|
||||
items: widget.aiModelConfigs
|
||||
.where((config) => config.isValidated)
|
||||
.map((config) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: config.id,
|
||||
child: Text(
|
||||
config.name,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
if (widget.aiModelConfigs.any((c) => c.isValidated && c.id == value)) {
|
||||
setState(() {
|
||||
_selectedConfigId = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
isDense: true,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// 重新生成按钮
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
tooltip: '使用选定模型重新生成',
|
||||
onPressed: widget.option.isGenerating || _selectedConfigId == null
|
||||
? null
|
||||
: () => widget.onRegenerateSingle(_selectedConfigId!, null),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// 选择按钮
|
||||
ElevatedButton(
|
||||
onPressed: widget.option.isGenerating
|
||||
? null
|
||||
: widget.onSelected,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.white,
|
||||
foregroundColor: widget.isSelected
|
||||
? Colors.white
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(
|
||||
color: widget.isSelected
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
elevation: widget.isSelected ? 2 : 0,
|
||||
),
|
||||
child: Text(
|
||||
widget.isSelected ? '已选择' : '选择此大纲',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 加载遮罩
|
||||
if (widget.option.isGenerating)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
612
AINoval/lib/screens/next_outline/widgets/results_grid.dart
Normal file
612
AINoval/lib/screens/next_outline/widgets/results_grid.dart
Normal file
@@ -0,0 +1,612 @@
|
||||
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<OutlineOptionState> outlineOptions;
|
||||
|
||||
/// 当前选中的剧情选项ID
|
||||
final String? selectedOptionId;
|
||||
|
||||
/// AI模型配置列表 - 用于重新生成操作
|
||||
final List<UserAIModelConfigModel> 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<ResultsGrid> createState() => _ResultsGridState();
|
||||
}
|
||||
|
||||
/// 结果网格状态管理
|
||||
///
|
||||
/// 负责:
|
||||
/// 1. 本地状态管理(重新生成提示等)
|
||||
/// 2. 响应式布局计算
|
||||
/// 3. 用户交互处理
|
||||
/// 4. 对话框和弹窗管理
|
||||
class _ResultsGridState extends State<ResultsGrid> {
|
||||
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<NextOutlineBloc>().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; // 大桌面:最多三列,保持卡片适当大小
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user