新增:档案页共享组件三件 + 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>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<RecordType, RecordTypeStyle> 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
-2
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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<void> 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<Icon>(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<Icon>(find.byIcon(Icons.edit));
|
||||
expect(badgeIcon.color, Colors.white);
|
||||
final badge = tester.widget<Container>(
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -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<Icon>(find.byIcon(Icons.vaccines_outlined));
|
||||
expect(icon.size, 20);
|
||||
expect(icon.color, AppColors.successInk);
|
||||
|
||||
final container = tester.widget<Container>(
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -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<Text> pumpPill(WidgetTester tester, TagPill pill) async {
|
||||
await tester.pumpWidget(MaterialApp(home: Scaffold(body: pill)));
|
||||
return tester.widget<Text>(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<DecoratedBox>(
|
||||
find.ancestor(of: find.text('底色'), matching: find.byType(DecoratedBox)),
|
||||
);
|
||||
final decoration = box.decoration as BoxDecoration;
|
||||
expect(decoration.color, AppColors.primary.withAlpha(20));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user