Files
MaliangAINovalWriter/AINoval/lib/screens/setting_generation/widgets/setting_node_widget.dart
2025-09-10 00:07:52 +08:00

433 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:ainoval/utils/web_theme.dart';
import '../../../models/setting_node.dart';
import '../../../blocs/setting_generation/setting_generation_state.dart'; // 导入渲染状态
/// 设定节点组件
class SettingNodeWidget extends StatefulWidget {
final SettingNode node;
final String? selectedNodeId;
final String viewMode;
final int level;
final Function(String nodeId) onTap;
// 渲染状态参数
final Set<String> renderedNodeIds;
final Map<String, NodeRenderInfo> nodeRenderStates;
// 是否渲染子节点(用于流式列表避免重复渲染)
final bool renderChildren;
const SettingNodeWidget({
Key? key,
required this.node,
this.selectedNodeId,
required this.viewMode,
required this.level,
required this.onTap,
this.renderedNodeIds = const {},
this.nodeRenderStates = const {},
this.renderChildren = true,
}) : super(key: key);
@override
State<SettingNodeWidget> createState() => _SettingNodeWidgetState();
}
class _SettingNodeWidgetState extends State<SettingNodeWidget>
with TickerProviderStateMixin {
bool _isExpanded = true;
late AnimationController _renderingController; // 渲染动画控制器
late Animation<double> _renderingAnimation;
@override
void initState() {
super.initState();
// 渲染动画控制器
_renderingController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_renderingAnimation = CurvedAnimation(
parent: _renderingController,
curve: Curves.easeOutBack,
);
// 检查初始渲染状态
_checkRenderingState();
}
@override
void dispose() {
_renderingController.dispose();
super.dispose();
}
@override
void didUpdateWidget(SettingNodeWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// 检查渲染状态变化
_checkRenderingState();
}
/// 检查并处理渲染状态变化
void _checkRenderingState() {
final renderInfo = widget.nodeRenderStates[widget.node.id];
if (renderInfo?.state == NodeRenderState.rendering) {
// 开始渲染动画
_renderingController.forward();
} else if (renderInfo?.state == NodeRenderState.rendered) {
// 确保渲染动画完成
_renderingController.value = 1.0;
}
}
@override
Widget build(BuildContext context) {
// 🔧 关键修复始终返回相同的widget结构用Opacity控制可见性
return _buildAlwaysStableWidget();
}
/// 🔧 核心修复构建绝对稳定的widget永远不改变结构
Widget _buildAlwaysStableWidget() {
final renderInfo = widget.nodeRenderStates[widget.node.id];
final isRendering = renderInfo?.state == NodeRenderState.rendering;
final isRendered = widget.renderedNodeIds.contains(widget.node.id);
// 🔧 关键确定最终可见性但不改变widget树结构
final shouldShow = isRendered || isRendering;
final opacity = shouldShow ? 1.0 : 0.0;
// 🔧 绝对稳定的widget结构始终存在只改变可见性
Widget nodeContent = Column(
key: ValueKey('stable_node_${widget.node.id}'),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildNodeHeader(),
// 🔧 子节点容器:始终存在,只改变内容可见性
if (widget.renderChildren && widget.node.children != null && widget.node.children!.isNotEmpty)
_buildStableChildrenContainer(),
],
);
// 🔧 使用Opacity + IgnorePointer确保不可见时完全不可交互
Widget result = Opacity(
opacity: opacity,
child: IgnorePointer(
ignoring: !shouldShow,
child: nodeContent,
),
);
// 🔧 只有在渲染中时才应用动画效果
if (isRendering) {
result = AnimatedBuilder(
animation: _renderingAnimation,
builder: (context, child) {
return Transform.scale(
scale: 0.95 + (_renderingAnimation.value * 0.05),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: const Color(0xFF3B82F6).withOpacity(0.15 * _renderingAnimation.value),
blurRadius: 4 * _renderingAnimation.value,
spreadRadius: 1 * _renderingAnimation.value,
),
],
),
child: child,
),
);
},
child: result,
);
}
return result;
}
/// 🔧 构建稳定的子节点容器:始终分配所有空间
Widget _buildStableChildrenContainer() {
// 使用 AnimatedSize + ClipRect 避免从 null 到 0 的高度动画在 Web 上导致异常
return ClipRect(
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: Alignment.topLeft,
curve: Curves.easeInOut,
child: _isExpanded
? Container(
padding: const EdgeInsets.only(top: 4),
child: _buildAbsolutelyStableChildrenList(),
)
: const SizedBox.shrink(),
),
);
}
/// 🔧 终极修复:构建绝对稳定的子节点列表
Widget _buildAbsolutelyStableChildrenList() {
if (widget.node.children == null || widget.node.children!.isEmpty) {
return const SizedBox.shrink();
}
// 🔧 终极方案:为所有子节点预分配固定空间,每个子节点自己控制可见性
// 这确保Column的children数量和类型永远不变
return Column(
key: ValueKey('stable_children_${widget.node.id}'),
mainAxisSize: MainAxisSize.min,
children: widget.node.children!.map((child) {
return Container(
key: ValueKey('stable_child_container_${child.id}'),
margin: const EdgeInsets.only(bottom: 4),
child: SettingNodeWidget(
key: ValueKey('stable_child_widget_${child.id}'),
node: child,
selectedNodeId: widget.selectedNodeId,
viewMode: widget.viewMode,
level: widget.level + 1,
onTap: widget.onTap,
renderedNodeIds: widget.renderedNodeIds,
nodeRenderStates: widget.nodeRenderStates,
),
);
}).toList(),
);
}
Widget _buildNodeHeader() {
final isDark = Theme.of(context).brightness == Brightness.dark;
final renderInfo = widget.nodeRenderStates[widget.node.id];
final isRendering = renderInfo?.state == NodeRenderState.rendering;
// 只有当前节点被选中时才显示选中状态,子节点不继承
final isCurrentNodeSelected = widget.selectedNodeId == widget.node.id;
// 根据Node.js版本的 paddingLeft: `${level * 1.5 + 0.5}rem`
final leftPadding = widget.level * 24.0 + 8.0; // 1rem = 16px, 1.5rem = 24px
return InkWell(
onTap: () => widget.onTap(widget.node.id),
borderRadius: BorderRadius.circular(6),
child: Container(
width: double.infinity,
padding: EdgeInsets.only(
left: leftPadding,
right: 8,
top: widget.viewMode == 'compact' ? 8 : 12,
bottom: widget.viewMode == 'compact' ? 8 : 12,
),
decoration: BoxDecoration(
color: _getBackgroundColor(),
borderRadius: BorderRadius.circular(6),
border: isCurrentNodeSelected
? Border.all(
color: const Color(0xFF6366F1), // indigo-500
width: 2,
)
: isRendering
? Border.all(
color: const Color(0xFF3B82F6), // blue-500
width: 1,
)
: null,
),
child: Row(
crossAxisAlignment: widget.viewMode == 'compact'
? CrossAxisAlignment.center
: CrossAxisAlignment.start,
children: [
// Rendering indicator
if (isRendering)
Container(
width: 8,
height: 8,
margin: const EdgeInsets.only(right: 8, top: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
const Color(0xFF3B82F6), // blue-500
),
),
),
// Expand/collapse icon
InkWell(
onTap: _toggleExpanded,
borderRadius: BorderRadius.circular(4),
child: Container(
width: 16,
height: 16,
margin: EdgeInsets.only(
right: 8,
top: widget.viewMode == 'detailed' ? 4 : 0,
),
child: (widget.renderChildren && widget.node.children != null && widget.node.children!.isNotEmpty)
? AnimatedRotation(
turns: _isExpanded ? 0.25 : 0,
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.chevron_right,
size: 16,
color: const Color(0xFF6B7280), // gray-500
),
)
: Icon(
Icons.description,
size: 16,
color: isDark
? const Color(0xFF4B5563) // gray-600 dark
: const Color(0xFF9CA3AF), // gray-400
),
),
),
// Node content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// 状态图标(小)
Padding(
padding: const EdgeInsets.only(right: 6),
child: _buildStatusIcon(),
),
Expanded(
child: Text(
widget.node.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isCurrentNodeSelected
? const Color(0xFF6366F1) // indigo-500
: isRendering
? const Color(0xFF3B82F6) // blue-500
: (isDark
? const Color(0xFFF9FAFB)
: const Color(0xFF111827)),
),
),
),
const SizedBox(width: 6),
_buildTypeChip(),
if (isRendering)
Text(
'生成中...',
style: TextStyle(
fontSize: 12,
color: const Color(0xFF3B82F6),
fontStyle: FontStyle.italic,
),
),
],
),
if (widget.viewMode == 'detailed' && widget.node.description.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
widget.node.description,
style: TextStyle(
fontSize: 14,
color: isDark
? const Color(0xFF9CA3AF) // gray-400 dark
: const Color(0xFF6B7280), // gray-500
height: 1.5,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
);
}
Widget _buildStatusIcon() {
// 移除“待处理”状态下的时钟图标
if (widget.node.generationStatus == GenerationStatus.pending) {
return const SizedBox.shrink();
}
IconData icon;
Color color;
switch (widget.node.generationStatus) {
case GenerationStatus.generating:
icon = Icons.autorenew;
color = Colors.blue;
break;
case GenerationStatus.completed:
icon = Icons.check_circle;
color = Colors.green;
break;
case GenerationStatus.failed:
icon = Icons.error;
color = Colors.red;
break;
case GenerationStatus.modified:
icon = Icons.edit;
color = Colors.purple;
break;
case GenerationStatus.pending:
// 已在上方提前返回
icon = Icons.check_circle; // 占位,不会被使用
color = Colors.transparent;
break;
}
return Icon(
icon,
size: 14,
color: color,
);
}
Widget _buildTypeChip() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: WebTheme.getPrimaryColor(context).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: WebTheme.getPrimaryColor(context).withOpacity(0.3),
width: 1,
),
),
child: Text(
widget.node.type.displayName,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: WebTheme.getPrimaryColor(context),
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
);
}
Color _getBackgroundColor() {
final isDark = Theme.of(context).brightness == Brightness.dark;
if (widget.selectedNodeId == widget.node.id) {
return isDark
? const Color(0xFF1E1B4B) // indigo-900/50 dark
: const Color(0xFFE0E7FF); // indigo-100
} else {
return Colors.transparent;
}
}
void _toggleExpanded() {
setState(() {
_isExpanded = !_isExpanded;
});
}
}