1441f0148f
CI / flutter-gates (push) Successful in 2m30s
- MediaUploader:选图→压缩(80→60 降质阶梯,10 MiB 超限拒绝)→createUpload →预签名 PUT 直传(进度回调)→confirm→ready assetId 交付;并发槽位 2、 顺序即 position、单图失败不拖垮整批;凭据过期预检与存储侧 403 各自动 换新凭据一次;失败重试复用压缩产物、从 createUpload 全新开始 - 孤儿防护:未 confirm 的 assetId 只以管线局部变量存在,快照 assetId 与 ready 态断言绑定,buildAttachRequests 非全员 ready 即抛 StateError - 线路修正:media 两步上传端点由 user 服务(:8082 MediaController)提供, T3-12 误挂 community 客户端(:8084 无 media 路由),补 mediaApi 分端口 直连,compose 真链路实测证实 - UploadProgressOverlay 四态进度层(05 号规范 §3.3);直传层本地 HttpServer 三线路测试(200/403/断连);compose 冒烟测试 (PATBOND_MEDIA_SMOKE=1 启用,默认跳过) - 新依赖:image_picker、flutter_image_compress(03 号评估选型) - flutter test 379 全绿(基线 347,+32)、analyze 0、format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
4.5 KiB
Dart
144 lines
4.5 KiB
Dart
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);
|
||
}
|
||
}
|