3179528c57
- PetAvatar 四尺寸档(96/64/44/32)收敛头像重复实现:3px 白描边 + 轻投影,编辑徽标底改 primaryStrong(白图标 4.49:1 达非文字 3:1), M2 无上传(ADR-010),url 缺省渲染本地占位形态 - RecordTypeDot 三尺寸档 + 五类记录类型的图标/三色映射唯一出口 (05 号规范 §2 表,供 T2-13/14 时间线复用) - EmptyStateIllustration 空态插画区(112 圆 + 标题 + 说明 + 可选 CTA) - TagPill 增可选 inkColor,缺省按 color 查深变体映射 (primary→primaryDark 8.74:1 / success→successInk 7.39:1 / accent→accentDark 7.07:1 / error→errorDark 5.78:1 / 未命中→ink), 既有调用零参数回归 - AppColors 新增 errorDark #B02C25、inkSoft #6B5A4A(05 号规范 D8) flutter test 141/141 全绿;analyze 0 问题(T2-12 第一部分) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
154 lines
4.2 KiB
Dart
154 lines
4.2 KiB
Dart
import 'package:flutter/material.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,
|
||
child: Image.network(
|
||
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),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|