// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../theme/app_colors.dart'; import '../theme/app_dimens.dart'; const double _iconSize = 72; const double _maxContentWidth = 260; const double _regularHorizontal = 24; const double _regularVertical = 28; const double _compactHorizontal = 16; const double _compactVertical = 4; 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, this.stretchContent = 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; /// 让图标和文案占满内容宽度,适用于需要严格居中的宽版空状态。 final bool stretchContent; @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 ? _compactHorizontal : _regularHorizontal, vertical: compact ? _compactVertical : _regularVertical, ), child: SizedBox( width: stretchContent ? double.infinity : null, child: Semantics( container: true, label: title, child: Column( mainAxisSize: MainAxisSize.min, children: [ ExcludeSemantics(child: _EmptyMark(icon: icon)), SizedBox(height: compact ? _compactGap : _regularGap), SizedBox( width: stretchContent ? double.infinity : null, child: _EmptyCopy( title: title, subtitle: subtitle, actionLabel: hasAction ? actionLabel : null, actionKey: actionKey, onAction: hasAction ? onAction : null, footer: footer, stretchContent: stretchContent, ), ), ], ), ), ), ), ), ); } } /// 空列表只放一枚大图标,不画线框,也不铺底色。 class _EmptyMark extends StatelessWidget { const _EmptyMark({required this.icon}); final IconData icon; @override Widget build(BuildContext context) { return Icon( icon, key: const Key('empty-state-icon'), size: _iconSize, 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, required this.stretchContent, }); final String title; final String? subtitle; final String? actionLabel; final Key? actionKey; final VoidCallback? onAction; final Widget? footer; final bool stretchContent; @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: stretchContent ? CrossAxisAlignment.stretch : CrossAxisAlignment.center, 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!], ], ); } }