Files
tongxunruanjian/mobile-next/lib/company/ui/state_views.dart
T

271 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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、退场 200msease-out;减少动态效果时无动画。
class CompanyToastBanner extends StatefulWidget {
const CompanyToastBanner({super.key, required this.message});
final String? message;
@override
State<CompanyToastBanner> createState() => _CompanyToastBannerState();
}
class _CompanyToastBannerState extends State<CompanyToastBanner> {
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),
),
),
),
),
),
),
);
}
}