import 'package:flutter/material.dart'; import '../utils/responsive_layout.dart'; import '../constants/app_constants.dart'; class ResponsiveButton extends StatelessWidget { final String text; final VoidCallback? onPressed; final IconData? icon; final Color? backgroundColor; final Color? textColor; final bool isLoading; final ButtonSize? size; const ResponsiveButton({ super.key, required this.text, this.onPressed, this.icon, this.backgroundColor, this.textColor, this.isLoading = false, this.size, }); @override Widget build(BuildContext context) { final buttonSize = size ?? (ResponsiveLayout.isMobile(context) ? ButtonSize.small : ButtonSize.medium); return SizedBox( width: ResponsiveLayout.isMobile(context) ? double.infinity : null, height: _getHeight(buttonSize), child: ElevatedButton( onPressed: isLoading ? null : onPressed, style: ElevatedButton.styleFrom( backgroundColor: backgroundColor ?? AppConstants.primaryColor, foregroundColor: textColor ?? Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(_getBorderRadius(buttonSize)), ), elevation: 2, padding: _getPadding(buttonSize), ), child: isLoading ? SizedBox( width: _getIconSize(buttonSize), height: _getIconSize(buttonSize), child: CircularProgressIndicator( strokeWidth: 2, valueColor: const AlwaysStoppedAnimation(Colors.white), ), ) : Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: _getIconSize(buttonSize)), const SizedBox(width: 8), ], Flexible( child: Text( text, style: TextStyle(fontSize: _getFontSize(buttonSize)), overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } double _getHeight(ButtonSize size) { switch (size) { case ButtonSize.small: return 36; case ButtonSize.medium: return 44; case ButtonSize.large: return 52; } } double _getBorderRadius(ButtonSize size) { switch (size) { case ButtonSize.small: return 6; case ButtonSize.medium: return 8; case ButtonSize.large: return 12; } } EdgeInsets _getPadding(ButtonSize size) { switch (size) { case ButtonSize.small: return const EdgeInsets.symmetric(horizontal: 16, vertical: 8); case ButtonSize.medium: return const EdgeInsets.symmetric(horizontal: 24, vertical: 12); case ButtonSize.large: return const EdgeInsets.symmetric(horizontal: 32, vertical: 16); } } double _getIconSize(ButtonSize size) { switch (size) { case ButtonSize.small: return 16; case ButtonSize.medium: return 20; case ButtonSize.large: return 24; } } double _getFontSize(ButtonSize size) { switch (size) { case ButtonSize.small: return 12; case ButtonSize.medium: return 14; case ButtonSize.large: return 16; } } } enum ButtonSize { small, medium, large } class ResponsiveText extends StatelessWidget { final String text; final TextStyle? style; final TextAlign? textAlign; final int? maxLines; final TextOverflow? overflow; const ResponsiveText({ super.key, required this.text, this.style, this.textAlign, this.maxLines, this.overflow, }); @override Widget build(BuildContext context) { final responsiveStyle = _getResponsiveStyle(context, style); return Text( text, style: responsiveStyle, textAlign: textAlign, maxLines: maxLines, overflow: overflow ?? TextOverflow.ellipsis, ); } TextStyle _getResponsiveStyle(BuildContext context, TextStyle? baseStyle) { final baseTextStyle = baseStyle ?? Theme.of(context).textTheme.bodyMedium!; if (ResponsiveLayout.isMobile(context)) { return baseTextStyle.copyWith(fontSize: (baseTextStyle.fontSize ?? 14) * 0.9); } else if (ResponsiveLayout.isLargeDesktop(context)) { return baseTextStyle.copyWith(fontSize: (baseTextStyle.fontSize ?? 14) * 1.1); } return baseTextStyle; } } class ResponsiveImage extends StatelessWidget { final String imageUrl; final double? width; final double? height; final BoxFit fit; final Widget? placeholder; final Widget? errorWidget; const ResponsiveImage({ super.key, required this.imageUrl, this.width, this.height, this.fit = BoxFit.cover, this.placeholder, this.errorWidget, }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final effectiveWidth = width ?? constraints.maxWidth; final effectiveHeight = height; return Container( width: effectiveWidth, height: effectiveHeight, constraints: BoxConstraints( maxWidth: effectiveWidth, maxHeight: effectiveHeight ?? double.infinity, ), child: Image.network( imageUrl, width: double.infinity, height: double.infinity, fit: fit, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return placeholder ?? _buildDefaultPlaceholder(); }, errorBuilder: (context, error, stackTrace) { return errorWidget ?? _buildDefaultError(); }, ), ); }, ); } Widget _buildDefaultPlaceholder() { return Container( color: Colors.grey[200], child: const Center( child: CircularProgressIndicator(), ), ); } Widget _buildDefaultError() { return Container( color: Colors.grey[200], child: const Center( child: Icon(Icons.error_outline, color: Colors.grey), ), ); } } class ResponsiveAppBar extends StatelessWidget implements PreferredSizeWidget { final String title; final List? actions; final Widget? leading; final bool automaticallyImplyLeading; final Color? backgroundColor; final Color? foregroundColor; const ResponsiveAppBar({ super.key, required this.title, this.actions, this.leading, this.automaticallyImplyLeading = true, this.backgroundColor, this.foregroundColor, }); @override Widget build(BuildContext context) { final isMobile = ResponsiveLayout.isMobile(context); final isLandscape = ResponsiveLayout.isLandscape(context); return AppBar( title: Text( title, style: TextStyle( fontSize: isMobile ? 18 : 20, fontWeight: FontWeight.w600, color: foregroundColor ?? Colors.white, ), ), actions: _buildActions(context), leading: leading, automaticallyImplyLeading: automaticallyImplyLeading, backgroundColor: backgroundColor ?? AppConstants.primaryColor, foregroundColor: foregroundColor ?? Colors.white, elevation: 0, centerTitle: !isMobile || isLandscape, titleSpacing: isMobile ? 16 : 24, ); } List? _buildActions(BuildContext context) { if (actions == null) return null; final isMobile = ResponsiveLayout.isMobile(context); if (isMobile && actions!.length > 2) { // 移动端如果操作按钮太多,显示省略菜单 return [ PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: (index) { // 处理菜单选择 }, itemBuilder: (context) { return actions!.asMap().entries.map((entry) { return PopupMenuItem( value: entry.key, child: entry.value, ); }).toList(); }, ), ]; } return actions; } @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } class ResponsiveScaffold extends StatelessWidget { final Widget body; final String? title; final Widget? floatingActionButton; final Widget? bottomNavigationBar; final Widget? drawer; final List? actions; final Widget? leading; final bool automaticallyImplyLeading; final Color? backgroundColor; const ResponsiveScaffold({ super.key, required this.body, this.title, this.floatingActionButton, this.bottomNavigationBar, this.drawer, this.actions, this.leading, this.automaticallyImplyLeading = true, this.backgroundColor, }); @override Widget build(BuildContext context) { final isDesktop = ResponsiveLayout.isDesktop(context); final isLandscape = ResponsiveLayout.isLandscape(context); Widget scaffoldBody = ResponsiveContainer( child: body, ); // 桌面端或横屏模式添加侧边间距 if (isDesktop || isLandscape) { scaffoldBody = Row( children: [ if (drawer != null && isDesktop) ...[ SizedBox( width: 280, child: drawer, ), const VerticalDivider(width: 1), ], Expanded(child: scaffoldBody), if (isDesktop) const SizedBox(width: 280), ], ); } return Scaffold( appBar: title != null ? ResponsiveAppBar( title: title!, actions: actions, leading: leading, automaticallyImplyLeading: automaticallyImplyLeading, ) : null, body: scaffoldBody, floatingActionButton: floatingActionButton, bottomNavigationBar: bottomNavigationBar, drawer: isDesktop ? null : drawer, backgroundColor: backgroundColor, ); } }