Files
ReelShort/lib/core/widgets/empty_state.dart
T

173 lines
4.8 KiB
Dart

// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
const double _regularIconSize = 72;
const double _compactIconSize = 44;
const double _maxContentWidth = 260;
const double _regularHorizontal = 24;
const double _regularVertical = 28;
const double _compactPad = 16;
const double _regularGap = 20;
const double _compactGap = 12;
const double _actionMinHeight = 44;
/// 统一空列表占位:一枚图标、标题、可选说明与操作。
class EmptyState extends StatelessWidget {
const EmptyState({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.actionLabel,
this.actionKey,
this.onAction,
this.footer,
this.compact = false,
});
final IconData icon;
final String title;
final String? subtitle;
final String? actionLabel;
final Key? actionKey;
final VoidCallback? onAction;
final Widget? footer;
final bool compact;
@override
Widget build(BuildContext context) {
final hasAction = actionLabel != null && onAction != null;
return Center(
child: ConstrainedBox(
key: const Key('empty-state-content'),
constraints: const BoxConstraints(maxWidth: _maxContentWidth),
child: Padding(
key: const Key('empty-state-padding'),
padding: EdgeInsets.symmetric(
horizontal: compact ? _compactPad : _regularHorizontal,
vertical: compact ? _compactPad : _regularVertical,
),
child: Semantics(
container: true,
label: title,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ExcludeSemantics(
child: _EmptyMark(icon: icon, compact: compact),
),
SizedBox(height: compact ? _compactGap : _regularGap),
_EmptyCopy(
title: title,
subtitle: subtitle,
actionLabel: hasAction ? actionLabel : null,
actionKey: actionKey,
onAction: hasAction ? onAction : null,
footer: footer,
),
],
),
),
),
),
);
}
}
/// 空列表只放一枚大图标,不画线框,也不铺底色。
class _EmptyMark extends StatelessWidget {
const _EmptyMark({required this.icon, required this.compact});
final IconData icon;
final bool compact;
@override
Widget build(BuildContext context) {
return Icon(
icon,
key: const Key('empty-state-icon'),
size: compact ? _compactIconSize : _regularIconSize,
color: AppColors.subtle,
);
}
}
class _EmptyCopy extends StatelessWidget {
const _EmptyCopy({
required this.title,
required this.subtitle,
required this.actionLabel,
required this.actionKey,
required this.onAction,
required this.footer,
});
final String title;
final String? subtitle;
final String? actionLabel;
final Key? actionKey;
final VoidCallback? onAction;
final Widget? footer;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ExcludeSemantics(
child: Text(
title,
key: const Key('empty-state-title'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: 15,
fontWeight: FontWeight.w400,
height: 1.35,
),
),
),
if (subtitle != null) ...[
const SizedBox(height: 6),
ExcludeSemantics(
child: Text(
subtitle!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.muted,
fontSize: AppDimens.emptyStateSubtitle,
height: 1.45,
),
),
),
],
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 8),
TextButton(
key: actionKey ?? const Key('empty-state-action'),
onPressed: onAction,
style: TextButton.styleFrom(
foregroundColor: AppColors.blueSoft,
minimumSize: const Size(44, _actionMinHeight),
padding: const EdgeInsets.symmetric(horizontal: 14),
),
child: Text(
actionLabel!,
style: const TextStyle(fontSize: AppDimens.emptyStateAction),
),
),
],
if (footer != null) ...[const SizedBox(height: 12), footer!],
],
);
}
}