import 'package:flutter/material.dart'; import 'package:ainoval/utils/web_theme.dart'; /// 浮动卡片配置 class FloatingCardConfig { final double? width; final double? height; final double? minWidth; final double? maxWidth; final double? minHeight; final double? maxHeight; final EdgeInsets? margin; final EdgeInsets? padding; final BorderRadius? borderRadius; final Color? backgroundColor; final List? shadows; final Border? border; final Duration animationDuration; final Curve animationCurve; final bool showCloseButton; final bool closeOnBackgroundTap; final bool enableBackgroundTap; final bool showFloatingCloseButton; const FloatingCardConfig({ this.width, this.height, this.minWidth = 300.0, this.maxWidth = 800.0, this.minHeight = 200.0, this.maxHeight = 600.0, this.margin, this.padding, this.borderRadius, this.backgroundColor, this.shadows, this.border, this.animationDuration = const Duration(milliseconds: 300), this.animationCurve = Curves.easeOutCubic, this.showCloseButton = true, this.closeOnBackgroundTap = false, this.enableBackgroundTap = true, this.showFloatingCloseButton = true, }); } /// 浮动卡片位置配置 class FloatingCardPosition { final double? left; final double? top; final double? right; final double? bottom; final Alignment? alignment; final double? offsetFromSidebar; const FloatingCardPosition({ this.left, this.top, this.right, this.bottom, this.alignment, this.offsetFromSidebar, }); /// 默认居中位置 static const center = FloatingCardPosition(alignment: Alignment.center); /// 从侧边栏偏移的位置 static FloatingCardPosition fromSidebar({ required double sidebarWidth, double offset = 16.0, double top = 80.0, }) { return FloatingCardPosition( left: sidebarWidth + offset, top: top, ); } } /// 通用浮动卡片管理器 class FloatingCard { static OverlayEntry? _overlayEntry; static bool _isShowing = false; /// 显示浮动卡片 static void show({ required BuildContext context, required Widget child, FloatingCardConfig config = const FloatingCardConfig(), FloatingCardPosition position = FloatingCardPosition.center, VoidCallback? onClose, String? title, List? actions, }) { if (_isShowing) { hide(); } _overlayEntry = _createOverlayEntry( context: context, child: child, config: config, position: position, onClose: onClose, title: title, actions: actions, ); Overlay.of(context).insert(_overlayEntry!); _isShowing = true; } /// 隐藏浮动卡片 static void hide() { if (_overlayEntry != null) { _overlayEntry!.remove(); _overlayEntry = null; _isShowing = false; } } /// 检查是否正在显示 static bool get isShowing => _isShowing; /// 创建 Overlay 条目 static OverlayEntry _createOverlayEntry({ required BuildContext context, required Widget child, required FloatingCardConfig config, required FloatingCardPosition position, VoidCallback? onClose, String? title, List? actions, }) { return OverlayEntry( builder: (context) => Stack( children: [ // 背景遮罩 if (config.enableBackgroundTap) Positioned.fill( child: GestureDetector( onTap: config.closeOnBackgroundTap ? (onClose ?? hide) : null, child: Container( color: config.closeOnBackgroundTap ? Colors.black.withOpacity(0.3) : Colors.transparent, ), ), ) else Positioned.fill( child: IgnorePointer( ignoring: true, child: Container(color: Colors.transparent), ), ), // 浮动卡片 _FloatingCardWidget( child: child, config: config, position: position, onClose: onClose ?? hide, title: title, actions: actions, ), ], ), ); } } /// 浮动卡片组件 class _FloatingCardWidget extends StatefulWidget { final Widget child; final FloatingCardConfig config; final FloatingCardPosition position; final VoidCallback onClose; final String? title; final List? actions; const _FloatingCardWidget({ required this.child, required this.config, required this.position, required this.onClose, this.title, this.actions, }); @override State<_FloatingCardWidget> createState() => _FloatingCardWidgetState(); } class _FloatingCardWidgetState extends State<_FloatingCardWidget> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _slideAnimation; late Animation _fadeAnimation; late Animation _scaleAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: widget.config.animationDuration, vsync: this, ); _slideAnimation = Tween( begin: 400.0, // 改为和原来相同的滑入距离 end: 0.0, ).animate(CurvedAnimation( parent: _animationController, curve: widget.config.animationCurve, )); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeOut, )); _scaleAnimation = Tween( begin: 0.8, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeOutBack, // 保持和原来相同的动画曲线 )); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _handleClose() { _animationController.reverse().then((_) { widget.onClose(); }); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animationController, builder: (context, child) => _buildPositionedCard(), ); } Widget _buildPositionedCard() { final screenSize = MediaQuery.of(context).size; // 计算位置 double? left = widget.position.left; double? top = widget.position.top; double? right = widget.position.right; double? bottom = widget.position.bottom; if (widget.position.alignment != null) { final alignment = widget.position.alignment!; final cardWidth = _calculateCardWidth(screenSize); final cardHeight = _calculateCardHeight(screenSize); switch (alignment) { case Alignment.center: left = (screenSize.width - cardWidth) / 2; top = (screenSize.height - cardHeight) / 2; break; case Alignment.topCenter: left = (screenSize.width - cardWidth) / 2; top = 50; break; case Alignment.bottomCenter: left = (screenSize.width - cardWidth) / 2; bottom = 50; break; // 可以添加更多对齐方式 } } return Stack( children: [ // 主卡片 Positioned( left: left, top: top, right: right, bottom: bottom, child: Transform.translate( offset: Offset(_slideAnimation.value, 0), child: Transform.scale( scale: _scaleAnimation.value, child: Opacity( opacity: _fadeAnimation.value, child: _buildCard(context), ), ), ), ), // 浮动关闭按钮 if (widget.config.showFloatingCloseButton) Positioned( left: (left ?? 0) - 12, top: (top ?? 0) - 12, child: Transform.translate( offset: Offset(_slideAnimation.value, 0), child: Transform.scale( scale: _scaleAnimation.value, child: Opacity( opacity: _fadeAnimation.value, child: _buildFloatingCloseButton(), ), ), ), ), ], ); } double _calculateCardWidth(Size screenSize) { if (widget.config.width != null) return widget.config.width!; double width = screenSize.width * 0.4; // 默认40%屏幕宽度 if (widget.config.minWidth != null) { width = width.clamp(widget.config.minWidth!, double.infinity); } if (widget.config.maxWidth != null) { width = width.clamp(0, widget.config.maxWidth!); } return width; } double _calculateCardHeight(Size screenSize) { if (widget.config.height != null) return widget.config.height!; double height = screenSize.height * 0.6; // 默认60%屏幕高度 if (widget.config.minHeight != null) { height = height.clamp(widget.config.minHeight!, double.infinity); } if (widget.config.maxHeight != null) { height = height.clamp(0, widget.config.maxHeight!); } return height; } Widget _buildCard(BuildContext context) { final isDark = WebTheme.isDarkMode(context); final screenSize = MediaQuery.of(context).size; final cardWidth = _calculateCardWidth(screenSize); final cardHeight = _calculateCardHeight(screenSize); return Material( color: Colors.transparent, child: GestureDetector( onTap: () {}, // 阻止点击穿透 child: Container( width: cardWidth, height: cardHeight, margin: widget.config.margin, padding: widget.config.padding, decoration: BoxDecoration( color: widget.config.backgroundColor ?? (isDark ? WebTheme.darkGrey100 : WebTheme.getBackgroundColor(context)), borderRadius: widget.config.borderRadius ?? BorderRadius.circular(12), border: widget.config.border ?? Border.all( color: isDark ? WebTheme.darkGrey800 : WebTheme.getShadowColor(context, opacity: 0.05), width: 1, ), boxShadow: widget.config.shadows ?? [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.2), offset: const Offset(0, 8), blurRadius: 32, spreadRadius: 0, ), ], ), child: Column( children: [ // 头部(如果有标题或动作) if (widget.title != null || widget.actions != null || (widget.config.showCloseButton && !widget.config.showFloatingCloseButton)) _buildHeader(isDark), // 内容区域 Expanded(child: widget.child), ], ), ), ), ); } Widget _buildHeader(bool isDark) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: isDark ? WebTheme.darkGrey800 : WebTheme.getBorderColor(context), width: 1, ), ), ), child: Row( children: [ // 标题 if (widget.title != null) Expanded( child: Text( widget.title!, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isDark ? WebTheme.grey100 : WebTheme.grey900, ), ), ), // 自定义操作按钮 if (widget.actions != null) ...[ ...widget.actions!, const SizedBox(width: 8), ], // 关闭按钮(仅在不显示浮动关闭按钮时显示) if (widget.config.showCloseButton && !widget.config.showFloatingCloseButton) IconButton( onPressed: _handleClose, icon: Icon( Icons.close, size: 20, color: isDark ? WebTheme.grey400 : WebTheme.grey600, ), style: IconButton.styleFrom( padding: EdgeInsets.zero, minimumSize: const Size(32, 32), backgroundColor: Colors.transparent, ), ), ], ), ); } /// 构建浮动关闭按钮 Widget _buildFloatingCloseButton() { return Material( elevation: 8, shape: const CircleBorder(), color: Colors.black87, child: InkWell( onTap: _handleClose, customBorder: const CircleBorder(), child: Container( width: 24, height: 24, decoration: const BoxDecoration( color: Colors.black87, shape: BoxShape.circle, ), child: const Icon( Icons.close, color: Colors.white, size: 16, ), ), ), ); } }