From 3179528c578c44e65e48a8e788cdb548c590dd4e Mon Sep 17 00:00:00 2001 From: Lixi20 Date: Tue, 8 Sep 2026 11:57:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=A1=A3=E6=A1=88?= =?UTF-8?q?=E9=A1=B5=E5=85=B1=E4=BA=AB=E7=BB=84=E4=BB=B6=E4=B8=89=E4=BB=B6?= =?UTF-8?q?=20+=20TagPill=20=E6=B7=B1=E5=8F=98=E4=BD=93=E6=98=A0=E5=B0=84?= =?UTF-8?q?=EF=BC=88DEBT-1=20=E5=81=BF=E8=BF=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lib/core/theme/app_theme.dart | 8 + .../widgets/empty_state_illustration.dart | 78 ++++++++ lib/core/widgets/pet_avatar.dart | 169 ++++++++++++++++++ lib/core/widgets/record_type_dot.dart | 111 ++++++++++++ lib/widgets/common.dart | 31 +++- .../empty_state_illustration_test.dart | 45 +++++ test/core/widgets/pet_avatar_test.dart | 72 ++++++++ test/core/widgets/record_type_dot_test.dart | 69 +++++++ test/widgets/tag_pill_test.dart | 67 +++++++ 9 files changed, 648 insertions(+), 2 deletions(-) create mode 100644 lib/core/widgets/empty_state_illustration.dart create mode 100644 lib/core/widgets/pet_avatar.dart create mode 100644 lib/core/widgets/record_type_dot.dart create mode 100644 test/core/widgets/empty_state_illustration_test.dart create mode 100644 test/core/widgets/pet_avatar_test.dart create mode 100644 test/core/widgets/record_type_dot_test.dart create mode 100644 test/widgets/tag_pill_test.dart diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 0ac4af5..7311a09 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -30,6 +30,10 @@ abstract final class AppColors { /// 主文字。 static const ink = Color(0xFF3E2A1F); + /// 承载信息的次级文字(日期、元数据、字段名;正典 feed-caption 色, + /// 白底 6.59:1 达 AA)。muted 仅限占位符、禁用态、纯装饰(05 号规范 DEBT-2)。 + static const inkSoft = Color(0xFF6B5A4A); + /// 次级文字、占位符。 static const muted = Color(0xFF9C8977); @@ -50,6 +54,10 @@ abstract final class AppColors { /// 校验错误文字、错误描边、失败提示(白底上约 5.4:1,AA 达标)。 static const error = Color(0xFFD0342C); + /// error 淡底(8% tint)上的文字(error 本身在自家淡底仅 4.44:1 贴线不过; + /// 与 primary→primaryStrong 同构加深,05 号规范 §2/D8)。 + static const errorDark = Color(0xFFB02C25); + /// 品牌渐变:hero 区、头像环、装饰性大面积(不承载正文文字)。 static const brandGradient = LinearGradient( begin: Alignment.topLeft, diff --git a/lib/core/widgets/empty_state_illustration.dart b/lib/core/widgets/empty_state_illustration.dart new file mode 100644 index 0000000..a5d81c8 --- /dev/null +++ b/lib/core/widgets/empty_state_illustration.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:patbond_flutter/core/theme/app_theme.dart'; + +/// 空态插画区(05 号规范 §3.4):承载引导动作的空态,向上兼容现有 +/// `EmptyState`(42 图标 + 一行 bodySmall 的纯提示版继续留用)。 +/// +/// 布局:112 圆形插画区(`surfaceTint` 底 + 56 `primary` 图标,大面积 +/// 装饰用法)→ 标题 → 说明(12 `inkSoft` ≤2 行居中)→ 可选 CTA。 +/// v1 插画用 Material 图标,正式素材待品牌侧供给后原位替换。 +class EmptyStateIllustration extends StatelessWidget { + const EmptyStateIllustration({ + required this.icon, + required this.title, + super.key, + this.description, + this.ctaLabel, + this.onCtaPressed, + }); + + final IconData icon; + final String title; + final String? description; + + /// CTA 文案;null 则不渲染按钮(如筛选后空态)。 + final String? ctaLabel; + final VoidCallback? onCtaPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 48, horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 112, + height: 112, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: AppColors.surfaceTint, + ), + child: Icon(icon, size: 56, color: AppColors.primary), + ), + const SizedBox(height: 16), + Text( + title, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + if (description != null) ...[ + const SizedBox(height: 8), + Text( + description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.inkSoft, + fontSize: 12, + height: 1.4, + ), + ), + ], + if (ctaLabel != null) ...[ + const SizedBox(height: 24), + FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24), + ), + onPressed: onCtaPressed, + child: Text(ctaLabel!), + ), + ], + ], + ), + ); + } +} diff --git a/lib/core/widgets/pet_avatar.dart b/lib/core/widgets/pet_avatar.dart new file mode 100644 index 0000000..309a4f3 --- /dev/null +++ b/lib/core/widgets/pet_avatar.dart @@ -0,0 +1,169 @@ +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), + ); + } +} diff --git a/lib/core/widgets/record_type_dot.dart b/lib/core/widgets/record_type_dot.dart new file mode 100644 index 0000000..728880a --- /dev/null +++ b/lib/core/widgets/record_type_dot.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:patbond_flutter/core/theme/app_theme.dart'; + +/// M2 记录类型(05 号规范 §2 五种,「其他」为扩展兜底)。 +enum RecordType { weight, vaccine, deworming, medical, other } + +/// 类型 → 图标 + 三色(dot 底 8% 淡染基色 / 图标色 / 文字标签色)+ 文案的 +/// 唯一映射出口(05 §3.2:映射只存在于本文件一处,杜绝散落硬编码)。 +/// +/// 对比度依据 05 §2 自查表:图标于 dot 底 4.16–7.39:1(非文字 ≥3:1)、 +/// 文字于 dot 底 5.78–8.74:1(AA ≥4.5:1)均达标。类型必须以 +/// 「图标 + 文字」双通道呈现,不得单靠颜色区分(色盲可辨性)。 +class RecordTypeStyle { + const RecordTypeStyle({ + required this.icon, + required this.baseColor, + required this.iconColor, + required this.inkColor, + required this.label, + }); + + final IconData icon; + + /// dot 底基色(渲染时 `withAlpha(20)` 即 8% 淡染)。 + final Color baseColor; + + final Color iconColor; + + /// 文字/标签色,供 TagPill、详情页复用。 + final Color inkColor; + + final String label; +} + +/// 五类映射常量(05 §2 表逐行对应)。 +const Map recordTypeStyles = { + RecordType.weight: RecordTypeStyle( + icon: Icons.monitor_weight_outlined, + baseColor: AppColors.primary, + iconColor: AppColors.primaryStrong, + inkColor: AppColors.primaryDark, + label: '体重', + ), + RecordType.vaccine: RecordTypeStyle( + icon: Icons.vaccines_outlined, + baseColor: AppColors.success, + iconColor: AppColors.successInk, + inkColor: AppColors.successInk, + label: '疫苗', + ), + RecordType.deworming: RecordTypeStyle( + icon: Icons.pest_control, + baseColor: AppColors.accent, + iconColor: AppColors.accentDark, + inkColor: AppColors.accentDark, + label: '驱虫', + ), + RecordType.medical: RecordTypeStyle( + icon: Icons.medical_services_outlined, + baseColor: AppColors.error, + iconColor: AppColors.error, + inkColor: AppColors.errorDark, + label: '就医', + ), + RecordType.other: RecordTypeStyle( + icon: Icons.sticky_note_2_outlined, + baseColor: AppColors.muted, + iconColor: AppColors.inkSoft, + inkColor: AppColors.inkSoft, + label: '其他', + ), +}; + +/// 圆标尺寸档(05 §3.2)。图标尺寸 = dot 的 50%。 +enum RecordTypeDotSize { + /// 24:表单类型选择器内。 + sm(24), + + /// 40:时间线条目。 + md(40), + + /// 56:记录详情页头。 + lg(56); + + const RecordTypeDotSize(this.dimension); + + final double dimension; +} + +/// 记录类型圆标(05 号规范 §3.2):正圆淡染底 + 类型图标。 +/// 无自身点击态(点击归属父容器)、无禁用态。 +class RecordTypeDot extends StatelessWidget { + const RecordTypeDot({required this.type, required this.size, super.key}); + + final RecordType type; + final RecordTypeDotSize size; + + @override + Widget build(BuildContext context) { + final style = recordTypeStyles[type]!; + return Container( + width: size.dimension, + height: size.dimension, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: style.baseColor.withAlpha(20), + ), + child: Icon(style.icon, color: style.iconColor, size: size.dimension / 2), + ); + } +} diff --git a/lib/widgets/common.dart b/lib/widgets/common.dart index 928b696..45ee16e 100644 --- a/lib/widgets/common.dart +++ b/lib/widgets/common.dart @@ -73,14 +73,41 @@ class SectionCard extends StatelessWidget { } } +/// 胶囊标签:底色为 [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}); + 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), @@ -91,7 +118,7 @@ class TagPill extends StatelessWidget { child: Text( label, style: TextStyle( - color: color, + color: effectiveInk, fontSize: 11, fontWeight: FontWeight.w700, ), diff --git a/test/core/widgets/empty_state_illustration_test.dart b/test/core/widgets/empty_state_illustration_test.dart new file mode 100644 index 0000000..b2a3e6a --- /dev/null +++ b/test/core/widgets/empty_state_illustration_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patbond_flutter/core/widgets/empty_state_illustration.dart'; + +void main() { + testWidgets('渲染插画 / 标题 / 说明 / CTA,点击 CTA 回调', (tester) async { + var taps = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: EmptyStateIllustration( + icon: Icons.pets, + title: '还没有宠物档案', + description: '添加毛孩子,开始记录 TA 的健康点滴', + ctaLabel: '添加宠物', + onCtaPressed: () => taps++, + ), + ), + ), + ); + + expect(find.byIcon(Icons.pets), findsOneWidget); + expect(find.text('还没有宠物档案'), findsOneWidget); + expect(find.text('添加毛孩子,开始记录 TA 的健康点滴'), findsOneWidget); + + await tester.tap(find.text('添加宠物')); + expect(taps, 1); + }); + + testWidgets('无 CTA / 无说明时不渲染对应区块', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: EmptyStateIllustration( + icon: Icons.event_note_outlined, + title: '暂无体重记录', + ), + ), + ), + ); + + expect(find.text('暂无体重记录'), findsOneWidget); + expect(find.byType(FilledButton), findsNothing); + }); +} diff --git a/test/core/widgets/pet_avatar_test.dart b/test/core/widgets/pet_avatar_test.dart new file mode 100644 index 0000000..22b7244 --- /dev/null +++ b/test/core/widgets/pet_avatar_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patbond_flutter/core/theme/app_theme.dart'; +import 'package:patbond_flutter/core/widgets/pet_avatar.dart'; + +void main() { + Future pump(WidgetTester tester, Widget child) { + return tester.pumpWidget( + MaterialApp( + home: Scaffold(body: Center(child: child)), + ), + ); + } + + testWidgets('四档尺寸与规格一致(96/64/44/32)', (tester) async { + expect(PetAvatarSize.xl.dimension, 96); + expect(PetAvatarSize.lg.dimension, 64); + expect(PetAvatarSize.md.dimension, 44); + expect(PetAvatarSize.sm.dimension, 32); + + await pump(tester, const PetAvatar(size: PetAvatarSize.xl)); + final size = tester.getSize(find.byType(PetAvatar)); + expect(size.width, 96); + expect(size.height, 96); + }); + + testWidgets('无 url 渲染本地占位形态(ADR-010:pets 图标)', (tester) async { + await pump(tester, const PetAvatar(size: PetAvatarSize.lg)); + final icon = tester.widget(find.byIcon(Icons.pets)); + expect(icon.color, AppColors.muted); + expect(find.byType(Image), findsNothing); + }); + + testWidgets('xl 档编辑徽标为 primaryStrong 底白图标(无障碍修订)', (tester) async { + await pump( + tester, + const PetAvatar(size: PetAvatarSize.xl, showEditBadge: true), + ); + final badgeIcon = tester.widget(find.byIcon(Icons.edit)); + expect(badgeIcon.color, Colors.white); + final badge = tester.widget( + find.ancestor( + of: find.byIcon(Icons.edit), + matching: find.byType(Container), + ), + ); + final decoration = badge.decoration as BoxDecoration; + expect(decoration.color, AppColors.primaryStrong); + }); + + testWidgets('sm/md 档不渲染编辑徽标', (tester) async { + await pump( + tester, + const PetAvatar(size: PetAvatarSize.sm, showEditBadge: true), + ); + expect(find.byIcon(Icons.edit), findsNothing); + }); + + testWidgets('onTap 生效且禁用态不响应', (tester) async { + var taps = 0; + await pump(tester, PetAvatar(size: PetAvatarSize.lg, onTap: () => taps++)); + await tester.tap(find.byType(PetAvatar)); + expect(taps, 1); + + await pump( + tester, + PetAvatar(size: PetAvatarSize.lg, enabled: false, onTap: () => taps++), + ); + await tester.tap(find.byType(PetAvatar)); + expect(taps, 1); + }); +} diff --git a/test/core/widgets/record_type_dot_test.dart b/test/core/widgets/record_type_dot_test.dart new file mode 100644 index 0000000..7427bc1 --- /dev/null +++ b/test/core/widgets/record_type_dot_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patbond_flutter/core/theme/app_theme.dart'; +import 'package:patbond_flutter/core/widgets/record_type_dot.dart'; + +void main() { + testWidgets('五类映射齐备:图标 + 双色(05 §2 表)', (tester) async { + // 映射表唯一出口:五类各有图标、底色、图标色、文字色、文案。 + expect(recordTypeStyles.length, RecordType.values.length); + expect( + recordTypeStyles[RecordType.weight]!.iconColor, + AppColors.primaryStrong, + ); + expect( + recordTypeStyles[RecordType.weight]!.inkColor, + AppColors.primaryDark, + ); + expect( + recordTypeStyles[RecordType.vaccine]!.inkColor, + AppColors.successInk, + ); + expect( + recordTypeStyles[RecordType.deworming]!.inkColor, + AppColors.accentDark, + ); + expect(recordTypeStyles[RecordType.medical]!.inkColor, AppColors.errorDark); + expect(recordTypeStyles[RecordType.other]!.inkColor, AppColors.inkSoft); + expect(recordTypeStyles[RecordType.medical]!.label, '就医'); + }); + + testWidgets('圆标渲染:尺寸档正确、图标为 dot 的 50%、底为基色 8% 淡染', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: RecordTypeDot( + type: RecordType.vaccine, + size: RecordTypeDotSize.md, + ), + ), + ), + ), + ); + + final size = tester.getSize(find.byType(RecordTypeDot)); + expect(size.width, 40); + expect(size.height, 40); + + final icon = tester.widget(find.byIcon(Icons.vaccines_outlined)); + expect(icon.size, 20); + expect(icon.color, AppColors.successInk); + + final container = tester.widget( + find.descendant( + of: find.byType(RecordTypeDot), + matching: find.byType(Container), + ), + ); + final decoration = container.decoration as BoxDecoration; + expect(decoration.color, AppColors.success.withAlpha(20)); + expect(decoration.shape, BoxShape.circle); + }); + + testWidgets('三档尺寸 24/40/56', (tester) async { + expect(RecordTypeDotSize.sm.dimension, 24); + expect(RecordTypeDotSize.md.dimension, 40); + expect(RecordTypeDotSize.lg.dimension, 56); + }); +} diff --git a/test/widgets/tag_pill_test.dart b/test/widgets/tag_pill_test.dart new file mode 100644 index 0000000..21bfd84 --- /dev/null +++ b/test/widgets/tag_pill_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patbond_flutter/core/theme/app_theme.dart'; +import 'package:patbond_flutter/widgets/common.dart'; + +/// DEBT-1 偿还回归(05 号规范 §5.3):TagPill 文字色改深变体映射, +/// 底色维持 8% 淡染;既有零参数调用仅视觉变深、不改布局。 +void main() { + Future pumpPill(WidgetTester tester, TagPill pill) async { + await tester.pumpWidget(MaterialApp(home: Scaffold(body: pill))); + return tester.widget(find.text(pill.label)); + } + + testWidgets('默认色(primary)文字映射为 primaryDark', (tester) async { + final text = await pumpPill(tester, const TagPill('标签')); + expect(text.style?.color, AppColors.primaryDark); + }); + + testWidgets('success → successInk、accent → accentDark、error → errorDark', ( + tester, + ) async { + final success = await pumpPill( + tester, + const TagPill('成功', color: AppColors.success), + ); + expect(success.style?.color, AppColors.successInk); + + final accent = await pumpPill( + tester, + const TagPill('强调', color: AppColors.accent), + ); + expect(accent.style?.color, AppColors.accentDark); + + final error = await pumpPill( + tester, + const TagPill('错误', color: AppColors.error), + ); + expect(error.style?.color, AppColors.errorDark); + }); + + testWidgets('未命中映射的自定义色兜底为 ink', (tester) async { + final text = await pumpPill( + tester, + const TagPill('自定义', color: Color(0xFF123456)), + ); + expect(text.style?.color, AppColors.ink); + }); + + testWidgets('inkColor 显式指定时优先于内置映射', (tester) async { + final text = await pumpPill( + tester, + const TagPill('指定', color: AppColors.success, inkColor: AppColors.ink), + ); + expect(text.style?.color, AppColors.ink); + }); + + testWidgets('底色维持基色 8% 淡染不变', (tester) async { + await tester.pumpWidget( + const MaterialApp(home: Scaffold(body: TagPill('底色'))), + ); + final box = tester.widget( + find.ancestor(of: find.text('底色'), matching: find.byType(DecoratedBox)), + ); + final decoration = box.decoration as BoxDecoration; + expect(decoration.color, AppColors.primary.withAlpha(20)); + }); +}