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>
170 lines
4.6 KiB
Dart
170 lines
4.6 KiB
Dart
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 15(05 §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),
|
||
);
|
||
}
|
||
}
|