8aac8c52cc
CI / flutter-gates (push) Successful in 2m41s
- home_page Feed segment 改接 CommunityController:骨架屏首载、空态 CTA、 错误横幅重试、下拉刷新(失败保留旧列表 + SnackBar)、触底游标翻页 (失败态只走显式重试,不随滚动风暴自动重打) - PostCard 三形态(单图通栏 4:3 / 多图折叠封面 +N 角标 / 纯文字 6 行)+ PostMediaGrid(列数规则 + ink 80% scrim 精算)+ LikeButton(error 族修订 Colors.red,T3-14 纯展示禁用态,ToggleSync 接线留 T3-15/16)+ FeedSkeleton (呼吸动效尊重减弱动态设置);降级作者统一「宠友」占位 - SignedNetworkImage:预签名 URL 缓存 key 剥离 X-Amz-* 签名参数, RemoteImage 全仓换用(同图不同签名命中同一缓存条目) - feed 域埋点:feed_viewed 聚合曝光(浏览段开/结算于切 Tab、切分段、 退后台、销毁;≥50% 可见 ≥500ms 段内按帖去重,postId 不上报)+ feed_load_failed(refresh/load_more 双路,会话失效不报) - integration_test/feed_live_test.dart:compose 六容器 Linux 桌面真链路 实测入口(PATBOND_FEED_LIVE=1 门控,默认跳过) - 测试 379 → 421(+42):四态/尾部三态/翻页不丢不重/降级作者/曝光结算 时机/缓存 key/事件属性全覆盖;analyze 0,format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
157 lines
4.4 KiB
Dart
157 lines
4.4 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:patbond_flutter/core/network/signed_network_image.dart';
|
||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||
|
||
class RemoteImage extends StatelessWidget {
|
||
const RemoteImage({
|
||
required this.url,
|
||
super.key,
|
||
this.width,
|
||
this.height,
|
||
this.fit = BoxFit.cover,
|
||
this.borderRadius = BorderRadius.zero,
|
||
});
|
||
|
||
final String url;
|
||
final double? width;
|
||
final double? height;
|
||
final BoxFit fit;
|
||
final BorderRadius borderRadius;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ClipRRect(
|
||
borderRadius: borderRadius,
|
||
// 缓存 key 剥离预签名参数(T3-14):媒体 URL 每次响应现签,
|
||
// 按完整 URL 缓存会同图重复下载。
|
||
child: Image(
|
||
image: SignedNetworkImage(url),
|
||
width: width,
|
||
height: height,
|
||
fit: fit,
|
||
loadingBuilder: (context, child, progress) {
|
||
if (progress == null) return child;
|
||
return ColoredBox(
|
||
color: AppColors.surfaceTint,
|
||
child: SizedBox(
|
||
width: width,
|
||
height: height,
|
||
child: const Center(
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
errorBuilder: (context, error, stackTrace) {
|
||
return ColoredBox(
|
||
color: AppColors.surfaceTint,
|
||
child: SizedBox(
|
||
width: width,
|
||
height: height,
|
||
child: const Center(
|
||
child: Icon(Icons.pets, color: AppColors.muted, size: 34),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class SectionCard extends StatelessWidget {
|
||
const SectionCard({required this.child, super.key, this.padding});
|
||
|
||
final Widget child;
|
||
final EdgeInsetsGeometry? padding;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
child: Padding(
|
||
padding: padding ?? const EdgeInsets.all(18),
|
||
child: child,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 胶囊标签:底色为 [color] 的 8% 淡染(`withAlpha(20)`),文字用深变体。
|
||
///
|
||
/// DEBT-1 偿还(05 号规范 §5.3):原实现文字直接用 [color],在自身淡底
|
||
/// 上仅 1.67–2.55:1 不达 WCAG AA;现文字色按内置映射取深变体
|
||
/// (primary→primaryDark 8.74:1、success→successInk 7.39:1、
|
||
/// accent→accentDark 7.07:1、error→errorDark 5.78:1、未命中→ink 兜底),
|
||
/// 或经 [inkColor] 显式指定。既有调用零参数变更、仅视觉变深。
|
||
class TagPill extends StatelessWidget {
|
||
const TagPill(
|
||
this.label, {
|
||
super.key,
|
||
this.color = AppColors.primary,
|
||
this.inkColor,
|
||
});
|
||
|
||
final String label;
|
||
|
||
/// 底色基色(淡染 8% 后作背景)。
|
||
final Color color;
|
||
|
||
/// 文字色;缺省按 [color] 查内置深变体映射。
|
||
final Color? inkColor;
|
||
|
||
/// 内置深变体映射(Color 重载 == 不能作 const map 键,用查表函数)。
|
||
static Color _defaultInkFor(Color color) {
|
||
if (color == AppColors.primary) return AppColors.primaryDark;
|
||
if (color == AppColors.success) return AppColors.successInk;
|
||
if (color == AppColors.accent) return AppColors.accentDark;
|
||
if (color == AppColors.error) return AppColors.errorDark;
|
||
return AppColors.ink;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final effectiveInk = inkColor ?? _defaultInkFor(color);
|
||
return DecoratedBox(
|
||
decoration: BoxDecoration(
|
||
color: color.withAlpha(20),
|
||
borderRadius: BorderRadius.circular(99),
|
||
),
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: effectiveInk,
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class EmptyState extends StatelessWidget {
|
||
const EmptyState({required this.message, super.key});
|
||
|
||
final String message;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||
child: Column(
|
||
children: [
|
||
const Icon(
|
||
Icons.search_off_rounded,
|
||
size: 42,
|
||
color: AppColors.muted,
|
||
),
|
||
const SizedBox(height: 12),
|
||
Text(message, style: Theme.of(context).textTheme.bodySmall),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|