import 'package:flutter/material.dart'; import '../brand/app_tokens.dart'; class CompanyLoadingView extends StatelessWidget { const CompanyLoadingView({super.key, this.label = '正在加载'}); final String label; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primary), ), const SizedBox(height: AppGap.x3), Text(label, style: const TextStyle( fontSize: AppFont.sub, color: AppColors.textSecondary)), ], ), ); } } /// 规范 3.1:空白用 64px 线性图标;失败用「对话气泡 + 感叹号」组合线性图标。 class CompanyStatusIcon extends StatelessWidget { const CompanyStatusIcon.chat({super.key}) : icon = Icons.chat_bubble_outline, fail = false; const CompanyStatusIcon.person({super.key}) : icon = Icons.person_outline, fail = false; const CompanyStatusIcon.fail({super.key}) : icon = Icons.chat_bubble_outline, fail = true; final IconData icon; final bool fail; @override Widget build(BuildContext context) { if (!fail) { return Icon(icon, size: 64, color: AppColors.divider); } return SizedBox( width: 64, height: 64, child: Stack( alignment: Alignment.center, children: [ Icon(icon, size: 64, color: AppColors.divider), const Positioned( right: 0, bottom: 0, child: Icon(Icons.error_outline, size: 22, color: AppColors.divider), ), ], ), ); } } class CompanyEmptyView extends StatelessWidget { const CompanyEmptyView({ super.key, required this.icon, required this.title, required this.subtitle, }); final IconData icon; final String title; final String subtitle; @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppGap.x8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 64, color: AppColors.divider), const SizedBox(height: AppGap.x4), Text(title, style: const TextStyle( fontSize: AppFont.body, color: AppColors.textPrimary)), const SizedBox(height: AppGap.x1), Text( subtitle, textAlign: TextAlign.center, style: const TextStyle( fontSize: AppFont.sub, color: AppColors.textSecondary), ), ], ), ), ); } } class CompanyFailView extends StatelessWidget { const CompanyFailView({super.key, required this.onRetry, this.title = '加载失败'}); final VoidCallback onRetry; final String title; @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppGap.x8), child: Column( mainAxisSize: MainAxisSize.min, children: [ const CompanyStatusIcon.fail(), const SizedBox(height: AppGap.x4), Text(title, style: const TextStyle( fontSize: AppFont.body, color: AppColors.textPrimary)), const SizedBox(height: AppGap.x1), const Text( '检查一下网络,再点重试', style: TextStyle( fontSize: AppFont.sub, color: AppColors.textSecondary), ), const SizedBox(height: AppGap.x4), OutlinedButton( onPressed: onRetry, style: OutlinedButton.styleFrom( minimumSize: const Size(96, 36), side: const BorderSide( color: AppColors.textSecondary, width: 0.5), foregroundColor: AppColors.textPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.control)), ), child: const Text('重试', style: TextStyle(fontSize: AppFont.body)), ), ], ), ), ); } } class CompanyOfflineBanner extends StatelessWidget { const CompanyOfflineBanner({super.key}); @override Widget build(BuildContext context) { return Container( height: 36, color: AppColors.bannerBg, padding: const EdgeInsets.symmetric(horizontal: AppGap.x4), child: const Row( children: [ Icon(Icons.info_outline, size: 16, color: Colors.white), SizedBox(width: AppGap.x2), Expanded( child: Text( '当前网络不可用,请检查网络连接', style: TextStyle(fontSize: AppFont.sub, color: Colors.white), ), ), ], ), ); } } /// 即时出现;恢复后 300ms ease-out 收起。减少动态效果时立刻切换。 class CompanyOfflineBannerHost extends StatelessWidget { const CompanyOfflineBannerHost({super.key, required this.visible}); final bool visible; @override Widget build(BuildContext context) { final reduce = AppMotion.reduce(context); return AnimatedSize( duration: visible || reduce ? Duration.zero : AppDuration.bannerCollapse, curve: Curves.easeOut, alignment: Alignment.topCenter, child: visible ? const SafeArea(bottom: false, child: CompanyOfflineBanner()) : const SizedBox(width: double.infinity, height: 0), ); } } /// 入场 150ms、停留 2s、退场 200ms,ease-out;减少动态效果时无动画。 class CompanyToastBanner extends StatefulWidget { const CompanyToastBanner({super.key, required this.message}); final String? message; @override State createState() => _CompanyToastBannerState(); } class _CompanyToastBannerState extends State { String? _shown; double _opacity = 0; @override void didUpdateWidget(CompanyToastBanner oldWidget) { super.didUpdateWidget(oldWidget); if (widget.message != null) { _shown = widget.message; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => _opacity = 1); }); } else if (_shown != null) { setState(() => _opacity = 0); } } @override Widget build(BuildContext context) { if (_shown == null) return const SizedBox.shrink(); final reduce = AppMotion.reduce(context); final duration = reduce ? Duration.zero : (_opacity == 0 ? AppDuration.toastOut : AppDuration.overlay); return SafeArea( child: Align( alignment: Alignment.topCenter, child: Padding( padding: const EdgeInsets.fromLTRB(AppGap.x4, AppGap.x2, AppGap.x4, 0), child: AnimatedOpacity( opacity: _opacity, duration: duration, curve: Curves.easeOut, onEnd: () { if (_opacity == 0 && mounted) { setState(() => _shown = null); } }, child: Material( color: AppColors.bannerBg, borderRadius: BorderRadius.circular(AppRadius.control), child: Padding( padding: const EdgeInsets.symmetric( horizontal: AppGap.x4, vertical: AppGap.x3), child: Text( _shown!, textAlign: TextAlign.center, style: const TextStyle( fontSize: AppFont.sub, color: Colors.white), ), ), ), ), ), ), ); } }