import 'package:flutter/material.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/features/community/media_uploader.dart'; /// 上传进度覆盖层(设计规范 05 号 §3.3):叠加在编辑态九宫格单格上, /// 四视觉态映射 [MediaItemPhase]: /// /// | 视觉态 | 阶段 | 形态 | /// | --- | --- | --- | /// | 排队 | queued / compressing | ink 40% scrim + 「等待中」白字胶囊 | /// | 上传中 | uploading / confirming | scrim + 白色环形进度 36 + 百分比胶囊 | /// | 成功 | ready | scrim 150ms 淡出,无残留角标 | /// | 失败 | failed | error 12% scrim + errorDark 图标 + 底部「重试」通栏 | /// /// 失败态整格点按重试([onRetry],仅可重试失败传入);组件本身不含 /// 图片,由九宫格把它叠在缩略图上(Stack)。 class UploadProgressOverlay extends StatelessWidget { const UploadProgressOverlay({ required this.phase, this.progress = 0, this.onRetry, super.key, }); final MediaItemPhase phase; /// 直传进度 0..1(uploading 态显示;confirming 定格 100%)。 final double progress; /// 失败态整格点按回调;null 即失败不可重试(终态,只展示不响应)。 final VoidCallback? onRetry; @override Widget build(BuildContext context) { return switch (phase) { MediaItemPhase.queued || MediaItemPhase.compressing => _Scrim( child: Center(child: _pill(const Text('等待中', style: _pillTextSm))), ), MediaItemPhase.uploading || MediaItemPhase.confirming => _Scrim( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 36, height: 36, child: CircularProgressIndicator( value: phase == MediaItemPhase.confirming ? 1.0 : progress, color: Colors.white, backgroundColor: Colors.white24, strokeWidth: 3, ), ), const SizedBox(height: 8), _pill( Text( '${((phase == MediaItemPhase.confirming ? 1.0 : progress) * 100).round()}%', style: _pillTextXs, ), ), ], ), ), // 成功:scrim 150ms 淡出后不留任何角标。 MediaItemPhase.ready => const IgnorePointer( child: AnimatedOpacity( opacity: 0, duration: Duration(milliseconds: 150), child: _Scrim(child: SizedBox.expand()), ), ), MediaItemPhase.failed => GestureDetector( onTap: onRetry, child: Container( color: AppColors.error.withAlpha(31), child: Column( children: [ const Expanded( child: Center( child: Icon( Icons.error_outline, size: 24, color: AppColors.errorDark, ), ), ), if (onRetry != null) Container( width: double.infinity, color: AppColors.errorDark, padding: const EdgeInsets.symmetric(vertical: 4), child: const Text( '重试', textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700, ), ), ), ], ), ), ), }; } static const _pillTextSm = TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600, ); static const _pillTextXs = TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700, ); /// 白字衬 ink 80% 胶囊底(对 40% scrim 上的合成底色对比度兜底,§3.3)。 Widget _pill(Widget child) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppColors.ink.withAlpha(204), borderRadius: const BorderRadius.all(Radius.circular(AppRadius.pill)), ), child: child, ); } } /// ink 40% 全格 scrim。 class _Scrim extends StatelessWidget { const _Scrim({required this.child}); final Widget child; @override Widget build(BuildContext context) { return Container(color: AppColors.ink.withAlpha(102), child: child); } }