Commit c8044b546a999ee92afd87a081ac8e3728bcad9a

Authored by 刘宏哲
1 parent 7293cdd4

feat(app): update ui

Showing 27 changed files with 809 additions and 391 deletions
@@ -317,215 +317,4 @@ class _SleepRange { @@ -317,215 +317,4 @@ class _SleepRange {
317 317
318 final DateTime start; 318 final DateTime start;
319 final DateTime end; 319 final DateTime end;
320 -}  
321 -  
322 -class MockActivityBurnReportDataSource implements ActivityBurnReportDataSource {  
323 - const MockActivityBurnReportDataSource();  
324 -  
325 - @override  
326 - Future<ActivityBurnReport> fetchDailyReport(  
327 - DateTime date, {  
328 - int? targetUserId,  
329 - }) async {  
330 - await Future<void>.delayed(const Duration(milliseconds: 150));  
331 -  
332 - if (date.day == 13) {  
333 - return ActivityBurnReport.empty(date);  
334 - }  
335 -  
336 - final start = DateTime(date.year, date.month, date.day);  
337 - final points = <ActivityBurnHeartRatePoint>[  
338 - for (var i = 0; i < _mockBpms.length; i++)  
339 - ActivityBurnHeartRatePoint(  
340 - time: start.add(Duration(minutes: i * 12)),  
341 - bpm: _mockBpms[i],  
342 - ),  
343 - ];  
344 -  
345 - return ActivityBurnReport(  
346 - date: date,  
347 - activeEnergy: const ActivityBurnMetric(value: 188, goal: 500),  
348 - exerciseMinutes: const ActivityBurnMetric(value: 15, goal: 30),  
349 - standHours: const ActivityBurnMetric(value: 7, goal: 12),  
350 - heartRate: ActivityBurnHeartRateSummary(  
351 - startTime: start,  
352 - endTime: start.add(const Duration(hours: 18)),  
353 - sleepStartTime: start,  
354 - sleepEndTime: start.add(const Duration(hours: 5)),  
355 - userAge: null,  
356 - points: points,  
357 - ),  
358 - );  
359 - }  
360 -  
361 - @override  
362 - Future<WeeklyActivityBurnReport> fetchWeeklyReport(  
363 - DateTime weekStart, {  
364 - int? targetUserId,  
365 - }) async {  
366 - await Future<void>.delayed(const Duration(milliseconds: 150));  
367 -  
368 - final normalizedStart =  
369 - DateTime(weekStart.year, weekStart.month, weekStart.day);  
370 - if (normalizedStart.day == 4) {  
371 - return WeeklyActivityBurnReport.empty(normalizedStart);  
372 - }  
373 -  
374 - final energy = [176, 217, 0, 0, 0, 0, 11];  
375 - final exercise = [12, 17, 0, 0, 0, 0, 0];  
376 - final stand = [7, 5, 0, 0, 0, 0, 0];  
377 - final days = <ActivityBurnReport>[];  
378 -  
379 - for (var i = 0; i < 7; i++) {  
380 - final date = normalizedStart.add(Duration(days: i));  
381 - if (energy[i] == 0 && exercise[i] == 0 && stand[i] == 0) {  
382 - days.add(ActivityBurnReport.empty(date));  
383 - continue;  
384 - }  
385 - days.add(  
386 - ActivityBurnReport(  
387 - date: date,  
388 - activeEnergy: ActivityBurnMetric(value: energy[i], goal: 200),  
389 - exerciseMinutes: ActivityBurnMetric(value: exercise[i], goal: 15),  
390 - standHours: ActivityBurnMetric(value: stand[i], goal: 4),  
391 - ),  
392 - );  
393 - }  
394 -  
395 - return WeeklyActivityBurnReport(  
396 - weekStart: normalizedStart,  
397 - weekEnd: normalizedStart.add(const Duration(days: 6)),  
398 - days: days,  
399 - );  
400 - }  
401 -  
402 - @override  
403 - Future<MonthlyActivityBurnReport> fetchMonthlyReport(  
404 - DateTime monthStart, {  
405 - int? targetUserId,  
406 - }) async {  
407 - await Future<void>.delayed(const Duration(milliseconds: 150));  
408 -  
409 - final start = DateTime(monthStart.year, monthStart.month);  
410 - final end = DateTime(monthStart.year, monthStart.month + 1, 0);  
411 - final energy = [  
412 - 245,  
413 - 319,  
414 - 207,  
415 - 276,  
416 - 303,  
417 - 84,  
418 - 358,  
419 - 144,  
420 - 285,  
421 - 198,  
422 - 342,  
423 - 217,  
424 - 108,  
425 - ];  
426 - final exercise = [18, 20, 15, 17, 16, 6, 24, 8, 19, 12, 20, 17, 9];  
427 - final stand = [8, 9, 7, 8, 8, 3, 10, 5, 9, 6, 8, 5, 4];  
428 - final days = <ActivityBurnReport>[];  
429 -  
430 - for (var i = 0; i < end.day; i++) {  
431 - final date = start.add(Duration(days: i));  
432 - if (i >= energy.length) {  
433 - days.add(ActivityBurnReport.empty(date));  
434 - continue;  
435 - }  
436 - days.add(  
437 - ActivityBurnReport(  
438 - date: date,  
439 - activeEnergy: ActivityBurnMetric(value: energy[i], goal: 200),  
440 - exerciseMinutes: ActivityBurnMetric(value: exercise[i], goal: 15),  
441 - standHours: ActivityBurnMetric(value: stand[i], goal: 4),  
442 - ),  
443 - );  
444 - }  
445 -  
446 - return MonthlyActivityBurnReport(  
447 - monthStart: start,  
448 - monthEnd: end,  
449 - days: days,  
450 - );  
451 - }  
452 -}  
453 -  
454 -const _mockBpms = [  
455 - 53.0,  
456 - 56.0,  
457 - 51.0,  
458 - 58.0,  
459 - 55.0,  
460 - 60.0,  
461 - 52.0,  
462 - 59.0,  
463 - 54.0,  
464 - 61.0,  
465 - 55.0,  
466 - 63.0,  
467 - 58.0,  
468 - 66.0,  
469 - 62.0,  
470 - 70.0,  
471 - 64.0,  
472 - 74.0,  
473 - 92.0,  
474 - 108.0,  
475 - 96.0,  
476 - 124.0,  
477 - 172.0,  
478 - 136.0,  
479 - 188.0,  
480 - 151.0,  
481 - 90.0,  
482 - 82.0,  
483 - 91.0,  
484 - 88.0,  
485 - 94.0,  
486 - 90.0,  
487 - 91.0,  
488 - 89.0,  
489 - 92.0,  
490 - 87.0,  
491 - 91.0,  
492 - 114.0,  
493 - 139.0,  
494 - 120.0,  
495 - 112.0,  
496 - 95.0,  
497 - 85.0,  
498 - 78.0,  
499 - 126.0,  
500 - 93.0,  
501 - 88.0,  
502 - 104.0,  
503 - 112.0,  
504 - 105.0,  
505 - 98.0,  
506 - 87.0,  
507 - 74.0,  
508 - 61.0,  
509 - 55.0,  
510 - 66.0,  
511 - 48.0,  
512 - 62.0,  
513 - 51.0,  
514 - 43.0,  
515 - 59.0,  
516 - 47.0,  
517 - 54.0,  
518 - 40.0,  
519 - 58.0,  
520 - 46.0,  
521 - 50.0,  
522 - 44.0,  
523 - 61.0,  
524 - 57.0,  
525 - 42.0,  
526 - 55.0,  
527 - 49.0,  
528 - 60.0,  
529 - 52.0,  
530 - 64.0,  
531 -]; 320 +}
@@ -7,9 +7,14 @@ class ActivityBurnMetric { @@ -7,9 +7,14 @@ class ActivityBurnMetric {
7 final int value; 7 final int value;
8 final int goal; 8 final int goal;
9 9
  10 + double get rawProgress {
  11 + if (goal <= 0) return 0;
  12 + return value / goal;
  13 + }
  14 +
10 double get progress { 15 double get progress {
11 if (goal <= 0) return 0; 16 if (goal <= 0) return 0;
12 - return (value / goal).clamp(0, 1).toDouble(); 17 + return rawProgress.clamp(0, 1).toDouble();
13 } 18 }
14 } 19 }
15 20
@@ -21,11 +21,13 @@ class ActivityBurnReportView extends StatelessWidget { @@ -21,11 +21,13 @@ class ActivityBurnReportView extends StatelessWidget {
21 required this.logic, 21 required this.logic,
22 required this.isVip, 22 required this.isVip,
23 required this.onSubscribe, 23 required this.onSubscribe,
  24 + this.loadingIndicator = const ReportLoadingIndicator(),
24 }); 25 });
25 26
26 final ActivityBurnReportLogic logic; 27 final ActivityBurnReportLogic logic;
27 final bool isVip; 28 final bool isVip;
28 final ValueChanged<String> onSubscribe; 29 final ValueChanged<String> onSubscribe;
  30 + final Widget loadingIndicator;
29 31
30 @override 32 @override
31 Widget build(BuildContext context) { 33 Widget build(BuildContext context) {
@@ -59,7 +61,7 @@ class ActivityBurnReportView extends StatelessWidget { @@ -59,7 +61,7 @@ class ActivityBurnReportView extends StatelessWidget {
59 child: Obx( 61 child: Obx(
60 () { 62 () {
61 if (logic.isLoading.value) { 63 if (logic.isLoading.value) {
62 - return const ReportLoadingIndicator(); 64 + return loadingIndicator;
63 } 65 }
64 return CustomScrollView( 66 return CustomScrollView(
65 physics: const ClampingScrollPhysics(), 67 physics: const ClampingScrollPhysics(),
@@ -93,9 +93,9 @@ class _RingProgress { @@ -93,9 +93,9 @@ class _RingProgress {
93 stand = 0; 93 stand = 0;
94 94
95 factory _RingProgress.fromReport(ActivityBurnReport? report) => _RingProgress( 95 factory _RingProgress.fromReport(ActivityBurnReport? report) => _RingProgress(
96 - active: report?.activeEnergy?.progress ?? 0,  
97 - exercise: report?.exerciseMinutes?.progress ?? 0,  
98 - stand: report?.standHours?.progress ?? 0, 96 + active: report?.activeEnergy?.rawProgress ?? 0,
  97 + exercise: report?.exerciseMinutes?.rawProgress ?? 0,
  98 + stand: report?.standHours?.rawProgress ?? 0,
99 ); 99 );
100 100
101 final double active; 101 final double active;
@@ -253,7 +253,16 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -253,7 +253,16 @@ class _ActivityBurnRingPainter extends CustomPainter {
253 required double progress, 253 required double progress,
254 required double startAngle, 254 required double startAngle,
255 }) { 255 }) {
  256 + if (progress <= 0) return;
  257 +
256 final rect = Rect.fromCircle(center: center, radius: radius); 258 final rect = Rect.fromCircle(center: center, radius: radius);
  259 + final safeProgress = math.max(0, progress);
  260 + final hasOverflow = safeProgress > 1;
  261 + final baseProgress = hasOverflow ? 1.0 : safeProgress;
  262 + final overflowProgress = safeProgress - 1;
  263 + final overflowLap = overflowProgress % 1;
  264 + final visibleOverflowLap =
  265 + hasOverflow && overflowLap == 0 ? 1.0 : overflowLap;
257 266
258 final progressPaint = Paint() 267 final progressPaint = Paint()
259 ..color = progressColor 268 ..color = progressColor
@@ -261,16 +270,86 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -261,16 +270,86 @@ class _ActivityBurnRingPainter extends CustomPainter {
261 ..strokeWidth = width 270 ..strokeWidth = width
262 ..strokeCap = StrokeCap.round; 271 ..strokeCap = StrokeCap.round;
263 272
264 - if (progress <= 0) return; 273 + final fullLapPaint = Paint()
  274 + ..color = progressColor
  275 + ..style = PaintingStyle.stroke
  276 + ..strokeWidth = width
  277 + ..strokeCap = StrokeCap.butt;
  278 +
  279 + if (baseProgress >= 1) {
  280 + canvas.drawArc(
  281 + rect,
  282 + startAngle,
  283 + math.pi * 2,
  284 + false,
  285 + fullLapPaint,
  286 + );
  287 + } else {
  288 + final baseEndAngle = startAngle + math.pi * 2 * baseProgress;
  289 + _drawRingHeadShadow(
  290 + canvas,
  291 + center: center,
  292 + radius: radius,
  293 + width: width,
  294 + angle: baseEndAngle,
  295 + );
  296 + canvas.drawArc(
  297 + rect,
  298 + startAngle,
  299 + math.pi * 2 * baseProgress,
  300 + false,
  301 + progressPaint,
  302 + );
  303 + }
  304 +
  305 + if (!hasOverflow || visibleOverflowLap <= 0) return;
  306 +
  307 + final overflowEndAngle = startAngle + math.pi * 2 * visibleOverflowLap;
  308 + _drawRingHeadShadow(
  309 + canvas,
  310 + center: center,
  311 + radius: radius,
  312 + width: width,
  313 + angle: overflowEndAngle,
  314 + );
265 canvas.drawArc( 315 canvas.drawArc(
266 rect, 316 rect,
267 startAngle, 317 startAngle,
268 - math.pi * 2 * progress, 318 + math.pi * 2 * visibleOverflowLap,
269 false, 319 false,
270 progressPaint, 320 progressPaint,
271 ); 321 );
272 } 322 }
273 323
  324 + void _drawRingHeadShadow(
  325 + Canvas canvas, {
  326 + required Offset center,
  327 + required double radius,
  328 + required double width,
  329 + required double angle,
  330 + }) {
  331 + final headCenter = Offset(
  332 + center.dx + math.cos(angle) * radius,
  333 + center.dy + math.sin(angle) * radius,
  334 + );
  335 + final tangent = Offset(-math.sin(angle), math.cos(angle));
  336 + final ringClip = Path()
  337 + ..fillType = PathFillType.evenOdd
  338 + ..addOval(Rect.fromCircle(center: center, radius: radius + width / 2))
  339 + ..addOval(Rect.fromCircle(center: center, radius: radius - width / 2));
  340 +
  341 + canvas.save();
  342 + canvas.clipPath(ringClip);
  343 + canvas.drawCircle(
  344 + headCenter + tangent * 2,
  345 + width / 2,
  346 + Paint()
  347 + ..color = Colors.black.withValues(alpha: 0.36)
  348 + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2),
  349 + );
  350 + canvas.restore();
  351 + }
  352 +
274 @override 353 @override
275 bool shouldRepaint(covariant _ActivityBurnRingPainter oldDelegate) { 354 bool shouldRepaint(covariant _ActivityBurnRingPainter oldDelegate) {
276 return oldDelegate.progress != progress || oldDelegate.hasData != hasData; 355 return oldDelegate.progress != progress || oldDelegate.hasData != hasData;
@@ -558,9 +558,11 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { @@ -558,9 +558,11 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> {
558 if (percent == null || !widget.report.hasData) { 558 if (percent == null || !widget.report.hasData) {
559 return context.l10n.activityComparedUnavailable; 559 return context.l10n.activityComparedUnavailable;
560 } 560 }
561 - if (percent == 0) return '与上周持平';  
562 - final direction = percent > 0 ? '多' : '少';  
563 - return '比上周$direction${percent.abs()}%'; 561 + if (percent == 0) return context.l10n.activitySameAsLastWeek;
  562 + final absPercent = percent.abs();
  563 + return percent > 0
  564 + ? context.l10n.activityMoreThanLastWeek(absPercent)
  565 + : context.l10n.activityLessThanLastWeek(absPercent);
564 } 566 }
565 567
566 double _barToY(int value, double maxY) { 568 double _barToY(int value, double maxY) {
@@ -43,13 +43,21 @@ class FriendsController extends GetxController { @@ -43,13 +43,21 @@ class FriendsController extends GetxController {
43 unawaited(refreshData()); 43 unawaited(refreshData());
44 } 44 }
45 45
46 - void markPageVisible() { 46 + void setPageVisible(bool visible) {
  47 + if (visible) {
  48 + _markPageVisible();
  49 + } else {
  50 + _markPageHidden();
  51 + }
  52 + }
  53 +
  54 + void _markPageVisible() {
47 if (_isPageVisible) return; 55 if (_isPageVisible) return;
48 _isPageVisible = true; 56 _isPageVisible = true;
49 ta.track('enter_doublefeel_friend_page'); 57 ta.track('enter_doublefeel_friend_page');
50 } 58 }
51 59
52 - void markPageHidden() { 60 + void _markPageHidden() {
53 _isPageVisible = false; 61 _isPageVisible = false;
54 } 62 }
55 63
@@ -110,7 +110,8 @@ class FriendsRepositoryImpl implements FriendsRepository { @@ -110,7 +110,8 @@ class FriendsRepositoryImpl implements FriendsRepository {
110 steps: healthData?.totalSteps == null 110 steps: healthData?.totalSteps == null
111 ? null 111 ? null
112 : l10n.friendsStepCount(healthData!.totalSteps!), 112 : l10n.friendsStepCount(healthData!.totalSteps!),
113 - stressState: FriendStressState.fromValue(healthData?.hrvState), 113 + stressState:
  114 + FriendStressState.fromValue(healthData?.comprehensiveStressState),
114 isOnWatchFace: friend.isShowInDial, 115 isOnWatchFace: friend.isShowInDial,
115 ); 116 );
116 } 117 }
@@ -125,7 +126,8 @@ class FriendsRepositoryImpl implements FriendsRepository { @@ -125,7 +126,8 @@ class FriendsRepositoryImpl implements FriendsRepository {
125 steps: healthData.totalSteps == null 126 steps: healthData.totalSteps == null
126 ? null 127 ? null
127 : l10n.friendsStepCount(healthData.totalSteps!), 128 : l10n.friendsStepCount(healthData.totalSteps!),
128 - stressState: FriendStressState.fromValue(healthData.hrvState), 129 + stressState:
  130 + FriendStressState.fromValue(healthData.comprehensiveStressState),
129 ); 131 );
130 } 132 }
131 133
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:get/get.dart'; 2 import 'package:get/get.dart';
  3 +import 'package:lottie/lottie.dart';
3 4
4 import '../../../../core/constants/intent_keys.dart'; 5 import '../../../../core/constants/intent_keys.dart';
5 import '../../../../data/local/user_preferences_storage.dart'; 6 import '../../../../data/local/user_preferences_storage.dart';
@@ -154,6 +155,23 @@ class _HealthTrendContentState extends State<HealthTrendContent> @@ -154,6 +155,23 @@ class _HealthTrendContentState extends State<HealthTrendContent>
154 } 155 }
155 } 156 }
156 157
  158 +class _HealthTrendLoadingIndicator extends StatelessWidget {
  159 + const _HealthTrendLoadingIndicator();
  160 +
  161 + @override
  162 + Widget build(BuildContext context) {
  163 + return Center(
  164 + child: Lottie.asset(
  165 + 'assets/lottie/loading.json',
  166 + width: 130,
  167 + height: 90,
  168 + repeat: true,
  169 + animate: true,
  170 + ),
  171 + );
  172 + }
  173 +}
  174 +
157 class _HrvTrendSection extends StatefulWidget { 175 class _HrvTrendSection extends StatefulWidget {
158 const _HrvTrendSection({ 176 const _HrvTrendSection({
159 required this.query, 177 required this.query,
@@ -247,6 +265,7 @@ class _HrvTrendSectionState extends State<_HrvTrendSection> { @@ -247,6 +265,7 @@ class _HrvTrendSectionState extends State<_HrvTrendSection> {
247 logic: _logic, 265 logic: _logic,
248 isVip: widget.isVip, 266 isVip: widget.isVip,
249 onSubscribe: widget.onSubscribe, 267 onSubscribe: widget.onSubscribe,
  268 + loadingIndicator: const _HealthTrendLoadingIndicator(),
250 ); 269 );
251 } 270 }
252 271
@@ -346,6 +365,7 @@ class _ActivityBurnTrendSectionState extends State<_ActivityBurnTrendSection> { @@ -346,6 +365,7 @@ class _ActivityBurnTrendSectionState extends State<_ActivityBurnTrendSection> {
346 logic: _logic, 365 logic: _logic,
347 isVip: widget.isVip, 366 isVip: widget.isVip,
348 onSubscribe: widget.onSubscribe, 367 onSubscribe: widget.onSubscribe,
  368 + loadingIndicator: const _HealthTrendLoadingIndicator(),
349 ); 369 );
350 } 370 }
351 371
@@ -444,6 +464,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> { @@ -444,6 +464,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
444 logic: _logic, 464 logic: _logic,
445 isVip: widget.isVip, 465 isVip: widget.isVip,
446 onSubscribe: widget.onSubscribe, 466 onSubscribe: widget.onSubscribe,
  467 + loadingIndicator: const _HealthTrendLoadingIndicator(),
447 ); 468 );
448 } 469 }
449 470
@@ -21,6 +21,11 @@ import '../../report_common/models/report_period.dart'; @@ -21,6 +21,11 @@ import '../../report_common/models/report_period.dart';
21 import 'today_controller.dart'; 21 import 'today_controller.dart';
22 import 'trend/trend_controller.dart'; 22 import 'trend/trend_controller.dart';
23 23
  24 +enum TrendEntrySource {
  25 + bottomTab,
  26 + jump,
  27 +}
  28 +
24 class HomeController extends GetxController { 29 class HomeController extends GetxController {
25 static const trendTabIndex = 1; 30 static const trendTabIndex = 1;
26 static const friendsTabIndex = 2; 31 static const friendsTabIndex = 2;
@@ -136,23 +141,16 @@ class HomeController extends GetxController { @@ -136,23 +141,16 @@ class HomeController extends GetxController {
136 int index, { 141 int index, {
137 TrendEntrySource trendEntrySource = TrendEntrySource.bottomTab, 142 TrendEntrySource trendEntrySource = TrendEntrySource.bottomTab,
138 }) { 143 }) {
139 - final previousIndex = selectedIndex.value;  
140 selectedIndex.value = index; 144 selectedIndex.value = index;
141 - if (index == trendTabIndex) {  
142 - final trendController = Get.find<TrendController>();  
143 - if (previousIndex != trendTabIndex) {  
144 - trendController.markPageVisible(source: trendEntrySource);  
145 - } else {  
146 - trendController.updateEntrySource(trendEntrySource);  
147 - }  
148 - } else if (previousIndex == trendTabIndex && index != trendTabIndex) {  
149 - Get.find<TrendController>().markPageHidden();  
150 - }  
151 - if (index == friendsTabIndex && previousIndex != friendsTabIndex) {  
152 - Get.find<FriendsController>().markPageVisible();  
153 - } else if (previousIndex == friendsTabIndex && index != friendsTabIndex) {  
154 - Get.find<FriendsController>().markPageHidden();  
155 - } 145 + final trendController = Get.find<TrendController>();
  146 + final friendsController = Get.find<FriendsController>();
  147 +
  148 + trendController.setPageVisible(
  149 + index == trendTabIndex,
  150 + resetQueryOnShow: trendEntrySource == TrendEntrySource.bottomTab,
  151 + );
  152 + friendsController.setPageVisible(index == friendsTabIndex);
  153 +
156 switch (index) { 154 switch (index) {
157 case 0: 155 case 0:
158 Get.find<TodayController>().refreshTab(); 156 Get.find<TodayController>().refreshTab();
@@ -8,6 +8,7 @@ import '../../../../../core/result/app_result.dart'; @@ -8,6 +8,7 @@ import '../../../../../core/result/app_result.dart';
8 import '../../../../../data/models/friend/friend_models.dart'; 8 import '../../../../../data/models/friend/friend_models.dart';
9 import '../../../health_trend/controllers/health_trend_analytics.dart'; 9 import '../../../health_trend/controllers/health_trend_analytics.dart';
10 import '../../../health_trend/controllers/health_trend_control.dart'; 10 import '../../../health_trend/controllers/health_trend_control.dart';
  11 +import '../../../report_common/config/report_date_range_config.dart';
11 import '../../../report_common/models/health_report_query.dart'; 12 import '../../../report_common/models/health_report_query.dart';
12 import '../../../report_common/models/report_period.dart'; 13 import '../../../report_common/models/report_period.dart';
13 import '../../widgets/trend/trend_friend_select_bottom_sheet.dart'; 14 import '../../widgets/trend/trend_friend_select_bottom_sheet.dart';
@@ -20,11 +21,6 @@ enum TrendType { @@ -20,11 +21,6 @@ enum TrendType {
20 int get tabIndex => index; 21 int get tabIndex => index;
21 } 22 }
22 23
23 -enum TrendEntrySource {  
24 - bottomTab,  
25 - jump,  
26 -}  
27 -  
28 /// 趋势页顶层 Controller,仅负责: 24 /// 趋势页顶层 Controller,仅负责:
29 /// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex) 25 /// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex)
30 class TrendController extends GetxController with HealthTrendControl { 26 class TrendController extends GetxController with HealthTrendControl {
@@ -33,7 +29,6 @@ class TrendController extends GetxController with HealthTrendControl { @@ -33,7 +29,6 @@ class TrendController extends GetxController with HealthTrendControl {
33 final FriendApi _friendApi; 29 final FriendApi _friendApi;
34 bool _isPageVisible = false; 30 bool _isPageVisible = false;
35 final refreshToken = 0.obs; 31 final refreshToken = 0.obs;
36 - final entrySource = TrendEntrySource.bottomTab.obs;  
37 32
38 // 当前查看的用户。null 表示查看自己;非 null 表示查看指定用户。 33 // 当前查看的用户。null 表示查看自己;非 null 表示查看指定用户。
39 final targetUserId = RxnInt(); 34 final targetUserId = RxnInt();
@@ -147,30 +142,34 @@ class TrendController extends GetxController with HealthTrendControl { @@ -147,30 +142,34 @@ class TrendController extends GetxController with HealthTrendControl {
147 ); 142 );
148 } 143 }
149 144
150 - void updateEntrySource(TrendEntrySource source) {  
151 - entrySource.value = source;  
152 - }  
153 -  
154 - void markPageVisible({  
155 - TrendEntrySource source = TrendEntrySource.bottomTab, 145 + void setPageVisible(
  146 + bool visible, {
  147 + bool resetQueryOnShow = false,
156 }) { 148 }) {
157 - updateEntrySource(source);  
158 - if (source == TrendEntrySource.bottomTab) {  
159 - _resetQueryForBottomTabEntry(); 149 + if (!visible) {
  150 + _markPageHidden();
  151 + return;
  152 + }
  153 + if (resetQueryOnShow) {
  154 + _resetToDefaultQuery();
160 } 155 }
  156 + _markPageVisible();
  157 + }
  158 +
  159 + void _markPageVisible() {
161 if (_isPageVisible) return; 160 if (_isPageVisible) return;
162 _isPageVisible = true; 161 _isPageVisible = true;
163 refreshToken.value++; 162 refreshToken.value++;
164 _trackEnterPage(); 163 _trackEnterPage();
165 } 164 }
166 165
167 - void markPageHidden() { 166 + void _markPageHidden() {
168 _isPageVisible = false; 167 _isPageVisible = false;
169 } 168 }
170 169
171 - void _resetQueryForBottomTabEntry() { 170 + void _resetToDefaultQuery() {
172 changeType(TrendType.hrv.tabIndex); 171 changeType(TrendType.hrv.tabIndex);
173 - changeQuery(ReportPeriod.week, DateTime.now()); 172 + changeQuery(ReportPeriod.week, ReportDateRangeConfig.lastWeekStart());
174 } 173 }
175 174
176 void _trackEnterPage() { 175 void _trackEnterPage() {
1 import 'package:cached_network_image/cached_network_image.dart'; 1 import 'package:cached_network_image/cached_network_image.dart';
2 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 2 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  3 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
4 import 'package:get/get.dart'; 5 import 'package:get/get.dart';
5 6
@@ -55,11 +56,11 @@ class _HomeTrendHeader extends GetView<TrendController> { @@ -55,11 +56,11 @@ class _HomeTrendHeader extends GetView<TrendController> {
55 child: Stack( 56 child: Stack(
56 alignment: Alignment.center, 57 alignment: Alignment.center,
57 children: [ 58 children: [
58 - const Align( 59 + Align(
59 alignment: Alignment.centerLeft, 60 alignment: Alignment.centerLeft,
60 child: Text( 61 child: Text(
61 - '趋势',  
62 - style: TextStyle( 62 + l10n.tabTrend,
  63 + style: const TextStyle(
63 color: Color(0xFF0F0F11), 64 color: Color(0xFF0F0F11),
64 fontSize: 24, 65 fontSize: 24,
65 fontWeight: FontWeight.w600, 66 fontWeight: FontWeight.w600,
1 import 'package:fl_chart/fl_chart.dart'; 1 import 'package:fl_chart/fl_chart.dart';
  2 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
2 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
3 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
4 5
@@ -62,15 +63,16 @@ class _ActivitySummaryCard extends GetView<ActivityController> { @@ -62,15 +63,16 @@ class _ActivitySummaryCard extends GetView<ActivityController> {
62 borderRadius: BorderRadius.circular(16), 63 borderRadius: BorderRadius.circular(16),
63 ), 64 ),
64 child: Obx(() { 65 child: Obx(() {
65 - final periodLabel = controller.currentPeriod.value.label; 66 + final periodLabel =
  67 + _periodLabel(context, controller.currentPeriod.value);
66 return Column( 68 return Column(
67 crossAxisAlignment: CrossAxisAlignment.start, 69 crossAxisAlignment: CrossAxisAlignment.start,
68 children: [ 70 children: [
69 Row( 71 Row(
70 children: [ 72 children: [
71 - const Text(  
72 - '活动消耗趋势',  
73 - style: TextStyle( 73 + Text(
  74 + context.l10n.activityTrendTitle,
  75 + style: const TextStyle(
74 color: _h1, 76 color: _h1,
75 fontSize: 16, 77 fontSize: 16,
76 fontWeight: FontWeight.w600, 78 fontWeight: FontWeight.w600,
@@ -84,14 +86,14 @@ class _ActivitySummaryCard extends GetView<ActivityController> { @@ -84,14 +86,14 @@ class _ActivitySummaryCard extends GetView<ActivityController> {
84 Row( 86 Row(
85 children: [ 87 children: [
86 _StatItem( 88 _StatItem(
87 - label: '本$periodLabel总消耗', 89 + label: context.l10n.activityPeriodTotalBurn(periodLabel),
88 value: controller.totalBurn.value, 90 value: controller.totalBurn.value,
89 unit: 'kcal', 91 unit: 'kcal',
90 color: _activeColor, 92 color: _activeColor,
91 ), 93 ),
92 const SizedBox(width: 24), 94 const SizedBox(width: 24),
93 _StatItem( 95 _StatItem(
94 - label: '日均消耗', 96 + label: context.l10n.activityDailyAverageBurn,
95 value: controller.averageBurn.value, 97 value: controller.averageBurn.value,
96 unit: 'kcal', 98 unit: 'kcal',
97 color: const Color(0xFF3BD49D), 99 color: const Color(0xFF3BD49D),
@@ -103,6 +105,13 @@ class _ActivitySummaryCard extends GetView<ActivityController> { @@ -103,6 +105,13 @@ class _ActivitySummaryCard extends GetView<ActivityController> {
103 }), 105 }),
104 ); 106 );
105 } 107 }
  108 +
  109 + String _periodLabel(BuildContext context, TrendPeriod period) =>
  110 + switch (period) {
  111 + TrendPeriod.week => context.l10n.reportPeriodWeek,
  112 + TrendPeriod.month => context.l10n.reportPeriodMonth,
  113 + TrendPeriod.year => context.l10n.reportPeriodYear,
  114 + };
106 } 115 }
107 116
108 class _StatItem extends StatelessWidget { 117 class _StatItem extends StatelessWidget {
@@ -173,11 +182,12 @@ class _ActivityChartCard extends GetView<ActivityController> { @@ -173,11 +182,12 @@ class _ActivityChartCard extends GetView<ActivityController> {
173 ), 182 ),
174 child: Obx(() { 183 child: Obx(() {
175 final period = controller.currentPeriod.value; 184 final period = controller.currentPeriod.value;
  185 + final periodLabel = _periodLabel(context, period);
176 return Column( 186 return Column(
177 crossAxisAlignment: CrossAxisAlignment.start, 187 crossAxisAlignment: CrossAxisAlignment.start,
178 children: [ 188 children: [
179 Text( 189 Text(
180 - '活动消耗${period.label}趋势图', 190 + context.l10n.activityPeriodTrendChart(periodLabel),
181 style: const TextStyle( 191 style: const TextStyle(
182 color: _h1, 192 color: _h1,
183 fontSize: 14, 193 fontSize: 14,
@@ -195,6 +205,13 @@ class _ActivityChartCard extends GetView<ActivityController> { @@ -195,6 +205,13 @@ class _ActivityChartCard extends GetView<ActivityController> {
195 ); 205 );
196 } 206 }
197 207
  208 + String _periodLabel(BuildContext context, TrendPeriod period) =>
  209 + switch (period) {
  210 + TrendPeriod.week => context.l10n.reportPeriodWeek,
  211 + TrendPeriod.month => context.l10n.reportPeriodMonth,
  212 + TrendPeriod.year => context.l10n.reportPeriodYear,
  213 + };
  214 +
198 Widget _buildChart() { 215 Widget _buildChart() {
199 if (controller.chartData.isEmpty) { 216 if (controller.chartData.isEmpty) {
200 return const Center( 217 return const Center(
@@ -239,10 +256,10 @@ class _ActivityChartCard extends GetView<ActivityController> { @@ -239,10 +256,10 @@ class _ActivityChartCard extends GetView<ActivityController> {
239 ), 256 ),
240 borderData: FlBorderData(show: false), 257 borderData: FlBorderData(show: false),
241 titlesData: FlTitlesData( 258 titlesData: FlTitlesData(
242 - topTitles: const AxisTitles(  
243 - sideTitles: SideTitles(showTitles: false)),  
244 - rightTitles: const AxisTitles(  
245 - sideTitles: SideTitles(showTitles: false)), 259 + topTitles:
  260 + const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  261 + rightTitles:
  262 + const AxisTitles(sideTitles: SideTitles(showTitles: false)),
246 leftTitles: AxisTitles( 263 leftTitles: AxisTitles(
247 sideTitles: SideTitles( 264 sideTitles: SideTitles(
248 showTitles: true, 265 showTitles: true,
1 import 'package:fl_chart/fl_chart.dart'; 1 import 'package:fl_chart/fl_chart.dart';
  2 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
2 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
3 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
4 5
@@ -65,15 +66,16 @@ class _HrvSummaryCard extends GetView<HrvController> { @@ -65,15 +66,16 @@ class _HrvSummaryCard extends GetView<HrvController> {
65 borderRadius: BorderRadius.circular(16), 66 borderRadius: BorderRadius.circular(16),
66 ), 67 ),
67 child: Obx(() { 68 child: Obx(() {
68 - final periodLabel = controller.currentPeriod.value.label; 69 + final periodLabel =
  70 + _periodLabel(context, controller.currentPeriod.value);
69 return Column( 71 return Column(
70 crossAxisAlignment: CrossAxisAlignment.start, 72 crossAxisAlignment: CrossAxisAlignment.start,
71 children: [ 73 children: [
72 Row( 74 Row(
73 children: [ 75 children: [
74 - const Text(  
75 - 'HRV趋势',  
76 - style: TextStyle( 76 + Text(
  77 + context.l10n.hrvTrendTitle,
  78 + style: const TextStyle(
77 color: _h1, 79 color: _h1,
78 fontSize: 16, 80 fontSize: 16,
79 fontWeight: FontWeight.w600, 81 fontWeight: FontWeight.w600,
@@ -87,14 +89,14 @@ class _HrvSummaryCard extends GetView<HrvController> { @@ -87,14 +89,14 @@ class _HrvSummaryCard extends GetView<HrvController> {
87 Row( 89 Row(
88 children: [ 90 children: [
89 _StatItem( 91 _StatItem(
90 - label: '本$periodLabel平均', 92 + label: context.l10n.hrvPeriodAverage(periodLabel),
91 value: controller.averageHrv.value, 93 value: controller.averageHrv.value,
92 unit: 'ms', 94 unit: 'ms',
93 color: _brandColor, 95 color: _brandColor,
94 ), 96 ),
95 const SizedBox(width: 24), 97 const SizedBox(width: 24),
96 _StatItem( 98 _StatItem(
97 - label: '较上$periodLabel', 99 + label: context.l10n.hrvComparedPreviousPeriod(periodLabel),
98 value: controller.changeHrv.value, 100 value: controller.changeHrv.value,
99 unit: 'ms', 101 unit: 'ms',
100 color: const Color(0xFF3BD49D), 102 color: const Color(0xFF3BD49D),
@@ -106,6 +108,13 @@ class _HrvSummaryCard extends GetView<HrvController> { @@ -106,6 +108,13 @@ class _HrvSummaryCard extends GetView<HrvController> {
106 }), 108 }),
107 ); 109 );
108 } 110 }
  111 +
  112 + String _periodLabel(BuildContext context, TrendPeriod period) =>
  113 + switch (period) {
  114 + TrendPeriod.week => context.l10n.reportPeriodWeek,
  115 + TrendPeriod.month => context.l10n.reportPeriodMonth,
  116 + TrendPeriod.year => context.l10n.reportPeriodYear,
  117 + };
109 } 118 }
110 119
111 class _StatItem extends StatelessWidget { 120 class _StatItem extends StatelessWidget {
@@ -179,11 +188,12 @@ class _HrvChartCard extends GetView<HrvController> { @@ -179,11 +188,12 @@ class _HrvChartCard extends GetView<HrvController> {
179 ), 188 ),
180 child: Obx(() { 189 child: Obx(() {
181 final period = controller.currentPeriod.value; 190 final period = controller.currentPeriod.value;
  191 + final periodLabel = _periodLabel(context, period);
182 return Column( 192 return Column(
183 crossAxisAlignment: CrossAxisAlignment.start, 193 crossAxisAlignment: CrossAxisAlignment.start,
184 children: [ 194 children: [
185 Text( 195 Text(
186 - 'HRV ${period.label}趋势图', 196 + context.l10n.hrvPeriodTrendChart(periodLabel),
187 style: const TextStyle( 197 style: const TextStyle(
188 color: _h1, 198 color: _h1,
189 fontSize: 14, 199 fontSize: 14,
@@ -201,6 +211,13 @@ class _HrvChartCard extends GetView<HrvController> { @@ -201,6 +211,13 @@ class _HrvChartCard extends GetView<HrvController> {
201 ); 211 );
202 } 212 }
203 213
  214 + String _periodLabel(BuildContext context, TrendPeriod period) =>
  215 + switch (period) {
  216 + TrendPeriod.week => context.l10n.reportPeriodWeek,
  217 + TrendPeriod.month => context.l10n.reportPeriodMonth,
  218 + TrendPeriod.year => context.l10n.reportPeriodYear,
  219 + };
  220 +
204 Widget _buildChart(TrendPeriod period) { 221 Widget _buildChart(TrendPeriod period) {
205 if (controller.chartData.isEmpty) { 222 if (controller.chartData.isEmpty) {
206 return const Center( 223 return const Center(
1 import 'package:fl_chart/fl_chart.dart'; 1 import 'package:fl_chart/fl_chart.dart';
  2 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
2 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
3 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
4 5
@@ -62,15 +63,16 @@ class _SleepSummaryCard extends GetView<SleepController> { @@ -62,15 +63,16 @@ class _SleepSummaryCard extends GetView<SleepController> {
62 borderRadius: BorderRadius.circular(16), 63 borderRadius: BorderRadius.circular(16),
63 ), 64 ),
64 child: Obx(() { 65 child: Obx(() {
65 - final periodLabel = controller.currentPeriod.value.label; 66 + final periodLabel =
  67 + _periodLabel(context, controller.currentPeriod.value);
66 return Column( 68 return Column(
67 crossAxisAlignment: CrossAxisAlignment.start, 69 crossAxisAlignment: CrossAxisAlignment.start,
68 children: [ 70 children: [
69 Row( 71 Row(
70 children: [ 72 children: [
71 - const Text(  
72 - '睡眠报告趋势',  
73 - style: TextStyle( 73 + Text(
  74 + context.l10n.sleepTrendTitle,
  75 + style: const TextStyle(
74 color: _h1, 76 color: _h1,
75 fontSize: 16, 77 fontSize: 16,
76 fontWeight: FontWeight.w600, 78 fontWeight: FontWeight.w600,
@@ -84,14 +86,14 @@ class _SleepSummaryCard extends GetView<SleepController> { @@ -84,14 +86,14 @@ class _SleepSummaryCard extends GetView<SleepController> {
84 Row( 86 Row(
85 children: [ 87 children: [
86 _StatItem( 88 _StatItem(
87 - label: '本$periodLabel均睡眠', 89 + label: context.l10n.sleepPeriodAverageDuration(periodLabel),
88 value: controller.averageSleep.value, 90 value: controller.averageSleep.value,
89 unit: 'h', 91 unit: 'h',
90 color: _sleepColor, 92 color: _sleepColor,
91 ), 93 ),
92 const SizedBox(width: 24), 94 const SizedBox(width: 24),
93 _StatItem( 95 _StatItem(
94 - label: '深睡占比', 96 + label: context.l10n.sleepDeepSleepRatio,
95 value: controller.deepSleepRatio.value, 97 value: controller.deepSleepRatio.value,
96 unit: '%', 98 unit: '%',
97 color: const Color(0xFF845EEE), 99 color: const Color(0xFF845EEE),
@@ -103,6 +105,13 @@ class _SleepSummaryCard extends GetView<SleepController> { @@ -103,6 +105,13 @@ class _SleepSummaryCard extends GetView<SleepController> {
103 }), 105 }),
104 ); 106 );
105 } 107 }
  108 +
  109 + String _periodLabel(BuildContext context, TrendPeriod period) =>
  110 + switch (period) {
  111 + TrendPeriod.week => context.l10n.reportPeriodWeek,
  112 + TrendPeriod.month => context.l10n.reportPeriodMonth,
  113 + TrendPeriod.year => context.l10n.reportPeriodYear,
  114 + };
106 } 115 }
107 116
108 class _StatItem extends StatelessWidget { 117 class _StatItem extends StatelessWidget {
@@ -170,11 +179,12 @@ class _SleepChartCard extends GetView<SleepController> { @@ -170,11 +179,12 @@ class _SleepChartCard extends GetView<SleepController> {
170 ), 179 ),
171 child: Obx(() { 180 child: Obx(() {
172 final period = controller.currentPeriod.value; 181 final period = controller.currentPeriod.value;
  182 + final periodLabel = _periodLabel(context, period);
173 return Column( 183 return Column(
174 crossAxisAlignment: CrossAxisAlignment.start, 184 crossAxisAlignment: CrossAxisAlignment.start,
175 children: [ 185 children: [
176 Text( 186 Text(
177 - '睡眠时长${period.label}趋势图', 187 + context.l10n.sleepDurationPeriodTrendChart(periodLabel),
178 style: const TextStyle( 188 style: const TextStyle(
179 color: _h1, 189 color: _h1,
180 fontSize: 14, 190 fontSize: 14,
@@ -192,6 +202,13 @@ class _SleepChartCard extends GetView<SleepController> { @@ -192,6 +202,13 @@ class _SleepChartCard extends GetView<SleepController> {
192 ); 202 );
193 } 203 }
194 204
  205 + String _periodLabel(BuildContext context, TrendPeriod period) =>
  206 + switch (period) {
  207 + TrendPeriod.week => context.l10n.reportPeriodWeek,
  208 + TrendPeriod.month => context.l10n.reportPeriodMonth,
  209 + TrendPeriod.year => context.l10n.reportPeriodYear,
  210 + };
  211 +
195 Widget _buildChart() { 212 Widget _buildChart() {
196 if (controller.chartData.isEmpty) { 213 if (controller.chartData.isEmpty) {
197 return const Center( 214 return const Center(
@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
2 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 2 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
3 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 3 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
4 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; 4 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
  5 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
5 import 'package:flutter/material.dart'; 6 import 'package:flutter/material.dart';
6 import 'package:get/get.dart'; 7 import 'package:get/get.dart';
7 8
@@ -39,7 +40,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { @@ -39,7 +40,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
39 children: [ 40 children: [
40 Center( 41 Center(
41 child: Text( 42 child: Text(
42 - '选择好友', 43 + context.l10n.friendsSelect,
43 style: TextStyle( 44 style: TextStyle(
44 color: context.colors.textPrimary, 45 color: context.colors.textPrimary,
45 fontSize: 16, 46 fontSize: 16,
@@ -89,8 +90,10 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { @@ -89,8 +90,10 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
89 return _BottomSheetUserRow( 90 return _BottomSheetUserRow(
90 name: selfNickname?.isNotEmpty == true 91 name: selfNickname?.isNotEmpty == true
91 ? selfNickname! 92 ? selfNickname!
92 - : '我',  
93 - subtitle: selfNickname?.isNotEmpty == true ? '我' : null, 93 + : context.l10n.friendsMe,
  94 + subtitle: selfNickname?.isNotEmpty == true
  95 + ? context.l10n.friendsMe
  96 + : null,
94 avatarUrl: self?.avatar, 97 avatarUrl: self?.avatar,
95 isSelected: currentSelectedId == null, 98 isSelected: currentSelectedId == null,
96 onTap: () { 99 onTap: () {
@@ -101,7 +104,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { @@ -101,7 +104,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
101 } 104 }
102 105
103 final friend = friendsList[index - 1]; 106 final friend = friendsList[index - 1];
104 - final name = _friendName(friend); 107 + final name = _friendName(context, friend);
105 final nickname = friend.friendNickname?.trim(); 108 final nickname = friend.friendNickname?.trim();
106 return _BottomSheetUserRow( 109 return _BottomSheetUserRow(
107 name: name, 110 name: name,
@@ -123,14 +126,14 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { @@ -123,14 +126,14 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
123 ); 126 );
124 } 127 }
125 128
126 - String _friendName(FriendItem friend) { 129 + String _friendName(BuildContext context, FriendItem friend) {
127 final remark = friend.remarkName?.trim(); 130 final remark = friend.remarkName?.trim();
128 if (remark?.isNotEmpty == true) return remark!; 131 if (remark?.isNotEmpty == true) return remark!;
129 132
130 final nickname = friend.friendNickname?.trim(); 133 final nickname = friend.friendNickname?.trim();
131 if (nickname?.isNotEmpty == true) return nickname!; 134 if (nickname?.isNotEmpty == true) return nickname!;
132 135
133 - return '未知好友'; 136 + return context.l10n.friendsUnknownFriend;
134 } 137 }
135 } 138 }
136 139
@@ -18,11 +18,13 @@ class HrvReportView extends StatelessWidget { @@ -18,11 +18,13 @@ class HrvReportView extends StatelessWidget {
18 required this.logic, 18 required this.logic,
19 required this.isVip, 19 required this.isVip,
20 required this.onSubscribe, 20 required this.onSubscribe,
  21 + this.loadingIndicator = const ReportLoadingIndicator(),
21 }); 22 });
22 23
23 final HrvReportLogic logic; 24 final HrvReportLogic logic;
24 final bool isVip; 25 final bool isVip;
25 final ValueChanged<String> onSubscribe; 26 final ValueChanged<String> onSubscribe;
  27 + final Widget loadingIndicator;
26 28
27 @override 29 @override
28 Widget build(BuildContext context) { 30 Widget build(BuildContext context) {
@@ -71,7 +73,7 @@ class HrvReportView extends StatelessWidget { @@ -71,7 +73,7 @@ class HrvReportView extends StatelessWidget {
71 child: Obx( 73 child: Obx(
72 () { 74 () {
73 if (logic.isLoading.value) { 75 if (logic.isLoading.value) {
74 - return const ReportLoadingIndicator(); 76 + return loadingIndicator;
75 } 77 }
76 return ListView( 78 return ListView(
77 physics: const ClampingScrollPhysics(), 79 physics: const ClampingScrollPhysics(),
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
1 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
2 3
3 class HrvDistributionBarSegment { 4 class HrvDistributionBarSegment {
@@ -47,7 +48,7 @@ class _HrvDistributionBarPainter extends CustomPainter { @@ -47,7 +48,7 @@ class _HrvDistributionBarPainter extends CustomPainter {
47 final Color backgroundColor; 48 final Color backgroundColor;
48 final double radius; 49 final double radius;
49 static const borderWidth = 1.0; 50 static const borderWidth = 1.0;
50 - static const overlapHeight = 16.0; 51 + static var overlapHeight = 16.h;
51 52
52 @override 53 @override
53 void paint(Canvas canvas, Size size) { 54 void paint(Canvas canvas, Size size) {
@@ -57,7 +58,6 @@ class _HrvDistributionBarPainter extends CustomPainter { @@ -57,7 +58,6 @@ class _HrvDistributionBarPainter extends CustomPainter {
57 ); 58 );
58 canvas.save(); 59 canvas.save();
59 canvas.clipRRect(clip); 60 canvas.clipRRect(clip);
60 -  
61 if (segments.isEmpty) { 61 if (segments.isEmpty) {
62 _drawLayeredSegment( 62 _drawLayeredSegment(
63 canvas, 63 canvas,
@@ -68,7 +68,6 @@ class _HrvDistributionBarPainter extends CustomPainter { @@ -68,7 +68,6 @@ class _HrvDistributionBarPainter extends CustomPainter {
68 canvas.restore(); 68 canvas.restore();
69 return; 69 return;
70 } 70 }
71 -  
72 final visibleHeights = _visibleSegmentHeightsFor(size.height, segments); 71 final visibleHeights = _visibleSegmentHeightsFor(size.height, segments);
73 var visibleTop = 0.0; 72 var visibleTop = 0.0;
74 for (var index = 0; index < segments.length; index++) { 73 for (var index = 0; index < segments.length; index++) {
@@ -467,32 +467,26 @@ class _TrendMetric extends StatelessWidget { @@ -467,32 +467,26 @@ class _TrendMetric extends StatelessWidget {
467 } 467 }
468 468
469 String _weekComparisonText(BuildContext context, int difference) { 469 String _weekComparisonText(BuildContext context, int difference) {
470 - final isZh = Localizations.localeOf(context).languageCode == 'zh';  
471 return difference == 0 470 return difference == 0
472 - ? (isZh ? '与上周一致' : 'Same as last week') 471 + ? context.l10n.hrvSameAsLastWeek
473 : difference > 0 472 : difference > 0
474 ? context.l10n.hrvMoreDaysThanLastWeek(difference) 473 ? context.l10n.hrvMoreDaysThanLastWeek(difference)
475 : context.l10n.hrvFewerDaysThanLastWeek(difference.abs()); 474 : context.l10n.hrvFewerDaysThanLastWeek(difference.abs());
476 } 475 }
477 476
478 String _monthComparisonText(BuildContext context, int difference) { 477 String _monthComparisonText(BuildContext context, int difference) {
479 - final isZh = Localizations.localeOf(context).languageCode == 'zh';  
480 if (difference == 0) { 478 if (difference == 0) {
481 - return isZh ? '与上月一致' : 'Same as last month'; 479 + return context.l10n.hrvSameAsLastMonth;
482 } 480 }
483 if (difference > 0) { 481 if (difference > 0) {
484 - return isZh  
485 - ? '比上月多$difference天'  
486 - : '$difference more days than last month'; 482 + return context.l10n.hrvMoreDaysThanLastMonth(difference);
487 } 483 }
488 - final count = difference.abs();  
489 - return isZh ? '比上月少$count天' : '$count fewer days than last month'; 484 + return context.l10n.hrvFewerDaysThanLastMonth(difference.abs());
490 } 485 }
491 486
492 String _unavailableComparisonText(BuildContext context) { 487 String _unavailableComparisonText(BuildContext context) {
493 if (!isMonth) return context.l10n.hrvComparedLastWeekUnavailable; 488 if (!isMonth) return context.l10n.hrvComparedLastWeekUnavailable;
494 - final isZh = Localizations.localeOf(context).languageCode == 'zh';  
495 - return isZh ? '比上月少-天' : 'Compared with last month: -'; 489 + return context.l10n.hrvComparedLastMonthUnavailable;
496 } 490 }
497 491
498 Color _comparisonColor(int? difference) { 492 Color _comparisonColor(int? difference) {
@@ -518,13 +512,19 @@ class _StressAxisLabels extends StatelessWidget { @@ -518,13 +512,19 @@ class _StressAxisLabels extends StatelessWidget {
518 512
519 @override 513 @override
520 Widget build(BuildContext context) { 514 Widget build(BuildContext context) {
521 - return const SizedBox( 515 + return SizedBox(
522 width: 14, 516 width: 14,
523 child: Column( 517 child: Column(
524 mainAxisAlignment: MainAxisAlignment.spaceBetween, 518 mainAxisAlignment: MainAxisAlignment.spaceBetween,
525 children: [ 519 children: [
526 - RotatedBox(quarterTurns: 1, child: _StressAxisText('轻松')),  
527 - RotatedBox(quarterTurns: 1, child: _StressAxisText('压力大')), 520 + RotatedBox(
  521 + quarterTurns: 1,
  522 + child: _StressAxisText(context.l10n.hrvRelaxedAxisLabel),
  523 + ),
  524 + RotatedBox(
  525 + quarterTurns: 1,
  526 + child: _StressAxisText(context.l10n.hrvStressedAxisLabel),
  527 + ),
528 ], 528 ],
529 ), 529 ),
530 ); 530 );
@@ -644,91 +644,91 @@ class _DistributionCard extends StatelessWidget { @@ -644,91 +644,91 @@ class _DistributionCard extends StatelessWidget {
644 final HrvPeriodReport report; 644 final HrvPeriodReport report;
645 final bool showExample; 645 final bool showExample;
646 final VoidCallback? onTap; 646 final VoidCallback? onTap;
647 - static const _cardHeight = 425.0;  
648 - static const _barRight = 45.0; 647 + static const _barRight = 25.0;
649 static const _barWidth = 42.0; 648 static const _barWidth = 42.0;
650 - static const _barHeight = 220.0;  
651 649
652 @override 650 @override
653 Widget build(BuildContext context) { 651 Widget build(BuildContext context) {
654 return _Card( 652 return _Card(
655 onTap: onTap, 653 onTap: onTap,
656 padding: EdgeInsets.zero, 654 padding: EdgeInsets.zero,
657 - child: ConstrainedBox(  
658 - constraints: const BoxConstraints(minHeight: _cardHeight),  
659 - child: Stack( 655 + child: Padding(
  656 + padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
  657 + child: Column(
  658 + crossAxisAlignment: CrossAxisAlignment.start,
660 children: [ 659 children: [
661 - Padding(  
662 - padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),  
663 - child: Column(  
664 - crossAxisAlignment: CrossAxisAlignment.start,  
665 - children: [  
666 - _ReportTitle(  
667 - title: HealthReportSubjectScope.titleOf(  
668 - context,  
669 - context.l10n.hrvStressDistribution,  
670 - ),  
671 - showExample: showExample,  
672 - fontSize: 16,  
673 - ),  
674 - const SizedBox(height: 20),  
675 - Text(  
676 - context.l10n.hrvValidDays,  
677 - style: const TextStyle(  
678 - color: Color(0xFF78787D),  
679 - fontSize: 12,  
680 - ),  
681 - ),  
682 - const SizedBox(height: 2),  
683 - _ValueWithUnit(  
684 - value: '${report.validDays}',  
685 - unit: context.l10n.reportUnitDay,  
686 - ),  
687 - const SizedBox(height: 28),  
688 - SizedBox(  
689 - width: 216,  
690 - child: _DistributionGrid(report: report), 660 + _topView(context),
  661 + const Divider(height: 1, color: Color(0xFFF3F3F3)),
  662 + const SizedBox(height: 20),
  663 + Row(
  664 + children: [
  665 + Expanded(
  666 + child: _ExtremeHrv(
  667 + title: context.l10n.hrvLowest,
  668 + day: report.minDay,
691 ), 669 ),
692 - const SizedBox(height: 20),  
693 - const Divider(height: 1, color: Color(0xFFF3F3F3)),  
694 - const SizedBox(height: 20),  
695 - Row(  
696 - children: [  
697 - Expanded(  
698 - child: _ExtremeHrv(  
699 - title: context.l10n.hrvLowest,  
700 - day: report.minDay,  
701 - ),  
702 - ),  
703 - Expanded(  
704 - child: _ExtremeHrv(  
705 - title: context.l10n.hrvHighest,  
706 - day: report.maxDay,  
707 - ),  
708 - ),  
709 - ], 670 + ),
  671 + Expanded(
  672 + child: _ExtremeHrv(
  673 + title: context.l10n.hrvHighest,
  674 + day: report.maxDay,
710 ), 675 ),
711 - ], 676 + ),
  677 + ],
  678 + ),
  679 + ],
  680 + ),
  681 + ),
  682 + );
  683 + }
  684 +
  685 + Widget _topView(BuildContext context) {
  686 + return Stack(
  687 + children: [
  688 + Positioned(
  689 + child: Column(
  690 + crossAxisAlignment: CrossAxisAlignment.start,
  691 + children: [
  692 + _ReportTitle(
  693 + title: HealthReportSubjectScope.titleOf(
  694 + context,
  695 + context.l10n.hrvStressDistribution,
712 ), 696 ),
  697 + showExample: showExample,
  698 + fontSize: 16,
713 ), 699 ),
714 - Positioned(  
715 - top: 0,  
716 - right: _barRight,  
717 - width: _barWidth,  
718 - child: Container(  
719 - alignment: Alignment.topCenter,  
720 - padding: EdgeInsets.only(top: 50.h),  
721 - child: SizedBox(  
722 - height: _barHeight,  
723 - child: IgnorePointer(  
724 - child: _DistributionBar(report: report),  
725 - ),  
726 - ), 700 + const SizedBox(height: 20),
  701 + Text(
  702 + context.l10n.hrvValidDays,
  703 + style: const TextStyle(
  704 + color: Color(0xFF78787D),
  705 + fontSize: 12,
727 ), 706 ),
728 ), 707 ),
  708 + const SizedBox(height: 2),
  709 + _ValueWithUnit(
  710 + value: '${report.validDays}',
  711 + unit: context.l10n.reportUnitDay,
  712 + ),
  713 + const SizedBox(height: 28),
  714 + SizedBox(
  715 + width: 216,
  716 + child: _DistributionGrid(report: report),
  717 + ),
  718 + const SizedBox(height: 20),
729 ], 719 ],
  720 + )),
  721 + Positioned(
  722 + top: 0,
  723 + width: _barWidth,
  724 + right: _barRight,
  725 + bottom: 25.h,
  726 + child: Container(
  727 + alignment: Alignment.topCenter,
  728 + padding: EdgeInsets.only(top: 25.h),
  729 + child: IgnorePointer(child: _DistributionBar(report: report))),
730 ), 730 ),
731 - ), 731 + ],
732 ); 732 );
733 } 733 }
734 } 734 }
@@ -859,7 +859,9 @@ class _ExtremeHrv extends StatelessWidget { @@ -859,7 +859,9 @@ class _ExtremeHrv extends StatelessWidget {
859 unit: 'ms'), 859 unit: 'ms'),
860 const SizedBox(height: 2), 860 const SizedBox(height: 2),
861 Text( 861 Text(
862 - day == null ? '-月-日' : reportMonthDay(day!.date), 862 + day == null
  863 + ? context.l10n.hrvEmptyMonthDayPlaceholder
  864 + : reportMonthDay(day!.date),
863 style: const TextStyle(color: Color(0xFF78787D), fontSize: 12), 865 style: const TextStyle(color: Color(0xFF78787D), fontSize: 12),
864 ), 866 ),
865 ], 867 ],
@@ -22,11 +22,13 @@ class SleepReportView extends StatelessWidget { @@ -22,11 +22,13 @@ class SleepReportView extends StatelessWidget {
22 required this.logic, 22 required this.logic,
23 required this.isVip, 23 required this.isVip,
24 required this.onSubscribe, 24 required this.onSubscribe,
  25 + this.loadingIndicator = const ReportLoadingIndicator(),
25 }); 26 });
26 27
27 final SleepReportLogic logic; 28 final SleepReportLogic logic;
28 final bool isVip; 29 final bool isVip;
29 final ValueChanged<String> onSubscribe; 30 final ValueChanged<String> onSubscribe;
  31 + final Widget loadingIndicator;
30 32
31 @override 33 @override
32 Widget build(BuildContext context) { 34 Widget build(BuildContext context) {
@@ -60,7 +62,7 @@ class SleepReportView extends StatelessWidget { @@ -60,7 +62,7 @@ class SleepReportView extends StatelessWidget {
60 child: Obx( 62 child: Obx(
61 () { 63 () {
62 if (logic.isLoading.value) { 64 if (logic.isLoading.value) {
63 - return const ReportLoadingIndicator(); 65 + return loadingIndicator;
64 } 66 }
65 return CustomScrollView( 67 return CustomScrollView(
66 physics: const ClampingScrollPhysics(), 68 physics: const ClampingScrollPhysics(),
@@ -377,7 +377,7 @@ class _TopMetric extends StatelessWidget { @@ -377,7 +377,7 @@ class _TopMetric extends StatelessWidget {
377 Text( 377 Text(
378 trendText, 378 trendText,
379 style: const TextStyle( 379 style: const TextStyle(
380 - color: SleepWeekReportView.h3, 380 + color: SleepWeekReportView.h2,
381 fontSize: 12, 381 fontSize: 12,
382 fontWeight: FontWeight.w400, 382 fontWeight: FontWeight.w400,
383 height: 1.2, 383 height: 1.2,
@@ -1437,6 +1437,7 @@ class _ExtremeMetric extends StatelessWidget { @@ -1437,6 +1437,7 @@ class _ExtremeMetric extends StatelessWidget {
1437 required this.unit, 1437 required this.unit,
1438 required this.date, 1438 required this.date,
1439 this.duration, 1439 this.duration,
  1440 + this.isDurationMetric = false,
1440 }); 1441 });
1441 1442
1442 factory _ExtremeMetric.duration({ 1443 factory _ExtremeMetric.duration({
@@ -1450,10 +1451,11 @@ class _ExtremeMetric extends StatelessWidget { @@ -1450,10 +1451,11 @@ class _ExtremeMetric extends StatelessWidget {
1450 color: color, 1451 color: color,
1451 value: duration == null ? '-' : '${duration.hoursPart}', 1452 value: duration == null ? '-' : '${duration.hoursPart}',
1452 unit: duration == null 1453 unit: duration == null
1453 - ? '${l10n.reportUnitHour} -${l10n.reportUnitMinute}' 1454 + ? ''
1454 : '${l10n.reportUnitHour}${duration.minutesPart}${l10n.reportUnitMinute}', 1455 : '${l10n.reportUnitHour}${duration.minutesPart}${l10n.reportUnitMinute}',
1455 date: report?.date, 1456 date: report?.date,
1456 duration: duration, 1457 duration: duration,
  1458 + isDurationMetric: true,
1457 ); 1459 );
1458 } 1460 }
1459 1461
@@ -1496,6 +1498,7 @@ class _ExtremeMetric extends StatelessWidget { @@ -1496,6 +1498,7 @@ class _ExtremeMetric extends StatelessWidget {
1496 final String unit; 1498 final String unit;
1497 final DateTime? date; 1499 final DateTime? date;
1498 final SleepDuration? duration; 1500 final SleepDuration? duration;
  1501 + final bool isDurationMetric;
1499 1502
1500 @override 1503 @override
1501 Widget build(BuildContext context) { 1504 Widget build(BuildContext context) {
@@ -1526,6 +1529,8 @@ class _ExtremeMetric extends StatelessWidget { @@ -1526,6 +1529,8 @@ class _ExtremeMetric extends StatelessWidget {
1526 const SizedBox(height: 3), 1529 const SizedBox(height: 3),
1527 if (duration != null) 1530 if (duration != null)
1528 _ExtremeDurationValue(duration: duration!) 1531 _ExtremeDurationValue(duration: duration!)
  1532 + else if (isDurationMetric)
  1533 + const _ExtremeDurationPlaceholderValue()
1529 else 1534 else
1530 Row( 1535 Row(
1531 crossAxisAlignment: CrossAxisAlignment.end, 1536 crossAxisAlignment: CrossAxisAlignment.end,
@@ -1559,7 +1564,9 @@ class _ExtremeMetric extends StatelessWidget { @@ -1559,7 +1564,9 @@ class _ExtremeMetric extends StatelessWidget {
1559 ), 1564 ),
1560 const SizedBox(height: 3), 1565 const SizedBox(height: 3),
1561 Text( 1566 Text(
1562 - date == null ? '-月-日 周-' : _dateText(context, date!), 1567 + date == null
  1568 + ? context.l10n.sleepEmptyDateWithWeekday
  1569 + : _dateText(context, date!),
1563 style: const TextStyle( 1570 style: const TextStyle(
1564 color: SleepWeekReportView.h2, 1571 color: SleepWeekReportView.h2,
1565 fontSize: 12, 1572 fontSize: 12,
@@ -1579,6 +1586,22 @@ class _ExtremeMetric extends StatelessWidget { @@ -1579,6 +1586,22 @@ class _ExtremeMetric extends StatelessWidget {
1579 } 1586 }
1580 } 1587 }
1581 1588
  1589 +class _ExtremeDurationPlaceholderValue extends StatelessWidget {
  1590 + const _ExtremeDurationPlaceholderValue();
  1591 +
  1592 + @override
  1593 + Widget build(BuildContext context) {
  1594 + return Row(
  1595 + crossAxisAlignment: CrossAxisAlignment.end,
  1596 + children: [
  1597 + _ExtremeDurationPart('-', context.l10n.reportUnitHour),
  1598 + const SizedBox(width: 2),
  1599 + _ExtremeDurationPart('-', context.l10n.reportUnitMinute),
  1600 + ],
  1601 + );
  1602 + }
  1603 +}
  1604 +
1582 class _ExtremeDurationValue extends StatelessWidget { 1605 class _ExtremeDurationValue extends StatelessWidget {
1583 const _ExtremeDurationValue({required this.duration}); 1606 const _ExtremeDurationValue({required this.duration});
1584 1607
@@ -1589,14 +1612,28 @@ class _ExtremeDurationValue extends StatelessWidget { @@ -1589,14 +1612,28 @@ class _ExtremeDurationValue extends StatelessWidget {
1589 return Row( 1612 return Row(
1590 crossAxisAlignment: CrossAxisAlignment.end, 1613 crossAxisAlignment: CrossAxisAlignment.end,
1591 children: [ 1614 children: [
1592 - _part('${duration.hoursPart}', context.l10n.reportUnitHour), 1615 + _ExtremeDurationPart(
  1616 + '${duration.hoursPart}',
  1617 + context.l10n.reportUnitHour,
  1618 + ),
1593 const SizedBox(width: 2), 1619 const SizedBox(width: 2),
1594 - _part('${duration.minutesPart}', context.l10n.reportUnitMinute), 1620 + _ExtremeDurationPart(
  1621 + '${duration.minutesPart}',
  1622 + context.l10n.reportUnitMinute,
  1623 + ),
1595 ], 1624 ],
1596 ); 1625 );
1597 } 1626 }
  1627 +}
  1628 +
  1629 +class _ExtremeDurationPart extends StatelessWidget {
  1630 + const _ExtremeDurationPart(this.value, this.unit);
1598 1631
1599 - Widget _part(String value, String unit) { 1632 + final String value;
  1633 + final String unit;
  1634 +
  1635 + @override
  1636 + Widget build(BuildContext context) {
1600 return Row( 1637 return Row(
1601 crossAxisAlignment: CrossAxisAlignment.end, 1638 crossAxisAlignment: CrossAxisAlignment.end,
1602 children: [ 1639 children: [
@@ -8,7 +8,6 @@ import 'package:doublefeel_flutter/core/result/app_result.dart'; @@ -8,7 +8,6 @@ import 'package:doublefeel_flutter/core/result/app_result.dart';
8 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 8 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
9 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 9 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
10 import 'package:get/get.dart'; 10 import 'package:get/get.dart';
11 -import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';  
12 11
13 import '../models/watch_theme_models.dart'; 12 import '../models/watch_theme_models.dart';
14 13
@@ -95,7 +94,7 @@ class WatchThemeController extends GetxController { @@ -95,7 +94,7 @@ class WatchThemeController extends GetxController {
95 94
96 Future<void> _toPremiumPage() async { 95 Future<void> _toPremiumPage() async {
97 await Get.toNamed(Routes.PURCHASE, arguments: { 96 await Get.toNamed(Routes.PURCHASE, arguments: {
98 - IntentKeys.channelType: l10n.watchThemePurchaseChannel, 97 + IntentKeys.channelType: "表盘主题",
99 }); 98 });
100 await _refreshVip(); 99 await _refreshVip();
101 } 100 }
@@ -106,6 +106,8 @@ class FriendItem { @@ -106,6 +106,8 @@ class FriendItem {
106 class FriendHealthData { 106 class FriendHealthData {
107 const FriendHealthData({ 107 const FriendHealthData({
108 this.hrvState, 108 this.hrvState,
  109 + this.comprehensiveStressScore,
  110 + this.comprehensiveStressState,
109 this.latestHrv, 111 this.latestHrv,
110 this.realtimeStress, 112 this.realtimeStress,
111 this.sleepEvaluate, 113 this.sleepEvaluate,
@@ -116,6 +118,12 @@ class FriendHealthData { @@ -116,6 +118,12 @@ class FriendHealthData {
116 /// HRV state indicator 118 /// HRV state indicator
117 final int? hrvState; 119 final int? hrvState;
118 120
  121 + /// Comprehensive stress score.
  122 + final int? comprehensiveStressScore;
  123 +
  124 + /// Comprehensive stress state indicator.
  125 + final int? comprehensiveStressState;
  126 +
119 /// Latest HRV value used by the friend health card. 127 /// Latest HRV value used by the friend health card.
120 final double? latestHrv; 128 final double? latestHrv;
121 129
@@ -134,6 +142,8 @@ class FriendHealthData { @@ -134,6 +142,8 @@ class FriendHealthData {
134 factory FriendHealthData.fromJson(Map<String, dynamic> json) { 142 factory FriendHealthData.fromJson(Map<String, dynamic> json) {
135 return FriendHealthData( 143 return FriendHealthData(
136 hrvState: _parseInt(json['hrv_state']), 144 hrvState: _parseInt(json['hrv_state']),
  145 + comprehensiveStressScore: _parseInt(json['comprehensive_stress_score']),
  146 + comprehensiveStressState: _parseInt(json['comprehensive_stress_state']),
137 latestHrv: _parseDouble(json['latest_hrv']), 147 latestHrv: _parseDouble(json['latest_hrv']),
138 realtimeStress: json['realtime_stress'] as Map<String, dynamic>?, 148 realtimeStress: json['realtime_stress'] as Map<String, dynamic>?,
139 sleepEvaluate: _parseInt(json['sleep_evaluate']), 149 sleepEvaluate: _parseInt(json['sleep_evaluate']),
@@ -145,6 +155,12 @@ class FriendHealthData { @@ -145,6 +155,12 @@ class FriendHealthData {
145 Map<String, dynamic> toJson() { 155 Map<String, dynamic> toJson() {
146 final val = <String, dynamic>{}; 156 final val = <String, dynamic>{};
147 if (hrvState != null) val['hrv_state'] = hrvState; 157 if (hrvState != null) val['hrv_state'] = hrvState;
  158 + if (comprehensiveStressScore != null) {
  159 + val['comprehensive_stress_score'] = comprehensiveStressScore;
  160 + }
  161 + if (comprehensiveStressState != null) {
  162 + val['comprehensive_stress_state'] = comprehensiveStressState;
  163 + }
148 if (latestHrv != null) val['latest_hrv'] = latestHrv; 164 if (latestHrv != null) val['latest_hrv'] = latestHrv;
149 if (realtimeStress != null) val['realtime_stress'] = realtimeStress; 165 if (realtimeStress != null) val['realtime_stress'] = realtimeStress;
150 if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate; 166 if (sleepEvaluate != null) val['sleep_evaluate'] = sleepEvaluate;
@@ -345,7 +345,15 @@ @@ -345,7 +345,15 @@
345 "hrvSameAsLastWeek": "Same as last week", 345 "hrvSameAsLastWeek": "Same as last week",
346 "hrvMoreDaysThanLastWeek": "{count} more days than last week", 346 "hrvMoreDaysThanLastWeek": "{count} more days than last week",
347 "hrvFewerDaysThanLastWeek": "{count} fewer days than last week", 347 "hrvFewerDaysThanLastWeek": "{count} fewer days than last week",
  348 + "hrvComparedLastMonthUnavailable": "Compared with last month: -",
  349 + "hrvSameAsLastMonth": "Same as last month",
  350 + "hrvMoreDaysThanLastMonth": "{count} more days than last month",
  351 + "hrvFewerDaysThanLastMonth": "{count} fewer days than last month",
348 "hrvUnlockNow": "Unlock Now", 352 "hrvUnlockNow": "Unlock Now",
  353 + "hrvTrendTitle": "HRV Trend",
  354 + "hrvPeriodAverage": "This {period} average",
  355 + "hrvComparedPreviousPeriod": "vs previous {period}",
  356 + "hrvPeriodTrendChart": "HRV {period} Trend Chart",
349 "activityTotalBurn": "Total Activity Burn", 357 "activityTotalBurn": "Total Activity Burn",
350 "activityExerciseTotalDuration": "Total Exercise Time", 358 "activityExerciseTotalDuration": "Total Exercise Time",
351 "activityStandTotalDuration": "Total Stand Time", 359 "activityStandTotalDuration": "Total Stand Time",
@@ -359,9 +367,16 @@ @@ -359,9 +367,16 @@
359 "activityComparedLastMonth": "34% less than last month", 367 "activityComparedLastMonth": "34% less than last month",
360 "activityComparedUnavailable": "Compared with last week: -", 368 "activityComparedUnavailable": "Compared with last week: -",
361 "activityComparedLastMonthUnavailable": "Compared with last month: -", 369 "activityComparedLastMonthUnavailable": "Compared with last month: -",
  370 + "activitySameAsLastWeek": "Same as last week",
  371 + "activityMoreThanLastWeek": "{percent}% more than last week",
  372 + "activityLessThanLastWeek": "{percent}% less than last week",
362 "activitySameAsLastMonth": "Same as last month", 373 "activitySameAsLastMonth": "Same as last month",
363 "activityMoreThanLastMonth": "{percent}% more than last month", 374 "activityMoreThanLastMonth": "{percent}% more than last month",
364 "activityLessThanLastMonth": "{percent}% less than last month", 375 "activityLessThanLastMonth": "{percent}% less than last month",
  376 + "activityTrendTitle": "Activity Burn Trend",
  377 + "activityPeriodTotalBurn": "This {period} total burn",
  378 + "activityDailyAverageBurn": "Daily average burn",
  379 + "activityPeriodTrendChart": "Activity Burn {period} Trend Chart",
365 "activityMove": "Move", 380 "activityMove": "Move",
366 "activityExercise": "Exercise", 381 "activityExercise": "Exercise",
367 "activityStand": "Stand", 382 "activityStand": "Stand",
@@ -389,6 +404,11 @@ @@ -389,6 +404,11 @@
389 "sleepTarget": "Target", 404 "sleepTarget": "Target",
390 "sleepHighest": "Highest", 405 "sleepHighest": "Highest",
391 "sleepLowest": "Lowest", 406 "sleepLowest": "Lowest",
  407 + "sleepTrendTitle": "Sleep Report Trend",
  408 + "sleepPeriodAverageDuration": "This {period} average sleep",
  409 + "sleepDeepSleepRatio": "Deep sleep ratio",
  410 + "sleepDurationPeriodTrendChart": "Sleep Duration {period} Trend Chart",
  411 + "sleepEmptyDateWithWeekday": "-",
392 "sleepQualityDescription": "DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.", 412 "sleepQualityDescription": "DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.",
393 "sleepQualityAttentionRange": "<60 pts", 413 "sleepQualityAttentionRange": "<60 pts",
394 "sleepQualityNormalRange": "60–85 pts", 414 "sleepQualityNormalRange": "60–85 pts",
@@ -531,7 +531,50 @@ @@ -531,7 +531,50 @@
531 } 531 }
532 } 532 }
533 }, 533 },
  534 + "hrvComparedLastMonthUnavailable": "比上月少-天",
  535 + "hrvSameAsLastMonth": "与上月一致",
  536 + "hrvMoreDaysThanLastMonth": "比上月多{count}天",
  537 + "@hrvMoreDaysThanLastMonth": {
  538 + "placeholders": {
  539 + "count": {
  540 + "type": "int"
  541 + }
  542 + }
  543 + },
  544 + "hrvFewerDaysThanLastMonth": "比上月少{count}天",
  545 + "@hrvFewerDaysThanLastMonth": {
  546 + "placeholders": {
  547 + "count": {
  548 + "type": "int"
  549 + }
  550 + }
  551 + },
534 "hrvUnlockNow": "立即解锁", 552 "hrvUnlockNow": "立即解锁",
  553 + "hrvTrendTitle": "HRV趋势",
  554 + "hrvPeriodAverage": "本{period}平均",
  555 + "@hrvPeriodAverage": {
  556 + "placeholders": {
  557 + "period": {
  558 + "type": "String"
  559 + }
  560 + }
  561 + },
  562 + "hrvComparedPreviousPeriod": "较上{period}",
  563 + "@hrvComparedPreviousPeriod": {
  564 + "placeholders": {
  565 + "period": {
  566 + "type": "String"
  567 + }
  568 + }
  569 + },
  570 + "hrvPeriodTrendChart": "HRV {period}趋势图",
  571 + "@hrvPeriodTrendChart": {
  572 + "placeholders": {
  573 + "period": {
  574 + "type": "String"
  575 + }
  576 + }
  577 + },
535 "activityTotalBurn": "活动总消耗", 578 "activityTotalBurn": "活动总消耗",
536 "activityExerciseTotalDuration": "锻炼总时长", 579 "activityExerciseTotalDuration": "锻炼总时长",
537 "activityStandTotalDuration": "站立总时长", 580 "activityStandTotalDuration": "站立总时长",
@@ -545,6 +588,23 @@ @@ -545,6 +588,23 @@
545 "activityComparedLastMonth": "比上月少34%", 588 "activityComparedLastMonth": "比上月少34%",
546 "activityComparedUnavailable": "比上周-", 589 "activityComparedUnavailable": "比上周-",
547 "activityComparedLastMonthUnavailable": "比上月-", 590 "activityComparedLastMonthUnavailable": "比上月-",
  591 + "activitySameAsLastWeek": "与上周持平",
  592 + "activityMoreThanLastWeek": "比上周多{percent}%",
  593 + "@activityMoreThanLastWeek": {
  594 + "placeholders": {
  595 + "percent": {
  596 + "type": "int"
  597 + }
  598 + }
  599 + },
  600 + "activityLessThanLastWeek": "比上周少{percent}%",
  601 + "@activityLessThanLastWeek": {
  602 + "placeholders": {
  603 + "percent": {
  604 + "type": "int"
  605 + }
  606 + }
  607 + },
548 "activitySameAsLastMonth": "与上月持平", 608 "activitySameAsLastMonth": "与上月持平",
549 "activityMoreThanLastMonth": "比上月多{percent}%", 609 "activityMoreThanLastMonth": "比上月多{percent}%",
550 "@activityMoreThanLastMonth": { 610 "@activityMoreThanLastMonth": {
@@ -562,6 +622,24 @@ @@ -562,6 +622,24 @@
562 } 622 }
563 } 623 }
564 }, 624 },
  625 + "activityTrendTitle": "活动消耗趋势",
  626 + "activityPeriodTotalBurn": "本{period}总消耗",
  627 + "@activityPeriodTotalBurn": {
  628 + "placeholders": {
  629 + "period": {
  630 + "type": "String"
  631 + }
  632 + }
  633 + },
  634 + "activityDailyAverageBurn": "日均消耗",
  635 + "activityPeriodTrendChart": "活动消耗{period}趋势图",
  636 + "@activityPeriodTrendChart": {
  637 + "placeholders": {
  638 + "period": {
  639 + "type": "String"
  640 + }
  641 + }
  642 + },
565 "activityMove": "活动", 643 "activityMove": "活动",
566 "activityExercise": "锻炼", 644 "activityExercise": "锻炼",
567 "activityStand": "站立", 645 "activityStand": "站立",
@@ -626,6 +704,25 @@ @@ -626,6 +704,25 @@
626 "sleepTarget": "目标", 704 "sleepTarget": "目标",
627 "sleepHighest": "最高", 705 "sleepHighest": "最高",
628 "sleepLowest": "最低", 706 "sleepLowest": "最低",
  707 + "sleepTrendTitle": "睡眠报告趋势",
  708 + "sleepPeriodAverageDuration": "本{period}均睡眠",
  709 + "@sleepPeriodAverageDuration": {
  710 + "placeholders": {
  711 + "period": {
  712 + "type": "String"
  713 + }
  714 + }
  715 + },
  716 + "sleepDeepSleepRatio": "深睡占比",
  717 + "sleepDurationPeriodTrendChart": "睡眠时长{period}趋势图",
  718 + "@sleepDurationPeriodTrendChart": {
  719 + "placeholders": {
  720 + "period": {
  721 + "type": "String"
  722 + }
  723 + }
  724 + },
  725 + "sleepEmptyDateWithWeekday": "-月-日 周-",
629 "sleepQualityDescription": "DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。", 726 "sleepQualityDescription": "DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。",
630 "sleepQualityAttentionRange": "<60分", 727 "sleepQualityAttentionRange": "<60分",
631 "sleepQualityNormalRange": "60~85分", 728 "sleepQualityNormalRange": "60~85分",
@@ -2169,12 +2169,60 @@ abstract class AppLocalizations { @@ -2169,12 +2169,60 @@ abstract class AppLocalizations {
2169 /// **'比上周少{count}天'** 2169 /// **'比上周少{count}天'**
2170 String hrvFewerDaysThanLastWeek(int count); 2170 String hrvFewerDaysThanLastWeek(int count);
2171 2171
  2172 + /// No description provided for @hrvComparedLastMonthUnavailable.
  2173 + ///
  2174 + /// In zh, this message translates to:
  2175 + /// **'比上月少-天'**
  2176 + String get hrvComparedLastMonthUnavailable;
  2177 +
  2178 + /// No description provided for @hrvSameAsLastMonth.
  2179 + ///
  2180 + /// In zh, this message translates to:
  2181 + /// **'与上月一致'**
  2182 + String get hrvSameAsLastMonth;
  2183 +
  2184 + /// No description provided for @hrvMoreDaysThanLastMonth.
  2185 + ///
  2186 + /// In zh, this message translates to:
  2187 + /// **'比上月多{count}天'**
  2188 + String hrvMoreDaysThanLastMonth(int count);
  2189 +
  2190 + /// No description provided for @hrvFewerDaysThanLastMonth.
  2191 + ///
  2192 + /// In zh, this message translates to:
  2193 + /// **'比上月少{count}天'**
  2194 + String hrvFewerDaysThanLastMonth(int count);
  2195 +
2172 /// No description provided for @hrvUnlockNow. 2196 /// No description provided for @hrvUnlockNow.
2173 /// 2197 ///
2174 /// In zh, this message translates to: 2198 /// In zh, this message translates to:
2175 /// **'立即解锁'** 2199 /// **'立即解锁'**
2176 String get hrvUnlockNow; 2200 String get hrvUnlockNow;
2177 2201
  2202 + /// No description provided for @hrvTrendTitle.
  2203 + ///
  2204 + /// In zh, this message translates to:
  2205 + /// **'HRV趋势'**
  2206 + String get hrvTrendTitle;
  2207 +
  2208 + /// No description provided for @hrvPeriodAverage.
  2209 + ///
  2210 + /// In zh, this message translates to:
  2211 + /// **'本{period}平均'**
  2212 + String hrvPeriodAverage(String period);
  2213 +
  2214 + /// No description provided for @hrvComparedPreviousPeriod.
  2215 + ///
  2216 + /// In zh, this message translates to:
  2217 + /// **'较上{period}'**
  2218 + String hrvComparedPreviousPeriod(String period);
  2219 +
  2220 + /// No description provided for @hrvPeriodTrendChart.
  2221 + ///
  2222 + /// In zh, this message translates to:
  2223 + /// **'HRV {period}趋势图'**
  2224 + String hrvPeriodTrendChart(String period);
  2225 +
2178 /// No description provided for @activityTotalBurn. 2226 /// No description provided for @activityTotalBurn.
2179 /// 2227 ///
2180 /// In zh, this message translates to: 2228 /// In zh, this message translates to:
@@ -2253,6 +2301,24 @@ abstract class AppLocalizations { @@ -2253,6 +2301,24 @@ abstract class AppLocalizations {
2253 /// **'比上月-'** 2301 /// **'比上月-'**
2254 String get activityComparedLastMonthUnavailable; 2302 String get activityComparedLastMonthUnavailable;
2255 2303
  2304 + /// No description provided for @activitySameAsLastWeek.
  2305 + ///
  2306 + /// In zh, this message translates to:
  2307 + /// **'与上周持平'**
  2308 + String get activitySameAsLastWeek;
  2309 +
  2310 + /// No description provided for @activityMoreThanLastWeek.
  2311 + ///
  2312 + /// In zh, this message translates to:
  2313 + /// **'比上周多{percent}%'**
  2314 + String activityMoreThanLastWeek(int percent);
  2315 +
  2316 + /// No description provided for @activityLessThanLastWeek.
  2317 + ///
  2318 + /// In zh, this message translates to:
  2319 + /// **'比上周少{percent}%'**
  2320 + String activityLessThanLastWeek(int percent);
  2321 +
2256 /// No description provided for @activitySameAsLastMonth. 2322 /// No description provided for @activitySameAsLastMonth.
2257 /// 2323 ///
2258 /// In zh, this message translates to: 2324 /// In zh, this message translates to:
@@ -2271,6 +2337,30 @@ abstract class AppLocalizations { @@ -2271,6 +2337,30 @@ abstract class AppLocalizations {
2271 /// **'比上月少{percent}%'** 2337 /// **'比上月少{percent}%'**
2272 String activityLessThanLastMonth(int percent); 2338 String activityLessThanLastMonth(int percent);
2273 2339
  2340 + /// No description provided for @activityTrendTitle.
  2341 + ///
  2342 + /// In zh, this message translates to:
  2343 + /// **'活动消耗趋势'**
  2344 + String get activityTrendTitle;
  2345 +
  2346 + /// No description provided for @activityPeriodTotalBurn.
  2347 + ///
  2348 + /// In zh, this message translates to:
  2349 + /// **'本{period}总消耗'**
  2350 + String activityPeriodTotalBurn(String period);
  2351 +
  2352 + /// No description provided for @activityDailyAverageBurn.
  2353 + ///
  2354 + /// In zh, this message translates to:
  2355 + /// **'日均消耗'**
  2356 + String get activityDailyAverageBurn;
  2357 +
  2358 + /// No description provided for @activityPeriodTrendChart.
  2359 + ///
  2360 + /// In zh, this message translates to:
  2361 + /// **'活动消耗{period}趋势图'**
  2362 + String activityPeriodTrendChart(String period);
  2363 +
2274 /// No description provided for @activityMove. 2364 /// No description provided for @activityMove.
2275 /// 2365 ///
2276 /// In zh, this message translates to: 2366 /// In zh, this message translates to:
@@ -2433,6 +2523,36 @@ abstract class AppLocalizations { @@ -2433,6 +2523,36 @@ abstract class AppLocalizations {
2433 /// **'最低'** 2523 /// **'最低'**
2434 String get sleepLowest; 2524 String get sleepLowest;
2435 2525
  2526 + /// No description provided for @sleepTrendTitle.
  2527 + ///
  2528 + /// In zh, this message translates to:
  2529 + /// **'睡眠报告趋势'**
  2530 + String get sleepTrendTitle;
  2531 +
  2532 + /// No description provided for @sleepPeriodAverageDuration.
  2533 + ///
  2534 + /// In zh, this message translates to:
  2535 + /// **'本{period}均睡眠'**
  2536 + String sleepPeriodAverageDuration(String period);
  2537 +
  2538 + /// No description provided for @sleepDeepSleepRatio.
  2539 + ///
  2540 + /// In zh, this message translates to:
  2541 + /// **'深睡占比'**
  2542 + String get sleepDeepSleepRatio;
  2543 +
  2544 + /// No description provided for @sleepDurationPeriodTrendChart.
  2545 + ///
  2546 + /// In zh, this message translates to:
  2547 + /// **'睡眠时长{period}趋势图'**
  2548 + String sleepDurationPeriodTrendChart(String period);
  2549 +
  2550 + /// No description provided for @sleepEmptyDateWithWeekday.
  2551 + ///
  2552 + /// In zh, this message translates to:
  2553 + /// **'-月-日 周-'**
  2554 + String get sleepEmptyDateWithWeekday;
  2555 +
2436 /// No description provided for @sleepQualityDescription. 2556 /// No description provided for @sleepQualityDescription.
2437 /// 2557 ///
2438 /// In zh, this message translates to: 2558 /// In zh, this message translates to:
@@ -1196,9 +1196,43 @@ class AppLocalizationsEn extends AppLocalizations { @@ -1196,9 +1196,43 @@ class AppLocalizationsEn extends AppLocalizations {
1196 } 1196 }
1197 1197
1198 @override 1198 @override
  1199 + String get hrvComparedLastMonthUnavailable => 'Compared with last month: -';
  1200 +
  1201 + @override
  1202 + String get hrvSameAsLastMonth => 'Same as last month';
  1203 +
  1204 + @override
  1205 + String hrvMoreDaysThanLastMonth(int count) {
  1206 + return '$count more days than last month';
  1207 + }
  1208 +
  1209 + @override
  1210 + String hrvFewerDaysThanLastMonth(int count) {
  1211 + return '$count fewer days than last month';
  1212 + }
  1213 +
  1214 + @override
1199 String get hrvUnlockNow => 'Unlock Now'; 1215 String get hrvUnlockNow => 'Unlock Now';
1200 1216
1201 @override 1217 @override
  1218 + String get hrvTrendTitle => 'HRV Trend';
  1219 +
  1220 + @override
  1221 + String hrvPeriodAverage(String period) {
  1222 + return 'This $period average';
  1223 + }
  1224 +
  1225 + @override
  1226 + String hrvComparedPreviousPeriod(String period) {
  1227 + return 'vs previous $period';
  1228 + }
  1229 +
  1230 + @override
  1231 + String hrvPeriodTrendChart(String period) {
  1232 + return 'HRV $period Trend Chart';
  1233 + }
  1234 +
  1235 + @override
1202 String get activityTotalBurn => 'Total Activity Burn'; 1236 String get activityTotalBurn => 'Total Activity Burn';
1203 1237
1204 @override 1238 @override
@@ -1239,6 +1273,19 @@ class AppLocalizationsEn extends AppLocalizations { @@ -1239,6 +1273,19 @@ class AppLocalizationsEn extends AppLocalizations {
1239 'Compared with last month: -'; 1273 'Compared with last month: -';
1240 1274
1241 @override 1275 @override
  1276 + String get activitySameAsLastWeek => 'Same as last week';
  1277 +
  1278 + @override
  1279 + String activityMoreThanLastWeek(int percent) {
  1280 + return '$percent% more than last week';
  1281 + }
  1282 +
  1283 + @override
  1284 + String activityLessThanLastWeek(int percent) {
  1285 + return '$percent% less than last week';
  1286 + }
  1287 +
  1288 + @override
1242 String get activitySameAsLastMonth => 'Same as last month'; 1289 String get activitySameAsLastMonth => 'Same as last month';
1243 1290
1244 @override 1291 @override
@@ -1252,6 +1299,22 @@ class AppLocalizationsEn extends AppLocalizations { @@ -1252,6 +1299,22 @@ class AppLocalizationsEn extends AppLocalizations {
1252 } 1299 }
1253 1300
1254 @override 1301 @override
  1302 + String get activityTrendTitle => 'Activity Burn Trend';
  1303 +
  1304 + @override
  1305 + String activityPeriodTotalBurn(String period) {
  1306 + return 'This $period total burn';
  1307 + }
  1308 +
  1309 + @override
  1310 + String get activityDailyAverageBurn => 'Daily average burn';
  1311 +
  1312 + @override
  1313 + String activityPeriodTrendChart(String period) {
  1314 + return 'Activity Burn $period Trend Chart';
  1315 + }
  1316 +
  1317 + @override
1255 String get activityMove => 'Move'; 1318 String get activityMove => 'Move';
1256 1319
1257 @override 1320 @override
@@ -1341,6 +1404,25 @@ class AppLocalizationsEn extends AppLocalizations { @@ -1341,6 +1404,25 @@ class AppLocalizationsEn extends AppLocalizations {
1341 String get sleepLowest => 'Lowest'; 1404 String get sleepLowest => 'Lowest';
1342 1405
1343 @override 1406 @override
  1407 + String get sleepTrendTitle => 'Sleep Report Trend';
  1408 +
  1409 + @override
  1410 + String sleepPeriodAverageDuration(String period) {
  1411 + return 'This $period average sleep';
  1412 + }
  1413 +
  1414 + @override
  1415 + String get sleepDeepSleepRatio => 'Deep sleep ratio';
  1416 +
  1417 + @override
  1418 + String sleepDurationPeriodTrendChart(String period) {
  1419 + return 'Sleep Duration $period Trend Chart';
  1420 + }
  1421 +
  1422 + @override
  1423 + String get sleepEmptyDateWithWeekday => '-';
  1424 +
  1425 + @override
1344 String get sleepQualityDescription => 1426 String get sleepQualityDescription =>
1345 'DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.'; 1427 'DoubleFeel calculates your daily sleep quality score from sleep duration, sleep stages, deep sleep and recovery, nighttime heart rate, and HRV changes.\nThis score helps you understand your recovery and sleep performance more clearly.';
1346 1428
@@ -1132,9 +1132,43 @@ class AppLocalizationsZh extends AppLocalizations { @@ -1132,9 +1132,43 @@ class AppLocalizationsZh extends AppLocalizations {
1132 } 1132 }
1133 1133
1134 @override 1134 @override
  1135 + String get hrvComparedLastMonthUnavailable => '比上月少-天';
  1136 +
  1137 + @override
  1138 + String get hrvSameAsLastMonth => '与上月一致';
  1139 +
  1140 + @override
  1141 + String hrvMoreDaysThanLastMonth(int count) {
  1142 + return '比上月多$count天';
  1143 + }
  1144 +
  1145 + @override
  1146 + String hrvFewerDaysThanLastMonth(int count) {
  1147 + return '比上月少$count天';
  1148 + }
  1149 +
  1150 + @override
1135 String get hrvUnlockNow => '立即解锁'; 1151 String get hrvUnlockNow => '立即解锁';
1136 1152
1137 @override 1153 @override
  1154 + String get hrvTrendTitle => 'HRV趋势';
  1155 +
  1156 + @override
  1157 + String hrvPeriodAverage(String period) {
  1158 + return '本$period平均';
  1159 + }
  1160 +
  1161 + @override
  1162 + String hrvComparedPreviousPeriod(String period) {
  1163 + return '较上$period';
  1164 + }
  1165 +
  1166 + @override
  1167 + String hrvPeriodTrendChart(String period) {
  1168 + return 'HRV $period趋势图';
  1169 + }
  1170 +
  1171 + @override
1138 String get activityTotalBurn => '活动总消耗'; 1172 String get activityTotalBurn => '活动总消耗';
1139 1173
1140 @override 1174 @override
@@ -1174,6 +1208,19 @@ class AppLocalizationsZh extends AppLocalizations { @@ -1174,6 +1208,19 @@ class AppLocalizationsZh extends AppLocalizations {
1174 String get activityComparedLastMonthUnavailable => '比上月-'; 1208 String get activityComparedLastMonthUnavailable => '比上月-';
1175 1209
1176 @override 1210 @override
  1211 + String get activitySameAsLastWeek => '与上周持平';
  1212 +
  1213 + @override
  1214 + String activityMoreThanLastWeek(int percent) {
  1215 + return '比上周多$percent%';
  1216 + }
  1217 +
  1218 + @override
  1219 + String activityLessThanLastWeek(int percent) {
  1220 + return '比上周少$percent%';
  1221 + }
  1222 +
  1223 + @override
1177 String get activitySameAsLastMonth => '与上月持平'; 1224 String get activitySameAsLastMonth => '与上月持平';
1178 1225
1179 @override 1226 @override
@@ -1187,6 +1234,22 @@ class AppLocalizationsZh extends AppLocalizations { @@ -1187,6 +1234,22 @@ class AppLocalizationsZh extends AppLocalizations {
1187 } 1234 }
1188 1235
1189 @override 1236 @override
  1237 + String get activityTrendTitle => '活动消耗趋势';
  1238 +
  1239 + @override
  1240 + String activityPeriodTotalBurn(String period) {
  1241 + return '本$period总消耗';
  1242 + }
  1243 +
  1244 + @override
  1245 + String get activityDailyAverageBurn => '日均消耗';
  1246 +
  1247 + @override
  1248 + String activityPeriodTrendChart(String period) {
  1249 + return '活动消耗$period趋势图';
  1250 + }
  1251 +
  1252 + @override
1190 String get activityMove => '活动'; 1253 String get activityMove => '活动';
1191 1254
1192 @override 1255 @override
@@ -1276,6 +1339,25 @@ class AppLocalizationsZh extends AppLocalizations { @@ -1276,6 +1339,25 @@ class AppLocalizationsZh extends AppLocalizations {
1276 String get sleepLowest => '最低'; 1339 String get sleepLowest => '最低';
1277 1340
1278 @override 1341 @override
  1342 + String get sleepTrendTitle => '睡眠报告趋势';
  1343 +
  1344 + @override
  1345 + String sleepPeriodAverageDuration(String period) {
  1346 + return '本$period均睡眠';
  1347 + }
  1348 +
  1349 + @override
  1350 + String get sleepDeepSleepRatio => '深睡占比';
  1351 +
  1352 + @override
  1353 + String sleepDurationPeriodTrendChart(String period) {
  1354 + return '睡眠时长$period趋势图';
  1355 + }
  1356 +
  1357 + @override
  1358 + String get sleepEmptyDateWithWeekday => '-月-日 周-';
  1359 +
  1360 + @override
1279 String get sleepQualityDescription => 1361 String get sleepQualityDescription =>
1280 'DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。'; 1362 'DoubleFeel 会根据你的睡眠时长、睡眠阶段、深度睡眠与恢复状态、夜间心率与 HRV 变化等综合生成当天的睡眠质量评分。\n该评分能够帮助你更直观地了解身体恢复状态与睡眠表现。';
1281 1363