Files
patbond-flutter/lib/core/widgets/pet_avatar.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

170 lines
4.6 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';
import 'package:patbond_flutter/widgets/common.dart';
/// 头像尺寸档(05 号规范 §3.1)。
enum PetAvatarSize {
/// 96:档案页头部(收敛现 104 → 96,与表单页一致)。
xl(96),
/// 64:宠物列表卡。
lg(64),
/// 44:头部宠物切换器(最小触控目标)。
md(44),
/// 32:记录详情等行内。
sm(32);
const PetAvatarSize(this.dimension);
final double dimension;
/// 编辑徽标仅 xl / lg 档提供(05 §3.1)。
bool get supportsBadge => this == xl || this == lg;
}
/// 宠物头像(05 号规范 §3.1):统一原档案页头部 / 编辑 sheet 各写一遍的
/// 头像实现。圆形裁切 + 3px `surface` 白描边 + 轻投影(正典
/// `.patbond-avatar` 规格),可选右下编辑徽标与品牌渐变环。
///
/// M2 首版不做头像上传(ADR-010 / D2-1 裁决),[url] 为 null 时渲染
/// 本地占位形态(`surfaceTint` 底 + `Icons.pets`);有图时经
/// [RemoteImage] 加载(loading / 失败兜底由其内置)。
class PetAvatar extends StatelessWidget {
const PetAvatar({
required this.size,
super.key,
this.url,
this.onTap,
this.showEditBadge = false,
this.ring = false,
this.enabled = true,
this.semanticLabel,
});
final PetAvatarSize size;
/// 头像图 URL;null 为本地占位形态(M2 默认)。
final String? url;
/// 可点击时整体 [InkWell] 圆形 ripple。
final VoidCallback? onTap;
/// 右下编辑徽标(`primaryStrong` 底 + 白 edit 图标,白/primaryStrong
/// 4.49:1 达非文字 3:1;修订现实现的 `primary` 底 2.75:1)。
final bool showEditBadge;
/// 2px `brandGradient` 外环(正典 story 环),仅作「当前选中」纯装饰指示。
final bool ring;
/// 禁用态 60% 不透明度(对齐 AppTextField 禁用惯例)。
final bool enabled;
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final dimension = size.dimension;
// 3px 白描边内的图面尺寸。
final imageDimension = dimension - 6;
Widget avatar = Container(
width: dimension,
height: dimension,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.surface,
boxShadow: [
BoxShadow(
color: Color(0x14000000), // rgba(0,0,0,0.08)
offset: Offset(0, 4),
blurRadius: 10,
),
],
),
padding: const EdgeInsets.all(3),
child: ClipOval(
child: url == null
? ColoredBox(
color: AppColors.surfaceTint,
child: Icon(
Icons.pets,
color: AppColors.muted,
size: imageDimension * 0.45,
),
)
: RemoteImage(
url: url!,
width: imageDimension,
height: imageDimension,
borderRadius: BorderRadius.circular(imageDimension / 2),
),
),
);
if (ring) {
avatar = Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: AppColors.brandGradient,
),
child: avatar,
);
}
if (showEditBadge && size.supportsBadge) {
avatar = Stack(
clipBehavior: Clip.none,
children: [
avatar,
const Positioned(right: 0, bottom: 0, child: _EditBadge()),
],
);
}
if (onTap != null) {
avatar = InkWell(
onTap: enabled ? onTap : null,
customBorder: const CircleBorder(),
child: avatar,
);
}
if (!enabled) {
avatar = Opacity(opacity: 0.6, child: avatar);
}
if (semanticLabel != null) {
avatar = Semantics(
label: semanticLabel,
button: onTap != null,
child: avatar,
);
}
return avatar;
}
}
/// 32 圆编辑徽标:`primaryStrong` 底 + 白 edit 1505 §3.1 无障碍修订)。
class _EditBadge extends StatelessWidget {
const _EditBadge();
@override
Widget build(BuildContext context) {
return Container(
width: 32,
height: 32,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primaryStrong,
border: Border.fromBorderSide(
BorderSide(color: AppColors.surface, width: 2),
),
),
child: const Icon(Icons.edit, color: Colors.white, size: 15),
);
}
}