Files
patbond-flutter/lib/widgets/common.dart
T
lixi 3179528c57 新增:档案页共享组件三件 + TagPill 深变体映射(DEBT-1 偿还)
- 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>
2026-09-08 11:57:31 +08:00

154 lines
4.2 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 '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.672.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),
],
),
);
}
}