import 'dart:async'; import 'dart:math'; import 'dart:convert'; // import 'dart:html' as html; import 'package:ainoval/screens/editor/controllers/editor_screen_controller.dart'; import 'package:flutter/gestures.dart'; import 'package:ainoval/blocs/editor/editor_bloc.dart' as editor_bloc; import 'package:ainoval/blocs/setting/setting_bloc.dart'; import 'package:ainoval/utils/quill_helper.dart'; import 'package:ainoval/screens/editor/widgets/selection_toolbar.dart'; import 'package:ainoval/screens/editor/widgets/ai_generation_toolbar.dart'; import 'package:ainoval/utils/web_theme.dart'; import 'package:ainoval/utils/setting_reference_processor.dart'; import 'package:ainoval/utils/ai_generated_content_processor.dart'; import 'package:ainoval/services/api_service/repositories/universal_ai_repository.dart'; import 'package:ainoval/services/api_service/base/api_exception.dart'; import 'package:ainoval/models/ai_request_models.dart'; import 'package:ainoval/models/unified_ai_model.dart'; import 'package:ainoval/models/scene_beat_data.dart'; import 'package:ainoval/screens/editor/components/text_generation_dialogs.dart'; import 'package:ainoval/models/novel_structure.dart'; import 'package:ainoval/widgets/common/top_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:ainoval/utils/word_count_analyzer.dart'; import 'package:provider/provider.dart'; import 'package:ainoval/screens/editor/managers/editor_layout_manager.dart'; import 'package:ainoval/screens/editor/widgets/menu_builder.dart'; import 'package:ainoval/screens/editor/widgets/setting_reference_hover.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ainoval/widgets/common/setting_preview_manager.dart'; import 'package:ainoval/models/novel_structure.dart' as novel_models; import 'package:ainoval/models/novel_setting_item.dart'; import 'package:ainoval/models/setting_group.dart'; import 'package:ainoval/models/novel_snippet.dart'; // import 'package:ainoval/models/user_ai_model_config_model.dart'; import 'package:ainoval/models/editor_settings.dart'; // import 'package:ainoval/models/public_model_config.dart'; import 'package:ainoval/widgets/editor/overlay_scene_beat_manager.dart'; import 'package:ainoval/blocs/credit/credit_bloc.dart'; /// 场景编辑器组件,用于编辑小说中的单个场景 /// /// [title] 场景标题 /// [wordCount] 场景字数统计 /// [isActive] 当前场景是否处于激活状态 /// [actId] 所属篇章ID /// [chapterId] 所属章节ID /// [sceneId] 场景ID /// [isFirst] 是否为章节中的第一个场景 /// [sceneIndex] 场景在章节中的序号,从1开始 /// [controller] 场景内容编辑控制器 /// [summaryController] 场景摘要编辑控制器 /// [editorBloc] 编辑器状态管理 /// [onContentChanged] 内容变更回调 class SceneEditor extends StatefulWidget { const SceneEditor({ super.key, required this.title, required this.wordCount, required this.isActive, this.actId, this.chapterId, this.sceneId, this.isFirst = true, this.sceneIndex, // 添加场景序号参数 required this.controller, required this.summaryController, required this.editorBloc, this.onContentChanged, // 添加回调函数 this.isVisuallyNearby = true, // 新增参数,默认为true以保持当前行为 // 🚀 新增:SelectionToolbar数据参数 this.novel, this.settings = const [], this.settingGroups = const [], this.snippets = const [], // 编辑器设置 this.editorSettings, }); final String title; final int wordCount; final bool isActive; final String? actId; final String? chapterId; final String? sceneId; final bool isFirst; final int? sceneIndex; // 场景在章节中的序号,从1开始 final QuillController controller; final TextEditingController summaryController; final editor_bloc.EditorBloc editorBloc; // 添加内容变更回调 final Function(String content, int wordCount, {bool syncToServer})? onContentChanged; final bool isVisuallyNearby; // 新增参数声明 // 🚀 新增:SelectionToolbar数据参数 final novel_models.Novel? novel; final List settings; final List settingGroups; final List snippets; // 编辑器设置 final EditorSettings? editorSettings; @override State createState() => _SceneEditorState(); } class _SceneEditorState extends State with AutomaticKeepAliveClientMixin { final FocusNode _focusNode = FocusNode(); Timer? _debounceTimer; bool _isFocused = false; // 为编辑器创建一个Key late final Key _editorKey; // 内容更新防抖定时器 Timer? _contentDebounceTimer; final FocusNode _summaryFocusNode = FocusNode(); bool _isSummaryFocused = false; // 焦点防抖定时器 Timer? _focusDebounceTimer; // 🚀 新增:活动状态设置防抖定时器 Timer? _activeStateDebounceTimer; // 🚀 新增:记录最后设置的活动状态,避免重复设置 String? _lastSetActiveActId; String? _lastSetActiveChapterId; String? _lastSetActiveSceneId; // 添加文本选择工具栏相关变量 bool _showToolbar = false; final LayerLink _toolbarLayerLink = LayerLink(); int _selectedTextWordCount = 0; Timer? _selectionDebounceTimer; bool _showToolbarAbove = false; // 默认在选区下方显示,简化计算 final GlobalKey _editorContentKey = GlobalKey(); // 编辑器内容区域的key // 🚀 AI工具栏相关状态 bool _showAIToolbar = false; final LayerLink _aiToolbarLayerLink = LayerLink(); bool _isAIGenerating = false; String _aiModelName = ''; String _generatedText = ''; int _aiGeneratedWordCount = 0; int _currentStreamIndex = 0; int _lastInsertedOffset = 0; int _aiGeneratedStartOffset = 0; // 🚀 新增:流式生成批量插入缓冲 String _pendingStreamText = ''; // 🚀 新增:用于保存重试信息的变量 UniversalAIRequest? _lastAIRequest; // 已移除:UserAIModelConfigModel? _lastAIModel; 现在使用_lastUnifiedModel String? _lastSelectedText; // 🚀 新增:保存统一模型信息(包含isPublic状态) UnifiedAIModel? _lastUnifiedModel; // 添加防抖处理 String _pendingContent = ''; String _lastSavedContent = ''; // 添加最后保存的内容,用于比较变化 DateTime _lastChangeTime = DateTime.now(); // 添加最后变更时间 int _pendingWordCount = 0; Timer? _syncTimer; final int _minorChangeThreshold = 5; // 定义微小改动的字符数阈值 // 添加内容变化标志,用于在dispose时判断是否需要强制保存 bool _hasUnsavedChanges = false; // 🚀 新增:设定引用处理状态标志,避免样式变化触发保存 bool _isProcessingSettingReferences = false; int _lastSettingHash = 0; // 简单文本哈希,避免重复处理 // 🚀 新增:AI生成状态标志,避免生成过程中触发保存 // 添加滚动控制器,用于工具栏定位 late final ScrollController _editorScrollController; // 设定引用处理相关 Timer? _settingReferenceProcessTimer; String _lastProcessedText = ''; String _lastProcessedDeltaContent = ''; // 上次处理的完整Delta内容 DateTime _lastProcessingTime = DateTime(2000); // 上次处理时间 static const Duration _minProcessingInterval = Duration(milliseconds: 1000); // 最小处理间隔 // 🚀 新增:摘要组件滚动固定相关变量 final GlobalKey _sceneContainerKey = GlobalKey(); // 场景容器的key final GlobalKey _summaryKey = GlobalKey(); // 摘要组件的key // 使用 ValueNotifier 代替频繁 setState final ValueNotifier _summaryTopOffsetVN = ValueNotifier(0.0); // 摘要Y偏移 bool _isSummarySticky = false; // 摘要是否处于sticky状态 Timer? _scrollPositionTimer; // 滚动位置更新定时器 ScrollController? _parentScrollController; // 父级滚动控制器 // 🚀 新增:流畅滚动优化变量 double _lastCalculatedOffset = 0.0; // 上次计算的偏移量 bool _lastStickyState = false; // 上次的sticky状态 double _summaryHeight = 200.0; // 摘要组件的实际高度,默认200px static const double _positionThreshold = 2.0; // 位置变化阈值,减少闪烁 // 🚀 新增:粘性滚动控制变量 static const double _minSceneHeightForSticky = 400.0; // 最小场景高度,低于此高度不启用粘性 static const double _summaryTopMargin = 16.0; // 摘要顶部边距 static const double _summaryBottomMargin = 24.0; // 摘要底部边距 static const double _bottomToolbarHeight = 40.0; // 🚀 新增:底部工具栏预留高度 // 🚀 新增:LayerLink目标的GlobalKey,用于工具栏检测位置 final GlobalKey _toolbarTargetKey = GlobalKey(); // 🚀 新增:AI生成状态标志,避免生成过程中触发保存 // 添加一个延迟初始化标志 bool _isEditorFullyInitialized = false; Timer? _streamingTimer; // ==================== Controller listeners管理 ==================== StreamSubscription? _docChangeSub; // 监听 document.changes 的订阅,便于在 controller 切换时取消 /// 获取当前小说ID String? _getNovelId() { final editorBloc = widget.editorBloc; if (editorBloc.state is editor_bloc.EditorLoaded) { final state = editorBloc.state as editor_bloc.EditorLoaded; return state.novel.id; } return null; } @override void initState() { super.initState(); // 修改初始化Key的方式,确保唯一性 final String sceneId = widget.sceneId ?? (widget.actId != null && widget.chapterId != null ? '${widget.actId}_${widget.chapterId}' : widget.title.replaceAll(' ', '_').toLowerCase()); // 使用ValueKey代替GlobalObjectKey _editorKey = ValueKey('editor_$sceneId'); // 初始化滚动控制器 _editorScrollController = ScrollController(); // 监听焦点变化 _focusNode.addListener(_onEditorFocusChange); _summaryFocusNode.addListener(_onSummaryFocusChange); // 添加控制器内容监听器(保存订阅以便后续取消) _docChangeSub = widget.controller.document.changes.listen(_onDocumentChange); // 添加文本选择变化监听 widget.controller.addListener(_handleSelectionChange); // 监听EditorBloc状态变化,确保摘要控制器内容与模型保持同步 _setupBlocListener(); // 监听设定状态变化,处理设定引用 _setupSettingBlocListener(); // 监听内容加载完成,重新处理设定引用 _setupContentLoadListener(); // 初始化最后保存的内容(纯文本用于比较) _lastSavedContent = widget.controller.document.toPlainText(); // 🚀 新增:设置摘要滚动固定监听 _setupSummaryScrollListener(); // 延迟完整初始化,优先显示基础UI WidgetsBinding.instance.addPostFrameCallback((_) { // 在渲染完成后再初始化复杂功能 Future.microtask(() { if (mounted) { setState(() { _isEditorFullyInitialized = true; }); // 🎯 简化:直接处理设定引用,不再等待DOM AppLogger.i('SceneEditor', '🎯 开始设定引用处理: ${widget.sceneId}'); //_checkAndProcessSettingReferences(); // 🚀 新增:初始化摘要位置 _updateSummaryPosition(); } }); }); } void _onEditorFocusChange() { // 使用节流控制焦点更新频率 _focusDebounceTimer?.cancel(); _focusDebounceTimer = Timer(const Duration(milliseconds: 100), () { if (mounted) { final newFocusState = _focusNode.hasFocus; // 仅当焦点状态真正改变时更新状态 if (_isFocused != newFocusState) { setState(() { _isFocused = newFocusState; // 🎯 当编辑器获得焦点时,处理设定引用(使用防抖) if (_isFocused && !_isProcessingSettingReferences) { ////AppLogger.d('SceneEditor', '📝 编辑器获得焦点,处理设定引用: ${widget.sceneId}'); _processSettingReferencesDebounced(); } // 🚀 优化:只有当获得焦点且确实需要改变活动状态时才设置活动元素 if (_isFocused && widget.actId != null && widget.chapterId != null) { // 检查当前是否已经是活动状态 final editorBloc = widget.editorBloc; if (editorBloc.state is editor_bloc.EditorLoaded) { final state = editorBloc.state as editor_bloc.EditorLoaded; final isAlreadyActive = state.activeActId == widget.actId && state.activeChapterId == widget.chapterId && state.activeSceneId == widget.sceneId; // 只有当不是活动状态时才设置 if (!isAlreadyActive) { _setActiveElementsQuietly(); } // 如果场景节拍面板已显示且当前场景有sceneId,则切换到当前场景 if (widget.sceneId != null && OverlaySceneBeatManager.instance.isVisible && OverlaySceneBeatManager.instance.currentSceneId != widget.sceneId) { AppLogger.i('SceneEditor', '🔄 场景获得焦点,切换场景节拍面板到: ${widget.sceneId}'); OverlaySceneBeatManager.instance.switchScene(widget.sceneId!); } } else { // 状态不明确时才设置 _setActiveElementsQuietly(); } } }); } } }); } void _onSummaryFocusChange() { // 使用节流控制焦点更新频率 _focusDebounceTimer?.cancel(); _focusDebounceTimer = Timer(const Duration(milliseconds: 100), () { if (mounted) { final newFocusState = _summaryFocusNode.hasFocus; // 仅当焦点状态真正改变时更新状态 if (_isSummaryFocused != newFocusState) { setState(() { _isSummaryFocused = newFocusState; // 🚀 优化:只有当获得焦点且确实需要改变活动状态时才设置活动元素 if (_isSummaryFocused && widget.actId != null && widget.chapterId != null) { // 检查当前是否已经是活动状态 final editorBloc = widget.editorBloc; if (editorBloc.state is editor_bloc.EditorLoaded) { final state = editorBloc.state as editor_bloc.EditorLoaded; final isAlreadyActive = state.activeActId == widget.actId && state.activeChapterId == widget.chapterId && state.activeSceneId == widget.sceneId; // 只有当不是活动状态时才设置 if (!isAlreadyActive) { _setActiveElementsQuietly(); } } else { // 状态不明确时才设置 _setActiveElementsQuietly(); } } }); } } }); } // 设置活动元素 - 原始方法 void _setActiveElements() { if (widget.actId != null && widget.chapterId != null) { widget.editorBloc.add( editor_bloc.SetActiveChapter(actId: widget.actId!, chapterId: widget.chapterId!)); if (widget.sceneId != null) { widget.editorBloc.add(editor_bloc.SetActiveScene( actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!)); } } } // 设置活动元素但不触发滚动 - 适用于编辑中场景(优化版) void _setActiveElementsQuietly() { if (widget.actId != null && widget.chapterId != null) { // 🚀 优化:检查是否与上次设置的状态相同,避免重复设置 final bool isSameAsLastSet = _lastSetActiveActId == widget.actId && _lastSetActiveChapterId == widget.chapterId && _lastSetActiveSceneId == widget.sceneId; if (isSameAsLastSet) { AppLogger.v('SceneEditor', '跳过设置活动状态:与上次设置相同 ${widget.actId}/${widget.chapterId}/${widget.sceneId}'); return; } // 🚀 使用防抖机制,避免短时间内频繁设置 _activeStateDebounceTimer?.cancel(); _activeStateDebounceTimer = Timer(const Duration(milliseconds: 100), () { if (!mounted) return; // 直接使用BlocProvider获取EditorBloc实例 final editorBloc = widget.editorBloc; // 检查当前活动状态,避免重复设置相同的活动元素 if (editorBloc.state is editor_bloc.EditorLoaded) { final state = editorBloc.state as editor_bloc.EditorLoaded; // 只有当活动元素确实需要变化时才发出事件 final needsToUpdateAct = state.activeActId != widget.actId; final needsToUpdateChapter = state.activeChapterId != widget.chapterId; final needsToUpdateScene = widget.sceneId != null && state.activeSceneId != widget.sceneId; if (needsToUpdateAct || needsToUpdateChapter) { ////AppLogger.d('SceneEditor', '设置活动章节: ${widget.actId}/${widget.chapterId}'); editorBloc.add(editor_bloc.SetActiveChapter( actId: widget.actId!, chapterId: widget.chapterId!, silent: true, // 🚀 使用静默模式,避免触发大范围UI刷新 )); // 🚀 记录已设置的状态 _lastSetActiveActId = widget.actId; _lastSetActiveChapterId = widget.chapterId; } if (needsToUpdateScene && widget.sceneId != null) { ////AppLogger.d('SceneEditor', '设置活动场景: ${widget.sceneId}'); editorBloc.add(editor_bloc.SetActiveScene( actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, silent: true, // 🚀 使用静默模式,避免触发大范围UI刷新 )); // 🚀 记录已设置的场景状态 _lastSetActiveSceneId = widget.sceneId; } } else { // 如果状态不是EditorLoaded,则使用原始方法 _setActiveElements(); // 🚀 记录已设置的状态 _lastSetActiveActId = widget.actId; _lastSetActiveChapterId = widget.chapterId; _lastSetActiveSceneId = widget.sceneId; } }); } } // 监听文档变化 void _onDocumentChange(DocChange change) { if (!mounted) return; // 🚫 生成期间:跳过文档变更的重处理(编码/过滤/保存) if (_isAIGenerating) { AppLogger.v('SceneEditor', '⏭️ 生成中,跳过文档变更处理: ${widget.sceneId}'); return; } // 🚀 关键修复:检查变化是否来源于设定引用样式应用 final currentText = widget.controller.document.toPlainText(); final currentDeltaJson = jsonEncode(widget.controller.document.toDelta().toJson()); // 🎯 新增:如果完整内容相等且正在处理设定引用,直接跳过 if (currentDeltaJson == _lastProcessedDeltaContent && _isProcessingSettingReferences) { AppLogger.v('SceneEditor', '⏭️ 场景内容完全相等且正在处理设定引用,跳过保存'); return; } // 如果是样式变化且文本内容没有变化,则不触发保存 if (currentText == _lastSavedContent && _isProcessingSettingReferences) { AppLogger.v('SceneEditor', '⏭️ 设定引用样式应用不触发保存'); return; } // 🎯 新增:检查是否仅为样式变化(不是文本内容变化) if (_isOnlyStyleChange(change) && _isProcessingSettingReferences) { AppLogger.v('SceneEditor', '⏭️ 仅样式变化且正在处理设定引用,跳过'); return; } // 🚀 修复关键问题:提取包含样式信息的完整Delta格式 // 不再使用 toPlainText() 因为它会丢失所有样式属性 final rawDeltaJson = currentDeltaJson; // 复用已计算的Delta JSON // 🧹 过滤设定引用相关的自定义样式,但保留其他样式(如粗体、斜体、下划线等) // 🎯 重新启用过滤,确保保存时不包含设定引用样式 final filteredDeltaJson = SettingReferenceProcessor.filterSettingReferenceStyles(rawDeltaJson, caller: '_onDocumentChange'); //////AppLogger.d('SceneEditor', '文档变化 - 过滤后保存Delta格式,原始长度: ${rawDeltaJson.length}, 过滤后长度: ${filteredDeltaJson.length}'); // 使用防抖动机制,避免频繁发送保存请求 _contentDebounceTimer?.cancel(); _contentDebounceTimer = Timer(const Duration(milliseconds: 800), () { // 延长为800毫秒防抖,更好地应对快速输入 _onTextChanged(filteredDeltaJson); }); // 🎯 优化:只在真正的文本内容变化时才处理设定引用 if (currentText != _lastSavedContent && !_isProcessingSettingReferences && currentDeltaJson != _lastProcessedDeltaContent) { // 延迟处理设定引用,避免在文档变化处理过程中立即触发 Timer(const Duration(milliseconds: 100), () { if (mounted) { _checkAndProcessSettingReferences(); } }); } } // 🎯 新增:检查是否仅为样式变化 bool _isOnlyStyleChange(DocChange change) { try { // 检查变化是否只涉及格式化而不涉及文本插入/删除 if (change.change.operations.every((op) { // 如果是retain操作且有attributes,说明是样式变化 if (op.key == 'retain' && op.attributes != null) { return true; } // 如果是insert操作但插入的是空字符串且有attributes,也是样式变化 if (op.key == 'insert' && op.data is String && (op.data as String).isEmpty && op.attributes != null) { return true; } return false; })) { return true; } return false; } catch (e) { AppLogger.w('SceneEditor', '检查样式变化失败', e); return false; } } // 添加防抖处理 void _onTextChanged(String content) { // 🚫 生成期间不进行保存与过滤,等待用户"应用/丢弃"后再处理 if (_isAIGenerating) { AppLogger.v('SceneEditor', '⏭️ 生成中,跳过_onTextChanged: ${widget.sceneId}'); return; } // 🚀 修复:避免在设定引用处理时触发保存 if (_isProcessingSettingReferences) { AppLogger.v('SceneEditor', '🛑 设定引用处理中,跳过保存: ${widget.sceneId}'); return; } // 🚫 如果文本内容未发生变化,直接跳过后续处理,防止重复保存 final String currentPlainText = QuillHelper.deltaToText(content); if (currentPlainText == _lastSavedContent) { AppLogger.v('SceneEditor', '⏭️ 文本内容与最后保存内容一致,跳过保存: ${widget.sceneId}'); return; } // 🆕 新增:如果有隐藏文本,使用过滤后的内容进行保存 if (AIGeneratedContentProcessor.hasAnyHiddenText(controller: widget.controller)) { AppLogger.v('SceneEditor', '🫥 检测到隐藏文本,使用过滤后的内容保存: ${widget.sceneId}'); // 使用过滤掉隐藏文本的内容 content = AIGeneratedContentProcessor.getVisibleDeltaJsonOnly(controller: widget.controller); } // 🚀 修复:现在接收的是Delta JSON格式,包含完整样式信息 // 先提取纯文本用于字数统计和变化检测 final plainText = currentPlainText; final wordCount = WordCountAnalyzer.countWords(plainText); // 判断是否为微小改动(基于纯文本比较) final bool isMinorChange = _isMinorTextChange(plainText); // 记录变动信息 AppLogger.v('SceneEditor', '文本变更 - Delta长度: ${content.length}, 字数: $wordCount, 是否微小改动: $isMinorChange'); // 保存到本地变量,避免立即更新 _pendingContent = content; // 🚀 现在保存的是包含样式的Delta JSON _pendingWordCount = wordCount; _lastChangeTime = DateTime.now(); // 触发设定引用处理 _checkAndProcessSettingReferences(); // 标记有未保存的更改(基于纯文本比较) _hasUnsavedChanges = true; // 🚀 新增:通过正则快速检测Delta JSON中是否仍包含 AI 临时属性,避免漏判 final bool hasTempAIMarks = content.contains('"ai-generated"') || content.contains('"hidden-text"'); // 只有在内容实际发生变化且没有临时标记时,才发送 UpdateSceneContent 事件 if (widget.actId != null && widget.chapterId != null && widget.sceneId != null && !hasTempAIMarks) { // 🧹 确保保存时过滤设定引用样式,避免保存临时样式 final filteredContent = SettingReferenceProcessor.filterSettingReferenceStylesForSave(_pendingContent, caller: '_onTextChanged'); widget.editorBloc.add( editor_bloc.UpdateSceneContent( novelId: widget.editorBloc.novelId, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, content: filteredContent, wordCount: _pendingWordCount.toString(), isMinorChange: isMinorChange, // 传递是否为微小改动的标志 ), ); } else { // 如果有临时标记,记录日志并完全跳过该事件,避免任何远端保存 AppLogger.v('SceneEditor', '🚫 存在临时标记,跳过 UpdateSceneContent: ${widget.sceneId}'); } // 无论是否为微小改动,都更新最后保存的内容(纯文本用于比较) _lastSavedContent = plainText; // 重置防抖计时器 - 连续输入时只触发一次保存 if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel(); _debounceTimer = Timer(const Duration(seconds: 2), () { // 等待2秒再保存本地,这样可以减少本地保存频率 _saveLocalOnly(); }); // 设置同步计时器 - 每5分钟同步一次到服务器,仅当存在未保存更改时 if (_syncTimer == null || !_syncTimer!.isActive) { _syncTimer = Timer(const Duration(minutes: 5), () { if (_hasUnsavedChanges) { _syncToServer(); } }); } } // 检测是否为微小文本改动 bool _isMinorTextChange(String plainText) { if (_lastSavedContent.isEmpty) return false; // 1. 检查变化的字符数 final int lengthDiff = (plainText.length - _lastSavedContent.length).abs(); // 2. 计算编辑距离 (简化版 - 仅考虑长度变化) // 对于完整的编辑距离(Levenshtein)需要更复杂的算法,这里简化处理 final int editDistance = min(lengthDiff, _minorChangeThreshold + 1); // 3. 检查时间间隔 (如果刚刚保存过,更可能是微小改动) final timeSinceLastChange = DateTime.now().difference(_lastChangeTime); final bool isRecentChange = timeSinceLastChange < const Duration(seconds: 3); // 4. 综合判断 (字符变化很小,或者最近刚改过且变化不大) final bool isMinor = editDistance <= _minorChangeThreshold || (isRecentChange && editDistance <= _minorChangeThreshold * 2); AppLogger.v('SceneEditor', '变更分析 - 字符差异: $lengthDiff, 编辑距离: $editDistance, 时间间隔: ${timeSinceLastChange.inMilliseconds}ms, 判定为${isMinor ? "微小" : "重要"}改动'); return isMinor; } // 保存到本地 void _saveLocalOnly() { // 🚫 避免在AI生成过程中保存含有临时标记的内容 if (_pendingContent.contains('"ai-generated"') || _pendingContent.contains('"hidden-text"')) { AppLogger.v('SceneEditor', '🚫 _saveLocalOnly 检测到临时AI标记,跳过本地保存: \\${widget.sceneId}'); return; } if (widget.actId != null && widget.chapterId != null && widget.sceneId != null) { // 🧹 本地保存时过滤设定引用样式,避免保存临时样式 final filteredContent = SettingReferenceProcessor.filterSettingReferenceStylesForSave(_pendingContent, caller: '_saveLocalOnly'); // 直接调用EditorBloc保存,不触发同步 widget.editorBloc.add( editor_bloc.SaveSceneContent( novelId: widget.editorBloc.novelId, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, content: filteredContent, wordCount: _pendingWordCount.toString(), localOnly: true, // 仅保存到本地 ), ); // 更新最后保存的内容(保存纯文本用于比较) _lastSavedContent = QuillHelper.deltaToText(_pendingContent); } else if (widget.onContentChanged != null) { // 🧹 本地保存时过滤设定引用样式,避免保存临时样式 final filteredContent = SettingReferenceProcessor.filterSettingReferenceStylesForSave(_pendingContent, caller: '_saveLocalOnly_callback'); // 如果提供了回调,使用回调函数 widget.onContentChanged!(filteredContent, _pendingWordCount, syncToServer: false); // 更新最后保存的内容(保存纯文本用于比较) _lastSavedContent = QuillHelper.deltaToText(_pendingContent); } } // 同步到服务器 void _syncToServer() { // 🚫 如果仍包含 AI 临时标记(ai-generated/hidden-text),直接跳过远端同步,避免在生成过程中保存至后端 if (_pendingContent.contains('"ai-generated"') || _pendingContent.contains('"hidden-text"')) { AppLogger.v('SceneEditor', '🚫 存在 AI 临时标记,跳过 _syncToServer'); // 仍然保留 _hasUnsavedChanges = true ,这样在 Apply 之后可以正常同步 return; } if (widget.actId != null && widget.chapterId != null && widget.sceneId != null) { // 🧹 同步到服务器时过滤设定引用样式,避免保存临时样式 final filteredContent = SettingReferenceProcessor.filterSettingReferenceStylesForSave(_pendingContent, caller: '_syncToServer'); // 使用EditorBloc同步到服务器 widget.editorBloc.add( editor_bloc.SaveSceneContent( novelId: widget.editorBloc.novelId, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, content: filteredContent, wordCount: _pendingWordCount.toString(), localOnly: false, // 同步到服务器 ), ); // 更新最后保存的内容(保存纯文本用于比较) _lastSavedContent = QuillHelper.deltaToText(_pendingContent); } else if (widget.onContentChanged != null) { // 🧹 同步到服务器时过滤设定引用样式,避免保存临时样式 final filteredContent = SettingReferenceProcessor.filterSettingReferenceStylesForSave(_pendingContent, caller: '_syncToServer_callback'); // 如果提供了回调,使用回调函数 widget.onContentChanged!(filteredContent, _pendingWordCount, syncToServer: true); // 更新最后保存的内容(保存纯文本用于比较) _lastSavedContent = QuillHelper.deltaToText(_pendingContent); } } // 处理文本选择变化 void _handleSelectionChange() { // 若选区变化太快,跳过更新 final selection = widget.controller.selection; if (selection.isCollapsed) { // 如果没有选择文本,隐藏工具栏 if (_showToolbar) { setState(() { _showToolbar = false; _selectedTextWordCount = 0; }); } return; } // 使用更高效的节流控制 _selectionDebounceTimer?.cancel(); _selectionDebounceTimer = Timer(const Duration(milliseconds: 250), () { if (!mounted) return; // 高效判断是否需要更新界面 final selectedText = widget.controller.document .getPlainText(selection.start, selection.end - selection.start); final wordCount = WordCountAnalyzer.countWords(selectedText); // 仅当选择内容与上次不同时才更新 if (!_showToolbar || _selectedTextWordCount != wordCount) { setState(() { _showToolbar = true; _selectedTextWordCount = wordCount; // 简化位置计算,使用固定位置 _showToolbarAbove = false; }); // 🚀 关键修复:选择区域变化时,强制重新构建LayerLink目标 ////AppLogger.d('SceneEditor', '🎯 选择区域变化,触发LayerLink目标重新定位'); // 🚀 强制触发下一帧重新构建,确保LayerLink目标位置更新 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { ////AppLogger.d('SceneEditor', '🔄 强制重新构建LayerLink目标位置'); setState(() { // 这个setState专门用于强制重新构建LayerLink目标 }); } }); } }); } // // 简化的选区矩形计算 // Rect _calculateSelectionRect() { // try { // // 获取编辑器渲染对象 // final RenderBox? editorBox = // _editorContentKey.currentContext?.findRenderObject() as RenderBox?; // if (editorBox == null) return Rect.zero; // // 获取编辑器全局坐标 // final editorOffset = editorBox.localToGlobal(Offset.zero); // final editorWidth = editorBox.size.width; // // 创建一个固定位置,避免复杂计算 // return Rect.fromLTWH( // editorWidth * 0.5 - 50, // 水平居中偏左 // 50, // 固定在顶部下方50像素 // 100, // 固定宽度 // 30, // 固定高度 // ); // } catch (e) { // return Rect.zero; // } // } @override void dispose() { // 页面关闭前确保同步到服务器 _debounceTimer?.cancel(); _syncTimer?.cancel(); _settingReferenceProcessTimer?.cancel(); // 取消设定引用处理定时器 _scrollPositionTimer?.cancel(); // 🚀 取消摘要位置更新定时器 // 强制保存未保存的更改 if (_hasUnsavedChanges && widget.actId != null && widget.chapterId != null && widget.sceneId != null && _pendingContent.isNotEmpty) { AppLogger.i('SceneEditor', '组件销毁前强制保存场景内容: ${widget.sceneId}'); // 🧹 确保保存前过滤设定引用样式 final filteredContent = SettingReferenceProcessor.filterSettingReferenceStyles(_pendingContent, caller: 'dispose'); // 获取当前摘要内容 final currentSummary = widget.summaryController.text; // 立即触发强制保存事件 widget.editorBloc.add( editor_bloc.ForceSaveSceneContent( novelId: widget.editorBloc.novelId, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, content: filteredContent, wordCount: _pendingWordCount.toString(), summary: currentSummary.isNotEmpty ? currentSummary : null, ), ); AppLogger.i('SceneEditor', '强制保存事件已触发: ${widget.sceneId}'); } _focusNode.removeListener(_onEditorFocusChange); _summaryFocusNode.removeListener(_onSummaryFocusChange); _contentDebounceTimer?.cancel(); // 取消内容防抖定时器 _selectionDebounceTimer?.cancel(); // 取消选择防抖定时器 _focusDebounceTimer?.cancel(); // 取消焦点防抖定时器 _activeStateDebounceTimer?.cancel(); // 🚀 取消活动状态防抖定时器 _streamingTimer?.cancel(); // 取消AI流式输出定时器 widget.controller.removeListener(_handleSelectionChange); // 移除选择变化监听 // 🚀 移除摘要滚动监听 _removeSummaryScrollListener(); // 🚀 场景销毁时不需要特别处理,数据管理器会自动处理数据持久化 if (widget.sceneId != null && OverlaySceneBeatManager.instance.isVisible && OverlaySceneBeatManager.instance.currentSceneId == widget.sceneId) { AppLogger.i('SceneEditor', '🔄 场景销毁,场景节拍数据由数据管理器自动管理: ${widget.sceneId}'); } _focusNode.dispose(); _summaryFocusNode.dispose(); _editorScrollController.dispose(); // 释放滚动控制器 _summaryTopOffsetVN.dispose(); super.dispose(); // 取消 document.changes 订阅,避免泄漏 _docChangeSub?.cancel(); } @override bool get wantKeepAlive => widget.isVisuallyNearby; @override Widget build(BuildContext context) { super.build(context); final theme = Theme.of(context); final bool isEditorOrSummaryFocused = _isFocused || _isSummaryFocused; return RepaintBoundary( child: _buildOptimizedSceneEditor(theme, isEditorOrSummaryFocused), ); } // 优化后的场景编辑器构建方法 Widget _buildOptimizedSceneEditor(ThemeData theme, bool isEditorOrSummaryFocused) { // 🚀 修改:使用Stack布局来实现摘要滚动固定效果 return Container( key: _sceneContainerKey, // 🚀 添加场景容器key decoration: WebTheme.getCleanCardDecoration(context: context), // 调整卡片间距,代替之前的 SceneDivider margin: EdgeInsets.only( bottom: widget.isFirst ? 16.0 : 24.0, top: widget.isFirst ? 0 : 8.0), child: GestureDetector( onTapDown: (_) { // ⚠️ 原因:避免在指针事件分发期间同步重建/状态修改导致 MouseTracker 重入(Flutter Web 断言) WidgetsBinding.instance.addPostFrameCallback((_) { // 🚀 优化:只在非焦点状态且活动状态确实需要改变时才进行激活操作 if (!_isFocused && !_isSummaryFocused) { // 检查当前是否已经是活动状态 final editorBloc = widget.editorBloc; if (editorBloc.state is editor_bloc.EditorLoaded) { final state = editorBloc.state as editor_bloc.EditorLoaded; final isAlreadyActive = state.activeActId == widget.actId && state.activeChapterId == widget.chapterId && state.activeSceneId == widget.sceneId; // 只有当不是活动状态时才设置 if (!isAlreadyActive) { _setActiveElementsQuietly(); } } else { // 状态不明确时才设置 _setActiveElementsQuietly(); } } }); }, // 添加点击处理,但确保不会干扰子控件的焦点 onTap: () { // ⚠️ 原因:同上,避免在指针事件回调里同步更改焦点树 WidgetsBinding.instance.addPostFrameCallback((_) { // 🚀 优化:如果编辑器还没有焦点,尝试获取焦点 if (!_isFocused && !_isSummaryFocused && mounted) { // 只有当没有其他焦点时,才请求焦点 if (!FocusScope.of(context).hasFocus && _focusNode.canRequestFocus) { _focusNode.requestFocus(); } } }); }, behavior: HitTestBehavior.translucent, // 确保即使有子组件也能接收手势 child: Padding( padding: const EdgeInsets.all(16.0), // 卡片内部统一内边距 child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 场景标题和字数统计 (移到卡片内部) _buildSceneHeader( theme, isEditorOrSummaryFocused), // 传入 theme 和焦点状态 const SizedBox(height: 12), // 增加标题和内容间距 // 🚀 修改:使用Stack布局来实现摘要滚动固定 Stack( children: [ // 编辑器区域 - 现在占用全宽度 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 编辑器区域 - 移除flex,让其自由填充 Expanded( child: Stack( children: [ // 编辑器(包装在设定引用悬停检测组件中) Stack( children: [ // 主编辑器 _buildEditor(theme, isEditorOrSummaryFocused), // 动态跟随选择区域的LayerLink目标 if (_showToolbar && _isEditorFullyInitialized) _buildEmbeddedLayerLinkTarget(), // AI工具栏的LayerLink目标 if (_showAIToolbar && _isEditorFullyInitialized) _buildEmbeddedAILayerLinkTarget(), ], ), // 文本选择工具栏 if (_showToolbar && _isEditorFullyInitialized) Positioned( child: SelectionToolbar( controller: widget.controller, layerLink: _toolbarLayerLink, wordCount: _selectedTextWordCount, editorSize: _editorContentKey.currentContext ?.findRenderObject() is RenderBox ? (_editorContentKey.currentContext! .findRenderObject() as RenderBox) .size : const Size(300, 150), selectionRect: Rect.zero, showAbove: _showToolbarAbove, scrollController: _editorScrollController, // 🚀 修改:使用从props传递的数据,而不是null值 novel: widget.novel, settings: widget.settings, settingGroups: widget.settingGroups, snippets: widget.snippets, novelId: _getNovelId(), // 传递小说ID onClosed: () { setState(() { _showToolbar = false; }); }, onFormatChanged: () { // 格式变更时可能需要更新选择状态 _handleSelectionChange(); }, onSettingCreated: (settingItem) { // 处理设定创建成功 - 现在后端保存已在detail组件内部处理 AppLogger.i('SceneEditor', '设定创建成功: ${settingItem.name}'); // 可以在这里刷新侧边栏设定列表或做其他UI更新 }, onSnippetCreated: (snippet) { // 处理片段创建成功 AppLogger.i('SceneEditor', '片段创建成功: ${snippet.title}'); // 可以在这里刷新片段列表或做其他操作 }, onStreamingGenerationStarted: (request, model) { // 处理流式生成开始 _handleStreamingGenerationStarted(request, model); }, targetKey: _toolbarTargetKey, ), ), // AI生成工具栏 if (_showAIToolbar && _isEditorFullyInitialized) Positioned( child: Builder( builder: (context) { // 检测是否位于前三行,参考写作工具栏逻辑 bool isInFirstThreeLines = false; try { final selection = widget.controller.selection; final document = widget.controller.document; final plainText = document.toPlainText(); final pos = selection.isCollapsed ? selection.baseOffset : selection.start; final safePos = pos.clamp(0, plainText.length); final before = plainText.substring(0, safePos); final lineBreaks = '\n'.allMatches(before).length; final lineNumber = lineBreaks + 1; // 1-based isInFirstThreeLines = lineNumber <= 3; } catch (_) { isInFirstThreeLines = false; } final bool showAbove = !isInFirstThreeLines; // 前三行强制下方 final double offsetBelow = isInFirstThreeLines ? 180.0 : 30.0; // 参考写作工具栏 return AIGenerationToolbar( layerLink: _aiToolbarLayerLink, onApply: _handleApplyGeneration, onRetry: _handleRetryGeneration, onDiscard: _handleDiscardGeneration, onSection: _handleSectionGeneration, onStop: _handleStopGeneration, wordCount: _aiGeneratedWordCount, modelName: _aiModelName, isGenerating: _isAIGenerating, showAbove: showAbove, offsetBelow: offsetBelow, ); }, ), ), ], ), ), // 固定宽度的占位空间 - 为摘要区域预留空间 (280px摘要 + 16px间距) const SizedBox(width: 296), ], ), // 🚀 新增:摘要区域 - 使用ValueListenableBuilder监听偏移,无需整棵树setState ValueListenableBuilder( valueListenable: _summaryTopOffsetVN, builder: (context, offsetY, child) { return Positioned( top: offsetY, right: 0, width: 280, child: child!, ); }, child: Container( key: _summaryKey, margin: const EdgeInsets.only(left: 0), constraints: const BoxConstraints( minHeight: 120, ), child: _buildSummaryArea(theme, isEditorOrSummaryFocused), ), ), ], ), const SizedBox(height: 16), // 内容和底部间距 ], ), ), ), ); } Widget _buildSceneHeader(ThemeData theme, bool isFocused) { return Padding( // 移除底部 padding,由 SizedBox 控制 padding: const EdgeInsets.only(bottom: 0.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 children: [ // 添加场景序号 if (widget.sceneIndex != null) Text( _getSceneIndexText(), style: WebTheme.getAlignedTextStyle( baseStyle: theme.textTheme.titleSmall?.copyWith( color: isFocused || widget.isActive ? WebTheme.getTextColor(context) : WebTheme.getSecondaryTextColor(context), fontWeight: FontWeight.w600, ) ?? const TextStyle(), ), ), Text( widget.title, style: WebTheme.getAlignedTextStyle( baseStyle: theme.textTheme.titleSmall?.copyWith( color: isFocused || widget.isActive ? WebTheme.getTextColor(context) : WebTheme.getSecondaryTextColor(context), fontWeight: FontWeight.w600, ) ?? const TextStyle(), ), ), const Spacer(), if (!widget.wordCount.isNaN) Text( widget.wordCount.toString(), style: WebTheme.getAlignedTextStyle( baseStyle: theme.textTheme.bodySmall?.copyWith( color: WebTheme.getSecondaryTextColor(context), fontSize: 11, ) ?? const TextStyle(), ), ), ], ), ); } // 添加获取场景序号文本的方法 String _getSceneIndexText() { if (widget.sceneIndex == null) return ''; // 使用中文数字表示场景序号 final List chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; if (widget.sceneIndex! <= 10) { return '场景${chineseNumbers[widget.sceneIndex!]} · '; } else if (widget.sceneIndex! < 20) { return '场景十${chineseNumbers[widget.sceneIndex! - 10]} · '; } else { // 对于更大的数字,直接使用阿拉伯数字 return '场景${widget.sceneIndex} · '; } } /// 构建动态跟随选择区域的LayerLink目标 /// 🚀 修复:使用实际的文档位置计算,而不是估算 Widget _buildEmbeddedLayerLinkTarget() { final selection = widget.controller.selection; // 只有在有选择时才显示目标 if (selection.isCollapsed) { return const SizedBox.shrink(); } //////AppLogger.d('SceneEditor', '🎯 构建精确定位LayerLink目标 - 选择范围: ${selection.start}-${selection.end}'); // 🚀 关键修复:计算选择区域的实际位置 final targetPosition = _calculateSelectionPosition(); return Positioned( // 保持同一个 Element,避免同帧出现多个 LeaderLayer // (移除动态 ValueKey,可用默认 key 策略) left: targetPosition.dx, top: targetPosition.dy, child: CompositedTransformTarget( link: _toolbarLayerLink, child: Container( key: _toolbarTargetKey, width: 4, height: 4, decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(2), ), ), ), ); } /// 构建AI工具栏的LayerLink目标 Widget _buildEmbeddedAILayerLinkTarget() { // 当AI工具栏需要显示时,始终创建目标点 if (!_showAIToolbar || !_isEditorFullyInitialized) { return const SizedBox.shrink(); } final selection = widget.controller.selection; // 🚀 修复:获取编辑器宽度,X坐标始终保持在中间 final RenderBox? editorBox = _editorContentKey.currentContext?.findRenderObject() as RenderBox?; if (editorBox == null) { return const SizedBox.shrink(); } // X坐标固定在编辑器中间 final centerX = editorBox.size.width / 2; double targetY; if (selection.isCollapsed) { // 🚀 当没有文本选择时(光标折叠),只计算Y坐标 try { final document = widget.controller.document; final plainText = document.toPlainText(); final cursorOffset = selection.baseOffset; // 计算光标前的文本和行数 final textBeforeCursor = plainText.substring(0, min(cursorOffset, plainText.length)); final lines = textBeforeCursor.split('\n'); final lineCount = lines.length - 1; // 获取编辑器设置 final editorSettings = widget.editorSettings ?? const EditorSettings(); final lineHeight = editorSettings.fontSize * editorSettings.lineSpacing; // 只计算Y坐标,基于光标所在行 targetY = editorSettings.paddingVertical + (lineCount * lineHeight); //AppLogger.d('SceneEditor', '🎯 AI工具栏位置: X=$centerX(固定中间), Y=$targetY, 行数=$lineCount'); } catch (e) { AppLogger.e('SceneEditor', '计算光标Y位置失败', e); // 回退到编辑器中下部位置 targetY = editorBox.size.height * 0.8; } } else { // 有文本选择时,计算选择区域的Y坐标 final selectionPosition = _calculateSelectionPosition(); targetY = selectionPosition.dy; } final targetPosition = Offset(centerX, targetY); // === 二次修正:如果工具栏不在可视区域内,则强制居中显示 === try { final viewportSize = MediaQuery.of(context).size; final RenderBox? editorBox2 = _editorContentKey.currentContext?.findRenderObject() as RenderBox?; if (editorBox2 != null) { final editorGlobal = editorBox2.localToGlobal(Offset.zero); // 与 AIGenerationToolbar 的偏移策略保持一致 bool isInFirstThreeLines = false; try { final selection2 = widget.controller.selection; final document2 = widget.controller.document; final plain2 = document2.toPlainText(); final pos2 = selection2.isCollapsed ? selection2.baseOffset : selection2.start; final safe2 = pos2.clamp(0, plain2.length); final before2 = plain2.substring(0, safe2); final lineBreaks2 = '\n'.allMatches(before2).length; final lineNumber2 = lineBreaks2 + 1; // 1-based isInFirstThreeLines = lineNumber2 <= 3; } catch (_) { isInFirstThreeLines = false; } final bool showAbove = !isInFirstThreeLines; // 与构建处一致 final double offsetBelow = isInFirstThreeLines ? 180.0 : 30.0; // 与构建处一致 final double offsetAbove = -60.0; // AIGenerationToolbar 默认 final double followerOffsetY = showAbove ? offsetAbove : offsetBelow; // 估算"工具栏顶部"的全局Y坐标 final double followerTopGlobalY = editorGlobal.dy + targetPosition.dy + followerOffsetY; // 若顶部超出屏幕,或大幅低于屏幕底部,则将其放到屏幕中间 final double topGuard = 8.0; final double bottomGuard = viewportSize.height - 8.0; if (followerTopGlobalY < topGuard || followerTopGlobalY > bottomGuard) { final double screenCenterY = viewportSize.height / 2; // 反推目标点本地Y:editorGlobal + correctedY + followerOffsetY = screenCenterY final double correctedLocalY = screenCenterY - editorGlobal.dy - followerOffsetY; // 约束在编辑器内容内部 targetY = correctedLocalY.clamp(0.0, editorBox2.size.height); } } } catch (_) { // 忽略修正失败,使用原位置 } return Positioned( key: ValueKey('ai_target_${targetPosition.dx}_${targetY}_${selection.baseOffset}_${selection.extentOffset}'), left: targetPosition.dx, top: targetY, child: CompositedTransformTarget( link: _aiToolbarLayerLink, child: Container( width: 4, height: 4, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.25), borderRadius: BorderRadius.circular(2), ), ), ), ); } /// 🚀 新增:精确计算选择区域在编辑器中的位置 Offset _calculateSelectionPosition() { try { final selection = widget.controller.selection; if (selection.isCollapsed) { ////AppLogger.d('SceneEditor', '❌ 选择已折叠,返回默认位置'); return Offset.zero; } // 获取编辑器的渲染对象 final RenderBox? editorBox = _editorContentKey.currentContext?.findRenderObject() as RenderBox?; if (editorBox == null) { ////AppLogger.d('SceneEditor', '❌ 编辑器渲染对象为空,返回默认位置'); return Offset.zero; } // 🚀 关键修复:使用基于行数的精确计算,避免TextPainter的累积误差 final document = widget.controller.document; final plainText = document.toPlainText(); // 获取选择开始位置的文本 final textBeforeSelection = plainText.substring(0, min(selection.start, plainText.length)); // 🚀 使用编辑器设置获取准确的样式信息 final editorSettings = widget.editorSettings ?? const EditorSettings(); // 🚀 关键修复:计算行数和位置,使用更准确的方法 final lines = textBeforeSelection.split('\n'); final lineCount = lines.length - 1; // 减1因为最后一行不算换行 final lastLineLength = lines.last.length; // 🚀 计算实际的行高(考虑编辑器的实际渲染) final actualLineHeight = editorSettings.fontSize * editorSettings.lineSpacing; // 🚀 关键修复:使用编辑器实际高度和文本总行数来计算比例因子 final totalLines = plainText.split('\n').length; final actualEditorHeight = editorBox.size.height - (editorSettings.paddingVertical * 2); final heightPerLine = actualEditorHeight / totalLines; // 🚀 使用修正后的行高,在长文档中使用实际渲染的行高 final correctedLineHeight = max(heightPerLine, actualLineHeight * 0.8); // 使用较小值,但有最小限制 // 🚀 计算Y位置(基于修正的行高) final estimatedY = editorSettings.paddingVertical + (lineCount * correctedLineHeight); // 🚀 计算X位置:始终使用编辑器内容区域的中心,让工具栏水平居中 final contentWidth = min(editorBox.size.width, editorSettings.maxLineWidth); final estimatedX = (contentWidth / 2) + editorSettings.paddingHorizontal; // 内容区域中心 final charWidth = editorSettings.fontSize * 0.6; // 仅用于日志 final finalPosition = Offset(estimatedX, estimatedY); // 🚀 详细日志,包含修正信息 //////AppLogger.d('SceneEditor', '✅ 修正选择区域位置: ${finalPosition.dx}, ${finalPosition.dy}'); //////AppLogger.d('SceneEditor', ' 选择位置: ${selection.start}-${selection.end}, 文本长度: ${textBeforeSelection.length}'); //////AppLogger.d('SceneEditor', ' 行数统计: 当前行=$lineCount, 总行数=$totalLines, 最后行长度=$lastLineLength'); //////AppLogger.d('SceneEditor', ' 编辑器尺寸: ${editorBox.size}, 实际内容高度: $actualEditorHeight'); //////AppLogger.d('SceneEditor', ' 行高计算: 理论行高=$actualLineHeight, 实际行高=$heightPerLine, 修正行高=$correctedLineHeight'); //////AppLogger.d('SceneEditor', ' 位置计算: X=$estimatedX (字符宽度=$charWidth), Y=$estimatedY'); return finalPosition; } catch (e) { AppLogger.e('SceneEditor', '❌ 精确计算选择区域位置失败: $e'); return Offset.zero; } } /// 🚀 构建完整的QuillEditorConfig,充分利用编辑器设置 QuillEditorConfig _buildQuillEditorConfig(EditorSettings editorSettings) { return QuillEditorConfig( // 基础设置 minHeight: editorSettings.minEditorHeight < 1200.0 ? 1200.0 : editorSettings.minEditorHeight, maxHeight: null, // 让场景编辑器自由扩展 maxContentWidth: editorSettings.maxLineWidth, // 占位符和焦点 placeholder: '开始写作...', autoFocus: false, // 禁用自动聚焦以减少不必要的渲染 // 布局和间距 padding: EdgeInsets.symmetric( vertical: editorSettings.paddingVertical, horizontal: editorSettings.paddingHorizontal, ), expands: false, // 不自动扩展,保持控制 // 滚动设置 scrollable: editorSettings.smoothScrolling, scrollPhysics: editorSettings.smoothScrolling ? const BouncingScrollPhysics() : const ClampingScrollPhysics(), // 文本设置 textCapitalization: TextCapitalization.sentences, // 光标和选择 showCursor: true, paintCursorAboveText: editorSettings.highlightActiveLine, enableInteractiveSelection: true, enableSelectionToolbar: true, // 键盘设置 keyboardAppearance: editorSettings.darkModeEnabled ? Brightness.dark : Brightness.light, // 自定义样式和交互 customStyles: _buildCustomStyles(editorSettings), customStyleBuilder: _buildCombinedCustomStyleBuilder(), customRecognizerBuilder: SettingReferenceInteractionMixin.getCustomRecognizerBuilder( onSettingReferenceClicked: (settingId) { AppLogger.i('SceneEditor', '🖱️ 设定引用被点击: $settingId'); _handleSettingReferenceClicked(settingId); }, onSettingReferenceHovered: null, onSettingReferenceHoverEnd: null, ), // 行为设置 detectWordBoundary: true, enableAlwaysIndentOnTab: false, floatingCursorDisabled: !editorSettings.useTypewriterMode, // 其他高级设置 onTapOutsideEnabled: true, disableClipboard: false, enableScribble: false, // 暂时禁用涂鸦功能 ); } // 为编辑器添加焦点处理 Widget _buildEditor(ThemeData theme, bool isFocused) { // 获取编辑器设置 final editorSettings = widget.editorSettings ?? const EditorSettings(); // 在编辑器区域添加MouseRegion return MouseRegion( //cursor: SystemMouseCursors.text, // 在编辑器区域显示文本光标 hitTestBehavior: HitTestBehavior.deferToChild, // 优先让子组件处理事件 child: Container( key: _editorContentKey, constraints: BoxConstraints( maxWidth: editorSettings.maxLineWidth, minHeight: editorSettings.minEditorHeight < 1200.0 ? 1200.0 : editorSettings.minEditorHeight, ), // 使用动态背景色,兼容暗黑 / 亮色主题 color: WebTheme.getSurfaceColor(context), child: Theme( data: theme.copyWith( // 确保QuillEditor的占位符没有下划线 inputDecorationTheme: const InputDecorationTheme( border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, filled: false, hintStyle: TextStyle( color: Colors.grey, decoration: TextDecoration.none, // 明确去掉下划线 ), ), ), child: QuillEditor.basic( // 关键修复:使用依赖 editorSettings 的动态 Key,确保编辑器设置更新后立即重建 key: ValueKey('editor_${widget.sceneId}_${widget.editorSettings?.hashCode ?? 0}'), controller: widget.controller, focusNode: _focusNode, // 使用编辑器的 FocusNode scrollController: _editorScrollController, // 使用实例变量的滚动控制器 config: _buildQuillEditorConfig(editorSettings), ), ), ), ); } /// 根据编辑器设置构建自定义样式 DefaultStyles _buildCustomStyles(EditorSettings settings) { final baseTextStyle = TextStyle( color: WebTheme.getTextColor(context), fontSize: settings.fontSize, fontFamily: settings.fontFamily, fontWeight: settings.fontWeight, height: settings.lineSpacing, letterSpacing: settings.letterSpacing, decoration: TextDecoration.none, ); return DefaultStyles( // 段落样式 - 🚀 修复:移除默认左缩进,避免大空白 paragraph: DefaultTextBlockStyle( baseTextStyle, HorizontalSpacing.zero, // 不使用默认缩进 settings.paragraphSpacing > 0 ? VerticalSpacing(settings.paragraphSpacing, 0) : VerticalSpacing.zero, // 🚀 修复:段落间距为0时使用zero VerticalSpacing.zero, // 🚀 修复:确保行间距也为zero null, ), // 占位符样式 - 🚀 修复:移除默认左缩进 placeHolder: DefaultTextBlockStyle( baseTextStyle.copyWith( color: WebTheme.getSecondaryTextColor(context), ), HorizontalSpacing.zero, // 不使用默认缩进 settings.paragraphSpacing > 0 ? VerticalSpacing(settings.paragraphSpacing, 0) : VerticalSpacing.zero, // 🚀 修复:段落间距为0时使用zero VerticalSpacing.zero, // 🚀 修复:确保行间距也为zero null, ), // 粗体样式 bold: baseTextStyle.copyWith( fontWeight: FontWeight.bold, ), // 斜体样式 italic: baseTextStyle.copyWith( fontStyle: FontStyle.italic, ), // 下划线样式 underline: baseTextStyle.copyWith( decoration: TextDecoration.underline, ), // 删除线样式 strikeThrough: baseTextStyle.copyWith( decoration: TextDecoration.lineThrough, ), // 链接样式 link: baseTextStyle.copyWith( color: settings.darkModeEnabled ? Colors.lightBlue : Colors.blue, decoration: TextDecoration.underline, ), // 标题样式 - 🚀 修复:移除默认左缩进 h1: DefaultTextBlockStyle( baseTextStyle.copyWith( fontSize: settings.fontSize * 2.0, fontWeight: FontWeight.bold, ), HorizontalSpacing.zero, // 不使用默认缩进 settings.paragraphSpacing > 0 ? VerticalSpacing(settings.paragraphSpacing * 2, 0) : VerticalSpacing.zero, // 🚀 修复:段落间距为0时使用zero VerticalSpacing.zero, // 🚀 修复:确保行间距也为zero null, ), h2: DefaultTextBlockStyle( baseTextStyle.copyWith( fontSize: settings.fontSize * 1.5, fontWeight: FontWeight.bold, ), HorizontalSpacing.zero, // 不使用默认缩进 settings.paragraphSpacing > 0 ? VerticalSpacing(settings.paragraphSpacing * 1.5, 0) : VerticalSpacing.zero, // 🚀 修复:段落间距为0时使用zero VerticalSpacing.zero, // 🚀 修复:确保行间距也为zero null, ), h3: DefaultTextBlockStyle( baseTextStyle.copyWith( fontSize: settings.fontSize * 1.25, fontWeight: FontWeight.bold, ), HorizontalSpacing.zero, // 不使用默认缩进 settings.paragraphSpacing > 0 ? VerticalSpacing(settings.paragraphSpacing, 0) : VerticalSpacing.zero, // 🚀 修复:段落间距为0时使用zero VerticalSpacing.zero, // 🚀 修复:确保行间距也为zero null, ), // 内联代码样式 inlineCode: InlineCodeStyle( backgroundColor: Colors.transparent, radius: const Radius.circular(3), style: baseTextStyle.copyWith( fontFamily: 'monospace', ), ), // 列表样式 - 🚀 保留缩进:列表项需要缩进来显示层级 lists: DefaultListBlockStyle( baseTextStyle, HorizontalSpacing(settings.indentSize, 0), // 列表项保持缩进 VerticalSpacing(settings.paragraphSpacing / 2, 0), VerticalSpacing(0, 0), null, null, ), // 引用样式 - 🚀 保留缩进:引用通常需要视觉上的缩进 quote: DefaultTextBlockStyle( baseTextStyle.copyWith( color: WebTheme.getSecondaryTextColor(context), fontStyle: FontStyle.italic, ), HorizontalSpacing(settings.indentSize, 0), // 引用保持缩进 VerticalSpacing(settings.paragraphSpacing, 0), VerticalSpacing(0, 0), BoxDecoration( border: Border( left: BorderSide( width: 4, color: WebTheme.getSecondaryTextColor(context), ), ), ), ), ); } @override void didChangeDependencies() { super.didChangeDependencies(); // 🎯 优化:只在真正需要时处理设定引用,避免频繁调用 // 检查是否有实质性的依赖变化 final hasSignificantChange = _hasSignificantDependencyChange(); if (hasSignificantChange) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_isProcessingSettingReferences) { ////AppLogger.d('SceneEditor', '🔄 依赖变化触发设定引用处理: ${widget.sceneId}'); _processSettingReferencesDebounced(); // 使用防抖版本 } }); } } @override void didUpdateWidget(SceneEditor oldWidget) { super.didUpdateWidget(oldWidget); // 🎯 优化:只在组件内容真正更新时处理设定引用 final hasContentChange = oldWidget.sceneId != widget.sceneId || oldWidget.controller != widget.controller; if (hasContentChange && mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_isProcessingSettingReferences) { ////AppLogger.d('SceneEditor', '🔄 组件更新触发设定引用处理: ${widget.sceneId}'); _processSettingReferencesDebounced(); // 使用防抖版本 } }); } // 🛠️ 当父组件替换了 controller(例如占位控制器异步解析完成后), // 需要把监听器从旧 controller 上移除并绑定到新的 controller, // 否则选区变化和文档变化都不会再触发当前组件的回调, // 从而导致 SelectionToolbar 无法弹出。 if (oldWidget.controller != widget.controller) { // 移除旧 controller 的监听 oldWidget.controller.removeListener(_handleSelectionChange); // 取消旧 controller 的 document 订阅 _docChangeSub?.cancel(); // 绑定新 controller 的监听 widget.controller.addListener(_handleSelectionChange); // 重新订阅 document.changes 并保存引用 _docChangeSub = widget.controller.document.changes.listen(_onDocumentChange); } } // 🎯 新增:检查是否有实质性的依赖变化 bool _hasSignificantDependencyChange() { // 可以根据需要检查具体的依赖变化 // 目前简化处理,减少不必要的触发 return true; // 暂时保持原有行为,后续可以进一步优化 } Widget _buildSummaryArea(ThemeData theme, bool isFocused) { // 🚀 优化:使用自适应高度的布局 return Container( // 移除 margin,由 Row 的 SizedBox 控制 padding: const EdgeInsets.all(12), // 调整摘要区内边距 decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), // 使用动态表面色 borderRadius: BorderRadius.circular(8), // 给摘要区本身加圆角 // 🚀 新增:添加微妙的阴影效果,当摘要处于sticky状态时更明显 boxShadow: _isSummarySticky ? [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.08), blurRadius: 8, offset: const Offset(0, 2), ), ] : [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.03), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: IntrinsicHeight( // 🚀 使用IntrinsicHeight让整个摘要区域自适应内容高度 child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, // 🚀 优化:最小化占用空间 children: [ // 摘要标题和右上角按钮 Row( crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 children: [ Expanded( child: Text( '摘要', style: WebTheme.getAlignedTextStyle( baseStyle: theme.textTheme.titleSmall?.copyWith( fontSize: 13, fontWeight: FontWeight.w600, color: isFocused || widget.isActive ? WebTheme.getTextColor(context) : WebTheme.getSecondaryTextColor(context), ) ?? const TextStyle(), ), ), ), // 摘要操作按钮(刷新、AI生成) - 移到右上角 _buildSummaryActionButtons(theme, isFocused), ], ), const SizedBox(height: 8), // 🚀 优化:摘要内容 - 使用自适应高度,统一背景色,保证最小高度 Container( padding: const EdgeInsets.all(12), // 🚀 保持统一的内边距 constraints: const BoxConstraints( minHeight: 60, // 🚀 新增:确保最小高度,即使空内容也有一行文字的高度 ), // 🚀 修复:设置正确的背景色 decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(12), ), child: MouseRegion( cursor: SystemMouseCursors.text, // 在摘要区域显示文本光标 child: Material( type: MaterialType.transparency, // 使用透明Material类型避免黄色下划线 child: IntrinsicHeight( child: TextField( controller: widget.summaryController, focusNode: _summaryFocusNode, style: WebTheme.getAlignedTextStyle( baseStyle: theme.textTheme.bodyMedium?.copyWith( color: WebTheme.getTextColor(context), // 改为主要文字颜色 fontSize: 13, height: 1.4, ) ?? const TextStyle(), ), // 🚀 改为自适应高度:不限制最大行数 maxLines: null, minLines: 2, keyboardType: TextInputType.multiline, // 支持多行输入 textInputAction: TextInputAction.newline, // 支持换行 decoration: WebTheme.getBorderlessInputDecoration( hintText: '添加场景摘要...', context: context, // 传递context以设置正确的hintStyle ), // 🚀 自适应模式下禁用内部滚动,让外层滚动容器接管 scrollPhysics: const NeverScrollableScrollPhysics(), onChanged: (value) { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 1200), () { // 🚀 新增:检查控制器是否仍然有效 if (!mounted || widget.summaryController.text != value) { AppLogger.v('SceneEditor', '摘要控制器已失效或内容已变化,跳过保存: ${widget.sceneId}'); return; } if (mounted && widget.actId != null && widget.chapterId != null && widget.sceneId != null) { AppLogger.i('SceneEditor', '通过onChange保存摘要: ${widget.sceneId}'); widget.editorBloc.add(editor_bloc.UpdateSummary( novelId: widget.editorBloc.novelId, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, summary: value, shouldRebuild: true, // 改为true,确保UI更新和完整保存 )); } // 🚀 新增:内容变化时更新摘要高度 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _updateSummaryHeight(); _updateSummaryPosition(); } }); }); }, ), ), ), ), ), const SizedBox(height: 12), // 🚀 新增:摘要内容和操作按钮之间的间距 // 🚀 新增:摘要操作按钮区域 _buildSummaryBottomActions(theme, isFocused), ], ), ), ); } // 🚀 新增:摘要底部操作按钮区域 Widget _buildSummaryBottomActions(ThemeData theme, bool isFocused) { return Row( mainAxisAlignment: MainAxisAlignment.start, // 🚀 改为左对齐,避免空间分散 children: [ // 🚀 最左边:更多操作按钮(三点菜单) if (widget.actId != null && widget.chapterId != null && widget.sceneId != null) MenuBuilder.buildSceneMenu( context: context, editorBloc: widget.editorBloc, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, ), if (widget.actId != null && widget.chapterId != null && widget.sceneId != null) const SizedBox(width: 4), // 🚀 减小间距 // // 标签按钮 // _SummaryActionButton( // icon: Icons.label_outline, // label: '标签', // tooltip: '添加标签', // onPressed: () {/* TODO */}, // ), // const SizedBox(width: 4), // 🚀 减小间距 // // Codex按钮 // _SummaryActionButton( // icon: Icons.lan_outlined, // label: 'Codex', // tooltip: '关联 Codex', // onPressed: () {/* TODO */}, // ), // 场景节拍按钮 const SizedBox(width: 4), // 🚀 减小间距 _SummaryActionButton( icon: Icons.auto_fix_high, label: '节拍', tooltip: '场景节拍生成', onPressed: () { if (widget.actId != null && widget.chapterId != null && widget.sceneId != null) { _showSceneBeatPanel(); } }, ), // AI生成场景按钮(仅在有摘要内容时显示) if (widget.summaryController.text.isNotEmpty) ...[ const SizedBox(width: 4), // 🚀 减小间距 _SummaryActionButton( icon: Icons.auto_stories, label: 'AI生成', tooltip: '从摘要生成场景内容', onPressed: () { if (widget.actId != null && widget.chapterId != null && widget.sceneId != null) { // 获取布局管理器并打开AI生成面板 final layoutManager = Provider.of(context, listen: false); // 保存当前摘要到EditorBloc中,以便AI生成面板可以获取到 widget.editorBloc.add( editor_bloc.SetPendingSummary( summary: widget.summaryController.text, ), ); // 显示AI生成面板 layoutManager.toggleAISceneGenerationPanel(); } }, ), ], ], ); } // 新增:摘要区域右上角的操作按钮 Widget _buildSummaryActionButtons(ThemeData theme, bool isFocused) { // 使用 Row + IconButton 实现 return Row( mainAxisSize: MainAxisSize.min, // 重要:避免 Row 占用过多空间 children: [ IconButton( icon: Icon(Icons.refresh, size: 18, color: WebTheme.getSecondaryTextColor(context)), tooltip: '刷新摘要', onPressed: () { // 实现刷新摘要逻辑 if (widget.summaryController.text.isNotEmpty && widget.actId != null && widget.chapterId != null && widget.sceneId != null && mounted) { // 🚀 新增:检查控制器是否仍然有效 try { // 尝试访问控制器文本以验证其有效性 final currentText = widget.summaryController.text; AppLogger.i('SceneEditor', '通过刷新按钮保存摘要: ${widget.sceneId}'); widget.editorBloc.add(editor_bloc.UpdateSummary( novelId: widget.editorBloc.novelId, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, summary: currentText, shouldRebuild: true, // 修改为true,确保完整保存到后端 )); } catch (e) { AppLogger.w('SceneEditor', '摘要控制器已失效,跳过刷新保存: ${widget.sceneId}', e); } } }, splashRadius: 18, constraints: const BoxConstraints(), padding: const EdgeInsets.symmetric(horizontal: 4), visualDensity: VisualDensity.compact, // 添加悬停效果 hoverColor: WebTheme.getSurfaceColor(context), ), IconButton( icon: Icon(Icons.auto_awesome, size: 18, color: WebTheme.getSecondaryTextColor(context)), tooltip: 'AI 生成摘要', onPressed: () { // 使用新的摘要生成器 if (widget.actId != null && widget.chapterId != null && widget.sceneId != null) { _showSummaryGenerator(); } }, splashRadius: 18, constraints: const BoxConstraints(), padding: const EdgeInsets.symmetric(horizontal: 4), visualDensity: VisualDensity.compact, // 添加悬停效果 hoverColor: WebTheme.getSurfaceColor(context), ), ], ); } // 🚀 优化:添加SettingBloc状态监听,处理设定引用 void _setupSettingBlocListener() { final novelId = _getNovelId(); if (novelId == null) { AppLogger.w('SceneEditor', '⚠️ 无法获取小说ID,跳过设定引用监听设置'); return; } AppLogger.i('SceneEditor', '🎯 设置SettingBloc监听器 - 场景: ${widget.sceneId}, 小说: $novelId'); // 🚀 新增:立即检查当前状态,如果数据已存在则直接处理 final currentState = context.read().state; if (currentState.itemsStatus == SettingStatus.success && currentState.items.isNotEmpty) { AppLogger.i('SceneEditor', '✅ 设定数据已就绪,立即处理引用 - 条目数量: ${currentState.items.length}'); // 延迟一帧执行,确保组件已完全初始化 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _checkAndProcessSettingReferences(); } }); } else { AppLogger.i('SceneEditor', '⏳ 设定数据尚未就绪 - 状态: ${currentState.itemsStatus}, 条目数量: ${currentState.items.length}'); } // 🚀 优化:设置流监听器,响应后续的数据更新 context.read().stream.listen((state) { if (!mounted) return; // 当设定项目加载完成时,处理设定引用 if (state.itemsStatus == SettingStatus.success && state.items.isNotEmpty) { AppLogger.i('SceneEditor', '🔄 设定数据更新,重新处理引用 - 场景: ${widget.sceneId}, 条目数量: ${state.items.length}'); _checkAndProcessSettingReferences(); } }); } // 🎯 优化:防抖处理设定引用,避免频繁调用 void _processSettingReferencesDebounced() { //if (true) return; // 如果正在处理设定引用,跳过新的请求 if (_isProcessingSettingReferences) { AppLogger.v('SceneEditor', '⏭️ 正在处理设定引用,跳过新的请求: ${widget.sceneId}'); return; } // 生成期间不处理设定引用,避免与流式变更抢占主线程 if (_isAIGenerating) { AppLogger.v('SceneEditor', '⏭️ 生成中,跳过设定引用处理请求: ${widget.sceneId}'); return; } // 🎯 新增:检查距离上次处理的时间间隔 final now = DateTime.now(); final timeSinceLastProcessing = now.difference(_lastProcessingTime); if (timeSinceLastProcessing < _minProcessingInterval) { AppLogger.v('SceneEditor', '⏭️ 处理间隔过短,跳过设定引用处理: ${widget.sceneId}'); return; } // _settingReferenceProcessTimer?.cancel(); // _settingReferenceProcessTimer = Timer(const Duration(milliseconds: 800), () { // if (mounted && !_isProcessingSettingReferences) { // _lastProcessingTime = DateTime.now(); // _processSettingReferences(); // } // }); } // 🎯 优化:智能处理设定引用(使用防抖和状态检查) void _checkAndProcessSettingReferences() { if (!mounted || _isProcessingSettingReferences || _isAIGenerating) { return; } //AppLogger.i('SceneEditor', '🎯 智能处理设定引用: ${widget.sceneId}'); try { // 使用防抖机制避免频繁调用 _processSettingReferencesDebounced(); } catch (e) { AppLogger.w('SceneEditor', '处理设定引用失败', e); } } // 🚀 新增:检查元素是否在视口中可见 // 处理设定引用 - Flutter原生实现 void _processSettingReferences() { try { if (!mounted) return; // 🎯 新增:完整内容相等判断,包括样式信息 final currentDeltaContent = jsonEncode(widget.controller.document.toDelta().toJson()); final currentText = widget.controller.document.toPlainText(); final int textHash = currentText.hashCode; if (textHash == _lastSettingHash) { // 文本无实质改动,跳过 _isProcessingSettingReferences = false; return; } // 首先检查完整Delta内容是否相等(包含样式) if (currentDeltaContent == _lastProcessedDeltaContent) { ////AppLogger.d('SceneEditor', '⏭️ 场景内容完全相等,跳过设定引用处理'); return; } // 其次检查纯文本内容是否相等(向后兼容) if (currentText == _lastProcessedText) { ////AppLogger.d('SceneEditor', '⏭️ 文本内容未变化,跳过设定引用处理'); return; } // 🚀 关键修复:设置处理标志,避免样式变化触发保存 _isProcessingSettingReferences = true; ////AppLogger.d('SceneEditor', '🔍 开始Flutter原生设定引用处理,文本长度: ${currentText.length}'); ////AppLogger.d('SceneEditor', '📝 文本内容预览: ${currentText.length > 100 ? currentText.substring(0, 100) + "..." : currentText}'); final settingState = context.read().state; final settingItems = settingState.items; AppLogger.i('SceneEditor', '📚 当前设定条目数量: ${settingItems.length}'); // if (settingItems.isNotEmpty) { // final validNames = settingItems.where((item) => item.name != null).map((item) => item.name!).join(', '); // } // 🚀 使用Flutter Quill原生Attribute系统处理设定引用 SettingReferenceProcessor.processSettingReferences( document: widget.controller.document, settingItems: settingItems, controller: widget.controller, ); // 🎯 更新:记录处理过的内容 _lastProcessedText = currentText; _lastProcessedDeltaContent = currentDeltaContent; _lastSettingHash = textHash; } catch (e) { AppLogger.e('SceneEditor', 'Flutter原生设定引用处理失败', e); } finally { // 🚀 关键修复:无论成功失败都重置处理标志 _isProcessingSettingReferences = false; } } // 处理设定引用点击 void _handleSettingReferenceClicked(String settingId) { AppLogger.i('SceneEditor', '🖱️ 设定引用被点击: $settingId'); final novelId = _getNovelId(); if (novelId == null) { AppLogger.w('SceneEditor', '无法显示设定预览:缺少小说ID'); return; } AppLogger.i('SceneEditor', '📋 设定引用详情: ID=$settingId, 小说=$novelId'); // 🎯 显示设定预览卡片 _showSettingPreviewCard(settingId, novelId); // 触发设定悬停回调 //_handleSettingReferenceHovered(settingId); } /// 🎯 构建组合的自定义样式构建器 /// 同时支持设定引用样式和AI生成内容样式 TextStyle Function(Attribute) _buildCombinedCustomStyleBuilder() { return (Attribute attribute) { // 1. 处理设定引用样式 final settingReferenceStyle = SettingReferenceInteractionMixin .getCustomStyleBuilderWithHover(hoveredSettingId: null)(attribute); // 2. 处理AI生成内容样式 final aiGeneratedStyle = AIGeneratedContentProcessor .getCustomStyleBuilder()(attribute); // 3. 处理背景色属性(保持原有逻辑) if (attribute.key == 'background' && attribute.value != null) { final colorValue = attribute.value as String; try { // 解析颜色值(支持#FFF3CD格式) Color? backgroundColor; if (colorValue.startsWith('#')) { final hexColor = colorValue.substring(1); if (hexColor.length == 6) { backgroundColor = Color(int.parse('FF$hexColor', radix: 16)); } } if (backgroundColor != null) { return TextStyle(backgroundColor: backgroundColor); } } catch (e) { AppLogger.w('SceneEditor', '解析背景色失败: $colorValue', e); } } // 4. 合并样式(优先级:AI生成 > 设定引用 > 其他) if (aiGeneratedStyle.color != null) { return aiGeneratedStyle; } else if (settingReferenceStyle.decoration != null) { return settingReferenceStyle; } // 返回空的TextStyle表示使用默认样式 return const TextStyle(); }; } /// 显示设定预览卡片 - 使用通用管理器 /// /// 🎨 采用全局样式和主题的统一设定预览卡片 /// 🚀 修复了Provider传递问题,确保详情卡片正常打开 void _showSettingPreviewCard(String settingId, String novelId) { try { // 获取当前屏幕中心位置 final screenSize = MediaQuery.of(context).size; final position = Offset( screenSize.width * 0.5, // 屏幕中心 screenSize.height * 0.3, // 靠上一些 ); AppLogger.i('SceneEditor', '📍 显示设定预览卡片: $settingId'); // 🚀 使用通用设定预览管理器,自动处理Provider传递问题 SettingPreviewManager.show( context: context, settingId: settingId, novelId: novelId, position: position, onClose: () { ////AppLogger.d('SceneEditor', '设定预览卡片已关闭'); }, onDetailOpened: () { AppLogger.i('SceneEditor', '设定详情卡片已打开'); }, ); AppLogger.i('SceneEditor', '✅ 设定预览卡片已显示'); } catch (e) { AppLogger.e('SceneEditor', '显示设定预览卡片失败', e); } } /// 处理流式生成开始 - 支持统一AI模型 void _handleStreamingGenerationStarted(UniversalAIRequest request, UnifiedAIModel model) { AppLogger.i('SceneEditor', '🚀 开始流式生成: ${request.requestType}, 模型: ${model.displayName} (公共:${model.isPublic})'); // 🚀 若存在未应用的AI生成内容或隐藏文本,先自动应用为正文,避免并发生成导致上下文缺失 try { final bool hasAIGen = AIGeneratedContentProcessor.hasAnyAIGeneratedContent( controller: widget.controller, ); final bool hasHidden = AIGeneratedContentProcessor.hasAnyHiddenText( controller: widget.controller, ); if (hasAIGen || hasHidden) { if (_isAIGenerating) { _handleStopGeneration(); } _handleApplyGeneration(); } } catch (_) {} // 🚀 新增:保存请求和统一模型配置,用于重试 _lastAIRequest = request; _lastUnifiedModel = model; // 已移除 UserAIModelConfigModel 相关逻辑,现在使用 UnifiedAIModel AppLogger.i('SceneEditor', '💾 保存模型信息: ${model.displayName} (公共模型: ${model.isPublic})'); // 获取当前选择范围 final selection = widget.controller.selection; final selectedText = selection.isCollapsed ? '' : widget.controller.document.toPlainText().substring(selection.start, selection.end); // 🚀 保存选中的文本,用于返回表单 _lastSelectedText = selectedText; // 🆕 根据请求类型决定处理方式 if ((request.requestType == AIRequestType.refactor || request.requestType == AIRequestType.summary) && !selection.isCollapsed) { // 重构或缩写:使用隐藏文本属性标记原选中的文本 final mode = request.requestType == AIRequestType.refactor ? '重构' : '缩写'; AppLogger.i('SceneEditor', '🫥 ${mode}模式:隐藏原选中文本 (${selectedText.length}字符)'); AIGeneratedContentProcessor.markAsHidden( controller: widget.controller, startOffset: selection.start, length: selection.end - selection.start, ); _lastInsertedOffset = selection.end; // 在隐藏文本后插入新内容 } else { // 扩写或其他:在选中范围末尾插入新内容 AppLogger.i('SceneEditor', '📝 扩写模式:在选中文本后插入新内容'); _lastInsertedOffset = selection.end; } // 隐藏选择工具栏 setState(() { _showToolbar = false; _showAIToolbar = true; _isAIGenerating = true; _aiModelName = model.displayName; _generatedText = ''; _aiGeneratedWordCount = 0; _currentStreamIndex = 0; _pendingStreamText = ''; }); _aiGeneratedStartOffset = _lastInsertedOffset; // 记录AI生成内容的起始位置 // 开始流式生成 _startStreamingGeneration(request); } /// 开始流式生成 Future _startStreamingGeneration(UniversalAIRequest request) async { try { final universalAIRepository = context.read(); AppLogger.i('SceneEditor', '📡 发送流式AI请求'); // 同步:如果是场景节拍生成,请先把浮动面板状态置为生成中 try { final bool isSceneBeat = request.requestType == AIRequestType.sceneBeat; final String? sid = request.sceneId ?? widget.sceneId; if (isSceneBeat && sid != null && sid.isNotEmpty) { SceneBeatDataManager.instance.updateSceneStatus(sid, SceneBeatStatus.generating); } } catch (e) { AppLogger.w('SceneEditor', '同步场景节拍状态为生成中失败', e); } // 发送流式请求 final stream = universalAIRepository.streamRequest(request); await for (final chunk in stream) { if (!mounted || !_isAIGenerating) { ////AppLogger.d('SceneEditor', '🛑 流式生成被中断: mounted=$mounted, _isAIGenerating=$_isAIGenerating'); break; } // 🚀 修复:检查是否收到结束信号 if (chunk.finishReason != null) { AppLogger.i('SceneEditor', '✅ 收到流式生成结束信号: ${chunk.finishReason}'); // 立即停止生成状态 setState(() { _isAIGenerating = false; }); // 同步:如果是场景节拍生成,将面板状态置为已生成 try { final bool isSceneBeat = request.requestType == AIRequestType.sceneBeat; final String? sid = request.sceneId ?? widget.sceneId; if (isSceneBeat && sid != null && sid.isNotEmpty) { SceneBeatDataManager.instance.updateSceneStatus(sid, SceneBeatStatus.generated); } } catch (e) { AppLogger.w('SceneEditor', '同步场景节拍完成状态失败', e); } // 🚀 扩写/重构/缩写等流式生成完成:刷新积分 try { // ignore: use_build_context_synchronously context.read().add(const RefreshUserCredits()); } catch (_) {} break; } if (chunk.content.isNotEmpty) { // 🚀 修复:使用同步方式逐字符显示,避免异步延迟导致的状态不一致 await _appendTextCharByCharSync(chunk.content); } // 更新模型信息 if (chunk.model != null) { setState(() { _aiModelName = chunk.model!; }); } } // 🚀 确保在流结束时状态被正确重置 if (mounted) { setState(() { _isAIGenerating = false; }); AppLogger.i('SceneEditor', '✅ 流式生成完成,状态已重置'); // 兜底:如果是场景节拍生成,确保面板状态为已生成 try { final bool isSceneBeat = request.requestType == AIRequestType.sceneBeat; final String? sid = request.sceneId ?? widget.sceneId; if (isSceneBeat && sid != null && sid.isNotEmpty) { SceneBeatDataManager.instance.updateSceneStatus(sid, SceneBeatStatus.generated); } } catch (e) { AppLogger.w('SceneEditor', '兜底同步场景节拍完成状态失败', e); } // 🚀 触发生成完成回调(如果存在) if (_onSceneBeatGenerationComplete != null) { try { _onSceneBeatGenerationComplete!.call(); } catch (e) { AppLogger.w('SceneEditor', '生成完成回调执行失败', e); } _onSceneBeatGenerationComplete = null; // 清理引用 } } } catch (e) { AppLogger.e('SceneEditor', '流式生成失败', e); // 🚀 立即恢复隐藏的文本样式(重构/缩写的横杠样式) _restoreHiddenTextOnError(); // 🚀 专门处理积分不足错误 if (e is InsufficientCreditsException) { AppLogger.w('SceneEditor', '积分不足: ${e.message}'); if (mounted) { _showInsufficientCreditsDialog(e, onReturnToForm: _returnToLastForm); } } else { AppLogger.e('SceneEditor', '流式生成其他错误', e); } // 异常情况下也要重置状态 if (mounted) { setState(() { _isAIGenerating = false; }); } // 同步:如果是场景节拍生成,错误时将状态置为 error,以恢复按钮可用 try { final bool isSceneBeat = request.requestType == AIRequestType.sceneBeat; final String? sid = request.sceneId ?? widget.sceneId; if (isSceneBeat && sid != null && sid.isNotEmpty) { SceneBeatDataManager.instance.updateSceneStatus(sid, SceneBeatStatus.error); } } catch (e2) { AppLogger.w('SceneEditor', '同步场景节拍错误状态失败', e2); } } finally { // 最终确保状态被重置 if (mounted && _isAIGenerating) { setState(() { _isAIGenerating = false; }); AppLogger.i('SceneEditor', '🔄 最终重置AI生成状态'); } } } /// 🚀 新增:同步的逐字符追加文本方法,避免异步延迟 Future _appendTextCharByCharSync(String text) async { try { // 合并当前收到的内容,帧级批量插入,避免字符级频繁更新 _pendingStreamText += text; await Future.delayed(Duration.zero); if (!mounted || !_isAIGenerating || _pendingStreamText.isEmpty) return; final String batch = _pendingStreamText; _pendingStreamText = ''; // 插入整段文本 widget.controller.document.insert(_lastInsertedOffset, batch); // 🎨 为新插入的文本整体添加AI生成标识 AIGeneratedContentProcessor.markAsAIGenerated( controller: widget.controller, startOffset: _lastInsertedOffset, length: batch.length, ); _generatedText += batch; _lastInsertedOffset += batch.length; _aiGeneratedWordCount = _generatedText.length; if (mounted) { setState(() {}); } } catch (e) { AppLogger.e('SceneEditor', '批量插入过程中出错', e); // 🚀 恢复隐藏的文本样式 _restoreHiddenTextOnError(); // 如果出错,确保停止生成状态 if (mounted) { setState(() { _isAIGenerating = false; }); } } } /// 逐字符追加文本(保留原方法以防其他地方调用) Future _appendTextCharByChar(String text) async { // 🚀 直接调用同步版本 await _appendTextCharByCharSync(text); } /// 应用生成的文本 void _handleApplyGeneration() { AppLogger.i('SceneEditor', '✅ 应用AI生成的文本'); // 🎨 移除AI生成标识,将内容转为正常文本 if (_generatedText.isNotEmpty) { final startOffset = _lastInsertedOffset - _generatedText.length; AIGeneratedContentProcessor.removeAIGeneratedMarks( controller: widget.controller, startOffset: startOffset, length: _generatedText.length, ); } // 🆕 同时移除所有隐藏文本标识(如果是重构,隐藏的原文本将被永久删除) AIGeneratedContentProcessor.clearAllAIGeneratedMarks(controller: widget.controller); // 🗑️ 清除所有隐藏文本标识并物理删除被隐藏的文本 _removeAllHiddenText(); // 隐藏AI工具栏并重置状态 setState(() { _showAIToolbar = false; _isAIGenerating = false; _generatedText = ''; _aiGeneratedWordCount = 0; _pendingStreamText = ''; }); AppLogger.i('SceneEditor', '🎯 AI生成内容已应用为正常文本'); // 📝 现在保存(隐藏文本已被自动过滤掉) _onTextChanged(jsonEncode(widget.controller.document.toDelta().toJson())); } /// 🆕 移除所有隐藏文本(物理删除) void _removeAllHiddenText() { try { final hiddenRanges = AIGeneratedContentProcessor.getHiddenTextRanges( controller: widget.controller, ); if (hiddenRanges.isEmpty) return; AppLogger.i('SceneEditor', '🗑️ 物理删除 ${hiddenRanges.length} 个隐藏文本段落'); // 从后往前删除,避免位置偏移问题 final sortedRanges = hiddenRanges.toList()..sort((a, b) => b.start.compareTo(a.start)); for (final range in sortedRanges) { widget.controller.document.delete(range.start, range.length); ////AppLogger.d('SceneEditor', '删除隐藏文本: 位置${range.start}, 长度${range.length}'); } AppLogger.i('SceneEditor', '✅ 所有隐藏文本已物理删除'); } catch (e) { AppLogger.e('SceneEditor', '删除隐藏文本失败', e); } } /// 重新生成 void _handleRetryGeneration() { AppLogger.i('SceneEditor', '🔄 重新生成AI文本'); // 删除已生成的文本 if (_generatedText.isNotEmpty) { final startOffset = _lastInsertedOffset - _generatedText.length; widget.controller.document.delete(startOffset, _generatedText.length); _lastInsertedOffset = startOffset; } // 🆕 如果有隐藏文本,保持隐藏状态(重构模式重试时不恢复原文本) if (AIGeneratedContentProcessor.hasAnyHiddenText(controller: widget.controller)) { AppLogger.i('SceneEditor', '🔄 重构模式:检测到隐藏文本,保持隐藏状态准备重新生成'); } // 重置状态并重新开始生成 setState(() { _generatedText = ''; _aiGeneratedWordCount = 0; _currentStreamIndex = 0; _isAIGenerating = true; }); // 🚀 修改:检查是否有保存的请求,有则重新发起,没有则使用模拟数据 if (_lastAIRequest != null && _lastUnifiedModel != null) { AppLogger.i('SceneEditor', '📡 重新发起AI请求: ${_lastAIRequest!.requestType.value}'); _startStreamingGeneration(_lastAIRequest!); } else { AppLogger.w('SceneEditor', '⚠️ 没有保存的请求,使用模拟数据'); _simulateStreamingGeneration(); } } /// 模拟流式生成(用于测试) void _simulateStreamingGeneration() { AppLogger.i('SceneEditor', '🧪 模拟流式生成测试'); const testText = '这是一段AI生成的测试文本,用于演示流式输出功能。文字会一个个地出现,营造出AI正在思考和写作的感觉。每个字符都会有一定的延迟,让用户感受到AI的创作过程。'; // 逐字符显示文本 _appendTextCharByChar(testText).then((_) { // 生成完成 setState(() { _isAIGenerating = false; }); AppLogger.i('SceneEditor', '✅ 模拟流式生成完成'); }); } /// 丢弃生成的文本 void _handleDiscardGeneration() { AppLogger.i('SceneEditor', '❌ 丢弃AI生成的文本'); // 首先停止生成(如果正在生成中) final wasGenerating = _isAIGenerating; // 删除已生成的文本 if (_generatedText.isNotEmpty) { final startOffset = _lastInsertedOffset - _generatedText.length; widget.controller.document.delete(startOffset, _generatedText.length); } // 🆕 恢复所有隐藏文本(移除隐藏标识,让原文本重新显示) AIGeneratedContentProcessor.removeHiddenMarks(controller: widget.controller); AppLogger.i('SceneEditor', '👁️ 已恢复所有隐藏的原文本'); // 隐藏AI工具栏并重置状态 setState(() { _showAIToolbar = false; _isAIGenerating = false; _generatedText = ''; _aiGeneratedWordCount = 0; _pendingStreamText = ''; }); if (wasGenerating) { AppLogger.i('SceneEditor', '🛑 AI生成已停止并丢弃'); } else { AppLogger.i('SceneEditor', '🗑️ AI生成的文本已丢弃'); } } /// 分段处理 void _handleSectionGeneration() { AppLogger.i('SceneEditor', '📝 处理分段'); // TODO: 实现分段功能 } /// 停止生成 void _handleStopGeneration() { AppLogger.i('SceneEditor', '🛑 停止AI生成'); // 立即停止生成状态 setState(() { _isAIGenerating = false; }); AppLogger.i('SceneEditor', '✅ AI生成已手动停止'); } /// 🚀 新增:在错误发生时恢复隐藏的文本样式 void _restoreHiddenTextOnError() { try { // 检查是否有隐藏文本(重构/缩写时应用的横杠样式) if (AIGeneratedContentProcessor.hasAnyHiddenText(controller: widget.controller)) { AppLogger.i('SceneEditor', '🔄 检测到隐藏文本,恢复原文本样式(移除横杠)'); // 移除隐藏标识,恢复原文本显示 AIGeneratedContentProcessor.removeHiddenMarks(controller: widget.controller); AppLogger.i('SceneEditor', '✅ 隐藏文本样式已恢复'); } } catch (e) { AppLogger.e('SceneEditor', '恢复隐藏文本样式失败', e); } } /// 🚀 新增:返回表单回调 void _returnToLastForm() { if (_lastAIRequest == null || _lastSelectedText == null) { AppLogger.w('SceneEditor', '没有保存的请求信息,无法返回表单'); return; } AppLogger.i('SceneEditor', '返回表单: ${_lastAIRequest!.requestType}, 文本长度: ${_lastSelectedText!.length}'); // 🚀 获取必要的数据(从EditorBloc中获取) Novel? novel; List settings = []; List settingGroups = []; List snippets = []; final editorBloc = widget.editorBloc; if (editorBloc.state is editor_bloc.EditorLoaded) { final state = editorBloc.state as editor_bloc.EditorLoaded; novel = state.novel; // TODO: 从状态中获取 settings, settingGroups, snippets // 暂时使用空列表,后续可以完善 } // 🚀 从保存的请求中提取表单参数 final lastRequest = _lastAIRequest!; final instructions = lastRequest.instructions; final enableSmartContext = lastRequest.enableSmartContext; final contextSelections = lastRequest.contextSelections; // 🚀 从参数中提取长度/风格等特定设置 String? length; String? style; if (lastRequest.parameters != null) { length = lastRequest.parameters!['length']?.toString(); style = lastRequest.parameters!['style']?.toString(); } // 🚀 根据请求类型显示对应的表单,传递保存的参数 switch (lastRequest.requestType) { case AIRequestType.expansion: showExpansionDialog( context, selectedText: _lastSelectedText!, // selectedModel: _lastAIModel, // 已废弃,使用initialSelectedUnifiedModel novel: novel, settings: settings, settingGroups: settingGroups, snippets: snippets, // 🚀 恢复之前的设置 initialInstructions: instructions, initialLength: length, initialEnableSmartContext: enableSmartContext, initialContextSelections: contextSelections, initialSelectedUnifiedModel: _lastUnifiedModel, onStreamingGenerate: (request, model) { _handleStreamingGenerationStarted(request, model); }, ); break; case AIRequestType.refactor: showRefactorDialog( context, selectedText: _lastSelectedText!, // selectedModel: _lastAIModel, // 已废弃,使用initialSelectedUnifiedModel novel: novel, settings: settings, settingGroups: settingGroups, snippets: snippets, // 🚀 恢复之前的设置 initialInstructions: instructions, initialStyle: style, initialEnableSmartContext: enableSmartContext, initialContextSelections: contextSelections, initialSelectedUnifiedModel: _lastUnifiedModel, onStreamingGenerate: (request, model) { _handleStreamingGenerationStarted(request, model); }, ); break; case AIRequestType.summary: showSummaryDialog( context, selectedText: _lastSelectedText!, // selectedModel: _lastAIModel, // 已废弃,使用initialSelectedUnifiedModel novel: novel, settings: settings, settingGroups: settingGroups, snippets: snippets, // 🚀 恢复之前的设置 initialInstructions: instructions, initialLength: length, initialEnableSmartContext: enableSmartContext, initialContextSelections: contextSelections, initialSelectedUnifiedModel: _lastUnifiedModel, onStreamingGenerate: (request, model) { _handleStreamingGenerationStarted(request, model); }, ); break; default: AppLogger.w('SceneEditor', '不支持的请求类型: ${lastRequest.requestType}'); TopToast.error(context, '不支持的请求类型'); } } /// 🚀 修改:显示积分不足对话框,支持返回表单 void _showInsufficientCreditsDialog(InsufficientCreditsException ex, {VoidCallback? onReturnToForm}) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return AlertDialog( title: Row( children: [ Icon( Icons.account_balance_wallet, color: Theme.of(context).colorScheme.error, ), const SizedBox(width: 8), const Text('积分余额不足'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(ex.message), const SizedBox(height: 16), if (ex.requiredCredits != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( Icons.info_outline, color: Theme.of(context).colorScheme.error, size: 16, ), const SizedBox(width: 8), Expanded( child: Text( '本次操作需要 ${ex.requiredCredits} 积分', style: TextStyle( color: Theme.of(context).colorScheme.onErrorContainer, fontSize: 14, ), ), ), ], ), ), const SizedBox(height: 16), ], const Text( '您可以:', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 8), const Text('• 充值积分以继续使用公共模型'), const Text('• 配置私有模型(使用自己的API Key)'), const Text('• 选择其他更便宜的模型'), ], ), actions: [ TextButton( onPressed: () { Navigator.of(dialogContext).pop(); // 🚀 恢复隐藏的文本样式 _restoreHiddenTextOnError(); // 重置AI工具栏状态 setState(() { _showAIToolbar = false; _isAIGenerating = false; }); }, child: const Text('取消'), ), if (onReturnToForm != null) // 🚀 只有当有返回表单回调时才显示 TextButton( onPressed: () { Navigator.of(dialogContext).pop(); // 🚀 恢复隐藏的文本样式 _restoreHiddenTextOnError(); // 🚀 重新显示选择工具栏 setState(() { _showToolbar = true; _showAIToolbar = false; _isAIGenerating = false; }); // 🚀 调用返回表单回调 onReturnToForm(); }, style: TextButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.primary, ), child: const Text('返回表单'), ), ElevatedButton( onPressed: () { Navigator.of(dialogContext).pop(); // TODO: 跳转到充值页面或设置页面 // Navigator.pushNamed(context, '/settings/credits'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('跳转到积分充值页面(功能开发中)')), ); }, child: const Text('去充值'), ), ], ); }, ); } // 监听内容加载完成,重新处理设定引用 void _setupContentLoadListener() { widget.editorBloc.stream.listen((state) { if (!mounted) return; // 当内容发生变化时,重新处理设定引用 if (state is editor_bloc.EditorLoaded) { // 延迟执行,确保UI已更新 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { ////AppLogger.d('SceneEditor', '📝 内容加载完成,重新处理设定引用: ${widget.sceneId}'); _checkAndProcessSettingReferences(); } }); } }); } // 添加EditorBloc状态监听,确保摘要控制器内容与模型保持同步 void _setupBlocListener() { widget.editorBloc.stream.listen((state) { if (!mounted) return; if (state is editor_bloc.EditorLoaded && widget.sceneId != null && widget.actId != null && widget.chapterId != null) { try { // 使用更安全的查找方式 bool found = false; String? modelSummaryContent; // 遍历所有元素查找指定场景 for (final act in state.novel.acts) { if (act.id == widget.actId) { for (final chapter in act.chapters) { if (chapter.id == widget.chapterId) { for (final scene in chapter.scenes) { if (scene.id == widget.sceneId) { found = true; modelSummaryContent = scene.summary.content ?? ''; break; } } if (found) break; } } if (found) break; } } // 如果场景不存在,则提前返回 if (!found) { ////AppLogger.d('SceneEditor', '跳过摘要同步:场景不存在或已被删除: ${widget.sceneId}'); return; } // 如果用户正在编辑摘要,避免用模型内容覆盖用户输入 if (_summaryFocusNode.hasFocus) { return; } // 当前控制器中的文本 final currentControllerText = widget.summaryController.text; // 仅当摘要控制器内容与模型不同时更新 if (currentControllerText != modelSummaryContent) { // 判断变更方向 if (currentControllerText.isNotEmpty && (modelSummaryContent == null || modelSummaryContent.isEmpty)) { // 如果控制器有内容但模型为空,说明是用户刚输入了内容但可能未保存成功 // 重新触发保存操作确保内容被保存 AppLogger.i('SceneEditor', '检测到摘要未同步到模型,重新保存: ${widget.sceneId}'); // 将更新放在下一帧执行,避免在build过程中修改 Future.microtask(() { if (mounted) { // 触发摘要保存并强制重建UI以确保更新成功 widget.editorBloc.add(editor_bloc.UpdateSummary( novelId: widget.editorBloc.novelId, actId: widget.actId!, chapterId: widget.chapterId!, sceneId: widget.sceneId!, summary: currentControllerText, shouldRebuild: true, // 强制重建UI )); } }); } else if (modelSummaryContent != null && modelSummaryContent.isNotEmpty) { // 模型中有内容但控制器不同,更新控制器 AppLogger.i('SceneEditor', '摘要内容从模型同步到控制器: ${widget.sceneId}'); // 将更新放在下一帧执行,避免在build过程中修改 Future.microtask(() { if (mounted) { widget.summaryController.text = modelSummaryContent!; } }); } } } catch (e, stackTrace) { // 记录详细错误信息但不抛出异常 AppLogger.i('SceneEditor', '同步摘要控制器失败,可能是场景已被删除: ${widget.sceneId}'); AppLogger.v('SceneEditor', '同步摘要控制器详细错误: ${e.toString()}', e, stackTrace); } } }); } // 🚀 新增:设置摘要滚动固定监听 void _setupSummaryScrollListener() { // 查找父级滚动控制器 WidgetsBinding.instance.addPostFrameCallback((_) { _findParentScrollController(); }); } // 🚀 新增:查找父级滚动控制器 void _findParentScrollController() { try { // 通过context查找最近的Scrollable final scrollableState = Scrollable.maybeOf(context); if (scrollableState != null) { _parentScrollController = scrollableState.widget.controller; if (_parentScrollController != null) { _parentScrollController!.addListener(_onParentScroll); ////AppLogger.d('SceneEditor', '已找到并监听父级滚动控制器: ${widget.sceneId}'); } } } catch (e) { AppLogger.w('SceneEditor', '查找父级滚动控制器失败: ${widget.sceneId}', e); } } // 🚀 新增:父级滚动监听 void _onParentScroll() { if (!mounted || _parentScrollController == null) return; // 🚀 优化:使用requestAnimationFrame的思路,在下一帧更新位置 _scrollPositionTimer?.cancel(); _scrollPositionTimer = Timer(Duration.zero, () { if (mounted) { // 使用WidgetsBinding.instance.addPostFrameCallback确保在下一帧执行 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _updateSummaryPosition(); } }); } }); } // 🚀 新增:更新摘要位置 void _updateSummaryPosition() { if (!mounted) return; try { // 🚀 优化:首先更新摘要组件的实际高度 _updateSummaryHeight(); // 获取场景容器的位置信息 final sceneRenderBox = _sceneContainerKey.currentContext?.findRenderObject() as RenderBox?; if (sceneRenderBox == null) return; // 获取场景容器在屏幕中的位置 final scenePosition = sceneRenderBox.localToGlobal(Offset.zero); final sceneSize = sceneRenderBox.size; // 🚀 新增:检查场景高度,如果太小则不启用粘性滚动 if (sceneSize.height < _minSceneHeightForSticky) { // 🚀 获取场景内容长度用于日志 final contentLength = widget.controller.document.toPlainText().trim().length; //AppLogger.v('SceneEditor', '场景高度过小(${sceneSize.height}px < $_minSceneHeightForSticky),内容长度: $contentLength,跳过粘性滚动: ${widget.sceneId}'); // 重置为非粘性状态 if (_isSummarySticky || _summaryTopOffsetVN.value != 0.0) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _summaryTopOffsetVN.value = 0.0; _isSummarySticky = false; }); _lastCalculatedOffset = 0.0; _lastStickyState = false; } }); } return; } // 获取屏幕可视区域 final mediaQuery = MediaQuery.of(context); final screenHeight = mediaQuery.size.height; final topPadding = mediaQuery.padding.top; final viewportTop = topPadding; final viewportBottom = screenHeight; // 计算场景在视口中的位置 final sceneTop = scenePosition.dy; final sceneBottom = sceneTop + sceneSize.height; double newTopOffset = 0.0; bool newStickyState = false; // 🚀 优化:计算安全的最大偏移,包含更多边距和底部工具栏高度 const totalMargin = _summaryTopMargin + _summaryBottomMargin + _bottomToolbarHeight; final maxOffset = (sceneSize.height - _summaryHeight - totalMargin).clamp(0.0, sceneSize.height - totalMargin); // 🚀 优化:添加顶部边距到视口计算 final adjustedViewportTop = viewportTop + _summaryTopMargin; // 场景顶部在视口上方,底部在视口内 - 摘要固定在视口顶部 if (sceneTop < adjustedViewportTop && sceneBottom > adjustedViewportTop) { newTopOffset = (adjustedViewportTop - sceneTop).clamp(0.0, maxOffset); newStickyState = true; } // 场景完全在视口内 - 摘要跟随场景顶部 else if (sceneTop >= adjustedViewportTop && sceneBottom <= viewportBottom) { newTopOffset = _summaryTopMargin; // 🚀 保持顶部边距 newStickyState = false; } // 场景顶部在视口内,底部在视口下方 - 摘要固定但不超出场景底部 else if (sceneTop < viewportBottom && sceneBottom > viewportBottom) { // 🚀 优化:考虑边距,确保摘要不会超出场景底部 final idealOffset = adjustedViewportTop - sceneTop; newTopOffset = idealOffset.clamp(_summaryTopMargin, maxOffset); newStickyState = true; } // 场景完全在视口外 - 摘要跟随场景 else { newTopOffset = _summaryTopMargin; // 🚀 保持顶部边距 newStickyState = false; } // 🚀 优化:使用更大的阈值减少闪烁,并检查状态变化 final offsetChanged = (_lastCalculatedOffset - newTopOffset).abs() > _positionThreshold; final stickyChanged = _lastStickyState != newStickyState; if (offsetChanged || stickyChanged) { // 🚀 优化:使用WidgetsBinding.instance.addPostFrameCallback确保UI更新的平滑性 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _summaryTopOffsetVN.value = newTopOffset; _isSummarySticky = newStickyState; }); // 更新缓存的值 _lastCalculatedOffset = newTopOffset; _lastStickyState = newStickyState; //AppLogger.v('SceneEditor', '摘要位置更新: offset=$newTopOffset, sticky=$newStickyState, height=$_summaryHeight, sceneHeight=${sceneSize.height}, 场景=${widget.sceneId}'); } }); } } catch (e) { AppLogger.w('SceneEditor', '更新摘要位置失败: ${widget.sceneId}', e); } } // 🚀 新增:更新摘要组件的实际高度 void _updateSummaryHeight() { try { final summaryRenderBox = _summaryKey.currentContext?.findRenderObject() as RenderBox?; if (summaryRenderBox != null) { final actualHeight = summaryRenderBox.size.height; if ((actualHeight - _summaryHeight).abs() > 5.0) { // 只在高度变化超过5px时更新 _summaryHeight = actualHeight; AppLogger.v('SceneEditor', '摘要高度更新: $_summaryHeight, 场景=${widget.sceneId}'); } } } catch (e) { AppLogger.v('SceneEditor', '获取摘要高度失败,使用默认值: ${widget.sceneId}', e); } } // 🚀 新增:移除摘要滚动监听 void _removeSummaryScrollListener() { if (_parentScrollController != null) { _parentScrollController!.removeListener(_onParentScroll); ////AppLogger.d('SceneEditor', '已移除父级滚动监听: ${widget.sceneId}'); } } // 🚀 新增:显示摘要生成器 void _showSummaryGenerator() { // 显示AI摘要面板(使用侧边栏方式) final layoutManager = context.read(); layoutManager.showAISummaryPanel(); } // 🚀 新增:显示场景节拍面板 void _showSceneBeatPanel() { if (widget.sceneId == null) return; AppLogger.i('SceneEditor', '🎯 显示场景节拍面板: ${widget.sceneId}'); // 🚀 新增:获取编辑器状态管理器 EditorScreenController? editorController; EditorLayoutManager? layoutManager; try { editorController = Provider.of(context, listen: false); layoutManager = Provider.of(context, listen: false); AppLogger.d('SceneEditor', '✅ 成功获取编辑器状态管理器'); } catch (e) { AppLogger.w('SceneEditor', '⚠️ 获取编辑器状态管理器失败: $e'); } // 使用Overlay场景节拍管理器显示面板 OverlaySceneBeatManager.instance.show( context: context, sceneId: widget.sceneId!, novel: widget.novel, settings: widget.settings, settingGroups: widget.settingGroups, snippets: widget.snippets, // 🚀 新增:传递编辑器状态管理器 editorController: editorController, layoutManager: layoutManager, onGenerate: (sceneId, request, model) { // 触发场景节拍生成 AppLogger.i('SceneEditor', '🚀 触发场景节拍生成: $sceneId, 模型: ${model.displayName}'); startSceneBeatGeneration( request: request, model: model, onGenerationComplete: () { AppLogger.i('SceneEditor', '✅ 场景节拍生成完成: $sceneId'); }, ); }, ); } /// 🚀 新增:公开方法,用于从外部触发场景节拍的AI生成 void startSceneBeatGeneration({ required UniversalAIRequest request, required UnifiedAIModel model, VoidCallback? onGenerationComplete, }) { AppLogger.i('SceneEditor', '🎯 接收到场景节拍生成请求: ${model.displayName}'); // 🚀 若存在未应用的AI生成内容或隐藏文本,先自动应用为正文,确保新请求包含最新上下文 try { final bool hasAIGen = AIGeneratedContentProcessor.hasAnyAIGeneratedContent( controller: widget.controller, ); final bool hasHidden = AIGeneratedContentProcessor.hasAnyHiddenText( controller: widget.controller, ); if (hasAIGen || hasHidden) { if (_isAIGenerating) { _handleStopGeneration(); } _handleApplyGeneration(); } } catch (_) {} // 🚀 新增:保存请求和统一模型配置,用于重试 _lastAIRequest = request; _lastUnifiedModel = model; _lastSelectedText = ''; // 场景节拍没有选中文本 _aiGeneratedStartOffset = _lastInsertedOffset; // 记录AI生成内容的起始位置 AppLogger.i('SceneEditor', '🚀 开始场景节拍流式生成,插入位置: $_lastInsertedOffset'); // 🚀 修复:延迟一帧显示AI工具栏,确保光标位置和LayerLink目标正确计算 WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; // 显示AI工具栏并设置生成状态 setState(() { _showToolbar = false; _showAIToolbar = true; _isAIGenerating = true; _aiModelName = model.displayName; _generatedText = ''; _aiGeneratedWordCount = 0; _currentStreamIndex = 0; _pendingStreamText = ''; }); AppLogger.i('SceneEditor', '✅ AI工具栏已显示,LayerLink目标应该已正确定位'); // 🚀 滚动到光标位置,确保AI工具栏可见 WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToCursorPosition(); }); // 保存回调 _onSceneBeatGenerationComplete = onGenerationComplete; // 开始流式生成 _startStreamingGeneration(request); }); } /// 🚀 新增:滚动到光标位置,确保AI工具栏可见 void _scrollToCursorPosition() { try { if (_editorContentKey.currentContext != null) { Scrollable.ensureVisible( _editorContentKey.currentContext!, alignment: 1.0, // 将目标放在视口底部 duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } catch (e) { AppLogger.e('SceneEditor', '滚动到光标位置失败', e); } } // 🚀 新增:保存生成完成回调 VoidCallback? _onSceneBeatGenerationComplete; } /// 🚀 新增:摘要操作按钮组件 class _SummaryActionButton extends StatelessWidget { const _SummaryActionButton({ required this.icon, required this.label, this.tooltip, this.onPressed, }); final IconData icon; final String label; final String? tooltip; final VoidCallback? onPressed; @override Widget build(BuildContext context) { return Tooltip( message: tooltip ?? label, child: TextButton.icon( onPressed: onPressed ?? () {}, icon: Icon(icon, size: 12, color: WebTheme.getSecondaryTextColor(context)), // 🚀 减小图标尺寸 label: Text( label, style: TextStyle( fontSize: 10, // 🚀 减小字体尺寸 color: WebTheme.getSecondaryTextColor(context), fontWeight: FontWeight.w500, ), ), style: TextButton.styleFrom( foregroundColor: WebTheme.getSecondaryTextColor(context), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), // 🚀 减小内边距 minimumSize: const Size(0, 24), // 🚀 减小最小尺寸 tapTargetSize: MaterialTapTargetSize.shrinkWrap, // 🚀 收缩点击目标尺寸 shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3)), // 🚀 减小圆角 visualDensity: VisualDensity.compact, ).copyWith( overlayColor: WidgetStateProperty.resolveWith( (Set states) { if (states.contains(WidgetState.hovered)) { return WebTheme.getSurfaceColor(context).withOpacity(0.8); } return null; }, ), ), ), ); } }