383 lines
10 KiB
Dart
383 lines
10 KiB
Dart
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<Color>(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<Widget>? 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<Widget>? _buildActions(BuildContext context) {
|
|
if (actions == null) return null;
|
|
|
|
final isMobile = ResponsiveLayout.isMobile(context);
|
|
|
|
if (isMobile && actions!.length > 2) {
|
|
// 移动端如果操作按钮太多,显示省略菜单
|
|
return [
|
|
PopupMenuButton<int>(
|
|
icon: const Icon(Icons.more_vert),
|
|
onSelected: (index) {
|
|
// 处理菜单选择
|
|
},
|
|
itemBuilder: (context) {
|
|
return actions!.asMap().entries.map((entry) {
|
|
return PopupMenuItem<int>(
|
|
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<Widget>? 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,
|
|
);
|
|
}
|
|
}
|