卡片标签硬编码「本月花费」,而服务端 summary.monthlyExpense 本就返回 month
(ISO year-month,按 tz 归月)。用户看不到实际月份,所以无法自证「我这条记录
到底落在哪个月」,把正确的 ¥0 当成统计故障。
已核实不改后端聚合:用户记录落在 2026-04-09、当天是 2026-09-10,
「本月花费 ¥0」是正确行为。本单只让客户端把口径亮出来。
- health_record_display 新增纯函数 monthlyExpenseCardLabel(month, {now}):
同年「9 月花费」(窄卡一行放得下,故不取「2026-09 花费」);跨年(服务端
归月年份 ≠ 设备当前年份)「2026/12 花费」补年份消歧;串非法退回
「本月花费」不崩不显示脏值。
- 可点提示:_SummaryCard 在 onTap 非空时右上角补 chevron_right。四张卡本都
可点进明细页却无任何视觉提示(用户反馈不知道能点),提示形态沿用项目既有
可点行/卡(宠物列表卡、健康提醒卡、资料页设置行)的 chevron_right,不自创。
- 整卡包 MergeSemantics:读屏一次读全「¥128.50,9 月花费,按钮」而非两段
孤立文字。没有用 excludeSemantics——那会连带丢掉 InkWell 的可激活性。
既有测试口径调整:pet_detail_page_test 两处断言改为实际月份,且样本
monthlyExpense.month 改用当月串使断言不随年份漂移(跨年格式由纯函数单测覆盖)。
新增 integration_test/client_ux_live_test.dart(环境变量门控,默认 skip,
沿用 M3 既有 live 测试形态):compose 六容器 + 真实 App 走完三项修复——
日期选择器全中文/品牌配色/手输录入/一键今天,以及花费卡实际月份 + chevron。
本机是 Wayland 会话、X11 import -window root 取不到根窗口,改为把整棵 App 包
一层 RepaintBoundary 后 toImage() 直出真实渲染像素落 build/ux-live/。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,27 @@ String healthEventMonthHeader(DateTime occurredAt) {
|
||||
return '${local.year} 年 ${local.month} 月';
|
||||
}
|
||||
|
||||
/// 月度花费卡标签(M3.5-03)。
|
||||
///
|
||||
/// 根因备忘:卡片标签此前硬编码「本月花费」,用户无法自证「本月」到底是
|
||||
/// 哪个月——他把当月记录误录到 2026-04 后看到「本月花费 ¥0」,以为聚合坏了。
|
||||
/// 服务端 `summary.monthlyExpense.month` 本就返回 ISO year-month(如 `2026-09`,
|
||||
/// 按 `tz` 归月),直接展示即可自查,无需改后端。
|
||||
///
|
||||
/// [month] 形如 `2026-09`。窄卡(一行四卡)只放得下 4~5 个字,故取「9 月花费」
|
||||
/// 而非「2026-09 花费」;跨年(服务端归月的年份与设备当前年份不一致,如设备
|
||||
/// 已跨到 1 月而窗口仍是去年 12 月)时补年份消歧。解析失败退回「本月花费」。
|
||||
String monthlyExpenseCardLabel(String month, {DateTime? now}) {
|
||||
final match = RegExp(r'^(\d{4})-(\d{2})$').firstMatch(month);
|
||||
if (match == null) return '本月花费';
|
||||
final year = int.parse(match.group(1)!);
|
||||
final monthNo = int.parse(match.group(2)!);
|
||||
if (monthNo < 1 || monthNo > 12) return '本月花费';
|
||||
final currentYear = (now ?? DateTime.now()).year;
|
||||
if (year != currentYear) return '$year/$monthNo 花费';
|
||||
return '$monthNo 月花费';
|
||||
}
|
||||
|
||||
/// 设备时区 → summary `tz` 参数(契约接受固定偏移形如 `+08:00`;
|
||||
/// Flutter 无 IANA 名可取,固定偏移语义等价——只作用于月度窗口)。
|
||||
String tzOffsetQueryValue(Duration offset) {
|
||||
|
||||
@@ -532,7 +532,9 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
// monthlyExpense 恒非 null(契约);金额整数分 → 元展示。
|
||||
value: '¥${formatCentsAsYuan(expense.amountCents)}',
|
||||
emphasized: expense.amountCents > 0,
|
||||
label: '本月花费',
|
||||
// M3.5-03:标签展示服务端归月的实际月份(此前硬编码
|
||||
// 「本月花费」,用户无法自证记录落在哪个月)。
|
||||
label: monthlyExpenseCardLabel(expense.month),
|
||||
onTap: () => _openTimeline(pet),
|
||||
),
|
||||
),
|
||||
@@ -545,6 +547,13 @@ class _PetDetailPageState extends State<PetDetailPage> {
|
||||
|
||||
/// 数据卡(正典 stat-card 形态):图标 + 数值 15/w800 + 标签 12 inkSoft;
|
||||
/// 空态数值降级 inkSoft 常规字重(区分「有数据」与「空态」两种视觉)。
|
||||
///
|
||||
/// M3.5-03:可点卡([onTap] 非空)右上角补 `chevron_right`——四张卡本都可点进
|
||||
/// 明细页,但此前无任何视觉提示,用户实测反馈不知道能点。提示形态沿用项目
|
||||
/// 既有可点行/卡(宠物列表卡、健康提醒卡、资料页设置行)的 `chevron_right`,
|
||||
/// 不自创。同时 [MergeSemantics] 把「数值 + 标签」并进 InkWell 的 button 节点,
|
||||
/// 读屏一次读全「¥0,9 月花费,按钮」,而不是两段孤立文字(tap 动作仍在
|
||||
/// InkWell 上,不用 `excludeSemantics` 以免连带丢掉可激活性)。
|
||||
class _SummaryCard extends StatelessWidget {
|
||||
const _SummaryCard({
|
||||
required this.icon,
|
||||
@@ -564,35 +573,51 @@ class _SummaryCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: iconColor),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: emphasized ? AppColors.ink : AppColors.inkSoft,
|
||||
fontSize: emphasized ? 15 : 13,
|
||||
fontWeight: emphasized ? FontWeight.w800 : FontWeight.w600,
|
||||
return MergeSemantics(
|
||||
child: Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: iconColor),
|
||||
const Spacer(),
|
||||
if (onTap != null)
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: AppColors.muted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: AppColors.inkSoft, fontSize: 11),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: emphasized ? AppColors.ink : AppColors.inkSoft,
|
||||
fontSize: emphasized ? 15 : 13,
|
||||
fontWeight: emphasized ? FontWeight.w800 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.inkSoft,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user