Showing
57 changed files
with
2157 additions
and
212 deletions
assets/images/common/ic_add.png
0 → 100644
202 Bytes
assets/images/common/ic_apple.png
0 → 100644
392 Bytes
assets/images/common/ic_arrow_down.png
0 → 100644
267 Bytes
285 Bytes
assets/images/common/ic_arrow_left.png
0 → 100644
566 Bytes
285 Bytes
assets/images/common/ic_arrow_right.png
0 → 100644
537 Bytes
assets/images/common/ic_back_to_today.png
0 → 100644
634 Bytes
assets/images/common/ic_calendar.png
0 → 100644
451 Bytes
assets/images/common/ic_check.png
0 → 100644
332 Bytes
assets/images/common/ic_chevron_right.png
0 → 100644
255 Bytes
assets/images/common/ic_close.png
0 → 100644
266 Bytes
assets/images/common/ic_close_filled.png
0 → 100644
498 Bytes
assets/images/common/ic_close_round.png
0 → 100644
345 Bytes
assets/images/common/ic_equal.png
0 → 100644
107 Bytes
assets/images/common/ic_exercise.png
0 → 100644
1.79 KB
assets/images/common/ic_info.png
0 → 100644
1.74 KB
assets/images/common/ic_lock.png
0 → 100644
981 Bytes
assets/images/common/ic_more.png
0 → 100644
597 Bytes
assets/images/common/ic_more_dots.png
0 → 100644
600 Bytes
assets/images/common/ic_more_grey.png
0 → 100644
406 Bytes
assets/images/common/ic_pro.png
0 → 100644
2.17 KB
assets/images/common/ic_replace.png
0 → 100644
856 Bytes
assets/images/common/ic_settings.png
0 → 100644
1.52 KB
assets/images/common/ic_sleep_filled.png
0 → 100644
872 Bytes
assets/images/common/ic_sleep_stroke.png
0 → 100644
961 Bytes
assets/images/today/ic_tooltip_hrv.png
0 → 100644
467 Bytes
| @@ -54,7 +54,7 @@ EXTERNAL SOURCES: | @@ -54,7 +54,7 @@ EXTERNAL SOURCES: | ||
| 54 | :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" | 54 | :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" |
| 55 | 55 | ||
| 56 | SPEC CHECKSUMS: | 56 | SPEC CHECKSUMS: |
| 57 | - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 | 57 | + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 |
| 58 | image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 | 58 | image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 |
| 59 | image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | 59 | image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a |
| 60 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | 60 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 |
| 1 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 1 | import 'package:doublefeel_flutter/l10n/l10n_extensions.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'; |
| @@ -11,6 +12,9 @@ class DoubleFeelApp extends StatelessWidget { | @@ -11,6 +12,9 @@ class DoubleFeelApp extends StatelessWidget { | ||
| 11 | Widget build(BuildContext context) { | 12 | Widget build(BuildContext context) { |
| 12 | return GetMaterialApp( | 13 | return GetMaterialApp( |
| 13 | title: context.l10n.appName, | 14 | title: context.l10n.appName, |
| 15 | + theme: AppTheme.lightTheme, | ||
| 16 | + darkTheme: AppTheme.darkTheme, | ||
| 17 | + themeMode: ThemeMode.light, | ||
| 14 | initialRoute: AppPages.initialRoute, | 18 | initialRoute: AppPages.initialRoute, |
| 15 | getPages: AppPages.routes, | 19 | getPages: AppPages.routes, |
| 16 | localizationsDelegates: AppLocalizations.localizationsDelegates, | 20 | localizationsDelegates: AppLocalizations.localizationsDelegates, |
| @@ -2,15 +2,19 @@ import 'package:get/get.dart'; | @@ -2,15 +2,19 @@ import 'package:get/get.dart'; | ||
| 2 | 2 | ||
| 3 | import '../controllers/home_controller.dart'; | 3 | import '../controllers/home_controller.dart'; |
| 4 | import '../controllers/today_controller.dart'; | 4 | import '../controllers/today_controller.dart'; |
| 5 | +import '../controllers/trend/trend_controller.dart'; | ||
| 6 | +import '../controllers/trend/hrv_controller.dart'; | ||
| 7 | +import '../controllers/trend/activity_controller.dart'; | ||
| 8 | +import '../controllers/trend/sleep_controller.dart'; | ||
| 5 | 9 | ||
| 6 | class HomeBinding extends Bindings { | 10 | class HomeBinding extends Bindings { |
| 7 | @override | 11 | @override |
| 8 | void dependencies() { | 12 | void dependencies() { |
| 9 | Get.lazyPut<HomeController>(() => HomeController(), fenix: true); | 13 | Get.lazyPut<HomeController>(() => HomeController(), fenix: true); |
| 10 | Get.lazyPut<TodayController>(() => TodayController(), fenix: true); | 14 | Get.lazyPut<TodayController>(() => TodayController(), fenix: true); |
| 11 | - // 其他 tab 的 controller 后续在此添加 | ||
| 12 | - // Get.lazyPut<TrendController>(() => TrendController(), fenix: true); | ||
| 13 | - // Get.lazyPut<FriendsController>(() => FriendsController(), fenix: true); | ||
| 14 | - // Get.lazyPut<MyController>(() => MyController(), fenix: true); | 15 | + Get.lazyPut<TrendController>(() => TrendController(), fenix: true); |
| 16 | + Get.lazyPut<HrvController>(() => HrvController(), fenix: true); | ||
| 17 | + Get.lazyPut<ActivityController>(() => ActivityController(), fenix: true); | ||
| 18 | + Get.lazyPut<SleepController>(() => SleepController(), fenix: true); | ||
| 15 | } | 19 | } |
| 16 | } | 20 | } |
| 1 | +import 'package:get/get.dart'; | ||
| 2 | +import 'trend_period_controller.dart'; | ||
| 3 | + | ||
| 4 | +/// 活动消耗 专属 Controller | ||
| 5 | +class ActivityController extends TrendPeriodController { | ||
| 6 | + // ── 响应式数据源 ────────────────────────────────────── | ||
| 7 | + final totalBurn = '0'.obs; | ||
| 8 | + final averageBurn = '0'.obs; | ||
| 9 | + final chartData = <double>[].obs; | ||
| 10 | + final chartLabels = <String>[].obs; | ||
| 11 | + | ||
| 12 | + @override | ||
| 13 | + void loadData() { | ||
| 14 | + isLoading.value = true; | ||
| 15 | + | ||
| 16 | + // 模拟 API 延迟加载 | ||
| 17 | + Future.delayed(const Duration(milliseconds: 300), () { | ||
| 18 | + final offset = dateOffset.value; | ||
| 19 | + | ||
| 20 | + // 依据时间范围偏移动态渲染不同数据,模拟真实 API 拉取效果 | ||
| 21 | + totalBurn.value = '${2340 + offset * 120}'; | ||
| 22 | + averageBurn.value = '${334 + offset * 18}'; | ||
| 23 | + | ||
| 24 | + chartData.value = [ | ||
| 25 | + 280.0 + offset * 15, | ||
| 26 | + 350.0 - offset * 25, | ||
| 27 | + 420.0 + offset * 35, | ||
| 28 | + 310.0 - offset * 15, | ||
| 29 | + 390.0 + offset * 20, | ||
| 30 | + 480.0 + offset * 45, | ||
| 31 | + 260.0 - offset * 30, | ||
| 32 | + ].map((e) => e.clamp(50.0, 600.0)).toList(); | ||
| 33 | + | ||
| 34 | + chartLabels.value = ['一', '二', '三', '四', '五', '六', '日']; | ||
| 35 | + | ||
| 36 | + isLoading.value = false; | ||
| 37 | + }); | ||
| 38 | + } | ||
| 39 | +} |
| 1 | +import 'package:get/get.dart'; | ||
| 2 | +import 'trend_period_controller.dart'; | ||
| 3 | + | ||
| 4 | +/// HRV 心率变异性 专属 Controller | ||
| 5 | +class HrvController extends TrendPeriodController { | ||
| 6 | + // ── 响应式数据源 ────────────────────────────────────── | ||
| 7 | + final averageHrv = '0'.obs; | ||
| 8 | + final changeHrv = '0'.obs; | ||
| 9 | + final chartData = <double>[].obs; | ||
| 10 | + final chartLabels = <String>[].obs; | ||
| 11 | + | ||
| 12 | + @override | ||
| 13 | + void loadData() { | ||
| 14 | + isLoading.value = true; | ||
| 15 | + | ||
| 16 | + // 模拟 API 延迟加载 | ||
| 17 | + Future.delayed(const Duration(milliseconds: 300), () { | ||
| 18 | + final offset = dateOffset.value; | ||
| 19 | + | ||
| 20 | + // 依据时间范围偏移动态渲染不同数据,模拟真实 API 拉取效果 | ||
| 21 | + averageHrv.value = '${46 + offset}'; | ||
| 22 | + changeHrv.value = offset >= 0 ? '+3' : '${offset * 2}'; | ||
| 23 | + | ||
| 24 | + chartData.value = [ | ||
| 25 | + 42.0 + offset, | ||
| 26 | + 48.0 - offset, | ||
| 27 | + 55.0 + offset * 2, | ||
| 28 | + 46.0 - offset, | ||
| 29 | + 52.0 + offset, | ||
| 30 | + 58.0 + offset * 3, | ||
| 31 | + 50.0 - offset * 2, | ||
| 32 | + ].map((e) => e.clamp(20.0, 80.0)).toList(); | ||
| 33 | + | ||
| 34 | + chartLabels.value = ['一', '二', '三', '四', '五', '六', '日']; | ||
| 35 | + | ||
| 36 | + isLoading.value = false; | ||
| 37 | + }); | ||
| 38 | + } | ||
| 39 | +} |
| 1 | +import 'package:get/get.dart'; | ||
| 2 | +import 'trend_period_controller.dart'; | ||
| 3 | + | ||
| 4 | +/// 睡眠报告 专属 Controller | ||
| 5 | +class SleepController extends TrendPeriodController { | ||
| 6 | + // ── 响应式数据源 ────────────────────────────────────── | ||
| 7 | + final averageSleep = '0.0'.obs; | ||
| 8 | + final deepSleepRatio = '0'.obs; | ||
| 9 | + final chartData = <double>[].obs; | ||
| 10 | + final chartLabels = <String>[].obs; | ||
| 11 | + | ||
| 12 | + @override | ||
| 13 | + void loadData() { | ||
| 14 | + isLoading.value = true; | ||
| 15 | + | ||
| 16 | + // 模拟 API 延迟加载 | ||
| 17 | + Future.delayed(const Duration(milliseconds: 300), () { | ||
| 18 | + final offset = dateOffset.value; | ||
| 19 | + | ||
| 20 | + // 依据时间范围偏移动态渲染不同数据,模拟真实 API 拉取效果 | ||
| 21 | + final avg = (7.2 + offset * 0.15).clamp(4.0, 10.0); | ||
| 22 | + averageSleep.value = avg.toStringAsFixed(1); | ||
| 23 | + deepSleepRatio.value = '${(28 + offset).clamp(10, 50)}'; | ||
| 24 | + | ||
| 25 | + chartData.value = [ | ||
| 26 | + 6.5 + offset * 0.1, | ||
| 27 | + 7.0 - offset * 0.2, | ||
| 28 | + 8.2 + offset * 0.3, | ||
| 29 | + 6.8 - offset * 0.1, | ||
| 30 | + 7.5 + offset * 0.2, | ||
| 31 | + 9.0 + offset * 0.4, | ||
| 32 | + 7.2 - offset * 0.3, | ||
| 33 | + ].map((e) => e.clamp(4.0, 10.0)).toList(); | ||
| 34 | + | ||
| 35 | + chartLabels.value = ['一', '二', '三', '四', '五', '六', '日']; | ||
| 36 | + | ||
| 37 | + isLoading.value = false; | ||
| 38 | + }); | ||
| 39 | + } | ||
| 40 | +} |
| 1 | +import 'package:get/get.dart'; | ||
| 2 | + | ||
| 3 | +/// 趋势页顶层 Controller,仅负责: | ||
| 4 | +/// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex) | ||
| 5 | +class TrendController extends GetxController { | ||
| 6 | + // 第1层类型 Tab(0 = HRV, 1 = 活动, 2 = 睡眠) | ||
| 7 | + final selectedTypeIndex = 0.obs; | ||
| 8 | + | ||
| 9 | + void changeType(int index) { | ||
| 10 | + if (index == selectedTypeIndex.value) return; | ||
| 11 | + selectedTypeIndex.value = index; | ||
| 12 | + } | ||
| 13 | +} |
| 1 | +import 'package:flutter/foundation.dart'; | ||
| 2 | +import 'package:get/get.dart'; | ||
| 3 | + | ||
| 4 | +import 'trend_types.dart'; | ||
| 5 | + | ||
| 6 | +/// TrendPeriodController — 抽象基类 | ||
| 7 | +/// | ||
| 8 | +/// 封装 HRV / 活动 / 睡眠 三个 Controller 共用的: | ||
| 9 | +/// - 第2层:周 / 月 / 年 period 切换 | ||
| 10 | +/// - 第3层:日期范围偏移(每个 period 各自记忆 offset) | ||
| 11 | +/// - isLoading 状态 | ||
| 12 | +/// - dateRangeLabel 计算 | ||
| 13 | +/// | ||
| 14 | +/// 子类只需实现 [loadData()],专注自己的数据逻辑。 | ||
| 15 | +abstract class TrendPeriodController extends GetxController { | ||
| 16 | + // ── 第2层:时间维度 ────────────────────────────────── | ||
| 17 | + final currentPeriod = TrendPeriod.week.obs; | ||
| 18 | + | ||
| 19 | + // ── 第3层:日期偏移(每个 period 独立记忆)────────── | ||
| 20 | + // key = TrendPeriod,value = offset(0=当前,-1=上一个…) | ||
| 21 | + final _offsetByPeriod = <TrendPeriod, int>{}; | ||
| 22 | + final dateOffset = 0.obs; | ||
| 23 | + | ||
| 24 | + // ── 加载状态 ───────────────────────────────────────── | ||
| 25 | + final isLoading = false.obs; | ||
| 26 | + | ||
| 27 | + // ──────────────────────────────────────────────────── | ||
| 28 | + // 公开 API | ||
| 29 | + // ──────────────────────────────────────────────────── | ||
| 30 | + | ||
| 31 | + void changePeriod(TrendPeriod period) { | ||
| 32 | + if (currentPeriod.value == period) return; | ||
| 33 | + // 保存当前 period 的 offset,再切换 | ||
| 34 | + _offsetByPeriod[currentPeriod.value] = dateOffset.value; | ||
| 35 | + currentPeriod.value = period; | ||
| 36 | + // 恢复新 period 上次记忆 of offset(默认 0) | ||
| 37 | + dateOffset.value = _offsetByPeriod[period] ?? 0; | ||
| 38 | + loadData(); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + void goToPrevious() { | ||
| 42 | + final key = currentPeriod.value; | ||
| 43 | + _offsetByPeriod[key] = (_offsetByPeriod[key] ?? 0) - 1; | ||
| 44 | + dateOffset.value = _offsetByPeriod[key]!; | ||
| 45 | + loadData(); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + void goToNext() { | ||
| 49 | + if (!canGoNext) return; | ||
| 50 | + final key = currentPeriod.value; | ||
| 51 | + _offsetByPeriod[key] = (_offsetByPeriod[key] ?? 0) + 1; | ||
| 52 | + dateOffset.value = _offsetByPeriod[key]!; | ||
| 53 | + loadData(); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + bool get canGoNext => dateOffset.value < 0; | ||
| 57 | + | ||
| 58 | + /// 当前 period + offset 对应的日期显示文字 | ||
| 59 | + String get dateRangeLabel => | ||
| 60 | + buildDateRangeLabel(currentPeriod.value, dateOffset.value); | ||
| 61 | + | ||
| 62 | + // ──────────────────────────────────────────────────── | ||
| 63 | + // 子类必须实现:各自的数据加载逻辑 | ||
| 64 | + // ──────────────────────────────────────────────────── | ||
| 65 | + | ||
| 66 | + @protected | ||
| 67 | + void loadData(); | ||
| 68 | + | ||
| 69 | + @override | ||
| 70 | + void onInit() { | ||
| 71 | + super.onInit(); | ||
| 72 | + loadData(); | ||
| 73 | + } | ||
| 74 | +} |
| 1 | +import 'package:intl/intl.dart'; | ||
| 2 | + | ||
| 3 | +/// 时间维度枚举(共享给所有 trend controller 使用) | ||
| 4 | +enum TrendPeriod { week, month, year } | ||
| 5 | + | ||
| 6 | +extension TrendPeriodLabel on TrendPeriod { | ||
| 7 | + String get label => switch (this) { | ||
| 8 | + TrendPeriod.week => '周', | ||
| 9 | + TrendPeriod.month => '月', | ||
| 10 | + TrendPeriod.year => '年', | ||
| 11 | + }; | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +/// 根据 period + offset 计算显示标签 | ||
| 15 | +String buildDateRangeLabel(TrendPeriod period, int offset) { | ||
| 16 | + final now = DateTime.now(); | ||
| 17 | + switch (period) { | ||
| 18 | + case TrendPeriod.week: | ||
| 19 | + final monday = now.subtract(Duration(days: now.weekday - 1)); | ||
| 20 | + final start = monday.add(Duration(days: offset * 7)); | ||
| 21 | + final end = start.add(const Duration(days: 6)); | ||
| 22 | + final fmt = DateFormat('M月d日'); | ||
| 23 | + return '${fmt.format(start)} - ${fmt.format(end)}'; | ||
| 24 | + case TrendPeriod.month: | ||
| 25 | + final month = DateTime(now.year, now.month + offset); | ||
| 26 | + return DateFormat('yyyy年M月').format(month); | ||
| 27 | + case TrendPeriod.year: | ||
| 28 | + return '${now.year + offset}年'; | ||
| 29 | + } | ||
| 30 | +} |
| @@ -175,14 +175,6 @@ class TodayTab extends GetView<TodayController> { | @@ -175,14 +175,6 @@ class TodayTab extends GetView<TodayController> { | ||
| 175 | const TodayHrvAdBanner(), | 175 | const TodayHrvAdBanner(), |
| 176 | const SizedBox(height: 12), | 176 | const SizedBox(height: 12), |
| 177 | 177 | ||
| 178 | - // 2. 该日压力状态行 | ||
| 179 | - _SectionHeader( | ||
| 180 | - title: '该日压力状态', | ||
| 181 | - showInfo: true, | ||
| 182 | - onInfoTap: () {}, | ||
| 183 | - ), | ||
| 184 | - const SizedBox(height: 8), | ||
| 185 | - | ||
| 186 | // 3. HRV + 心率数字卡 | 178 | // 3. HRV + 心率数字卡 |
| 187 | const TodayHrvNumberCard(), | 179 | const TodayHrvNumberCard(), |
| 188 | const SizedBox(height: 12), | 180 | const SizedBox(height: 12), |
| @@ -190,16 +182,27 @@ class TodayTab extends GetView<TodayController> { | @@ -190,16 +182,27 @@ class TodayTab extends GetView<TodayController> { | ||
| 190 | // 4. HRV 趋势图 | 182 | // 4. HRV 趋势图 |
| 191 | const TodayHrvChartCard(), | 183 | const TodayHrvChartCard(), |
| 192 | const SizedBox(height: 12), | 184 | const SizedBox(height: 12), |
| 193 | - | 185 | + Container( |
| 186 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), | ||
| 187 | + margin: const EdgeInsets.only(bottom: 12), | ||
| 188 | + decoration: BoxDecoration( | ||
| 189 | + color: Colors.white, | ||
| 190 | + borderRadius: BorderRadius.circular(16), | ||
| 191 | + ), | ||
| 192 | + child: Text( | ||
| 193 | + '立即测量HRV', | ||
| 194 | + style: TextStyle( | ||
| 195 | + color: const Color(0xFF0F0F11), | ||
| 196 | + fontSize: 16, | ||
| 197 | + fontWeight: FontWeight.w600, | ||
| 198 | + ), | ||
| 199 | + ), | ||
| 200 | + ), | ||
| 194 | // 5. 每日行动 | 201 | // 5. 每日行动 |
| 195 | const TodayActionsCard(), | 202 | const TodayActionsCard(), |
| 196 | 203 | ||
| 197 | - const TodayHrvChartCard(), | ||
| 198 | - const TodayHrvChartCard(), | ||
| 199 | - const TodayHrvChartCard(), | ||
| 200 | - const TodayHrvChartCard(), | ||
| 201 | // 底部安全间距(留给 TabBar 高度) | 204 | // 底部安全间距(留给 TabBar 高度) |
| 202 | - const SizedBox(height: 80), | 205 | + const SizedBox(height: 180), |
| 203 | ]), | 206 | ]), |
| 204 | ), | 207 | ), |
| 205 | ), | 208 | ), |
| @@ -288,43 +291,3 @@ class TodayTab extends GetView<TodayController> { | @@ -288,43 +291,3 @@ class TodayTab extends GetView<TodayController> { | ||
| 288 | bool _isSameDay(DateTime a, DateTime b) => | 291 | bool _isSameDay(DateTime a, DateTime b) => |
| 289 | a.year == b.year && a.month == b.month && a.day == b.day; | 292 | a.year == b.year && a.month == b.month && a.day == b.day; |
| 290 | } | 293 | } |
| 291 | - | ||
| 292 | -/// 通用 Section 标题行 | ||
| 293 | -class _SectionHeader extends StatelessWidget { | ||
| 294 | - final String title; | ||
| 295 | - final bool showInfo; | ||
| 296 | - final VoidCallback? onInfoTap; | ||
| 297 | - | ||
| 298 | - const _SectionHeader({ | ||
| 299 | - required this.title, | ||
| 300 | - this.showInfo = false, | ||
| 301 | - this.onInfoTap, | ||
| 302 | - }); | ||
| 303 | - | ||
| 304 | - @override | ||
| 305 | - Widget build(BuildContext context) { | ||
| 306 | - return Row( | ||
| 307 | - children: [ | ||
| 308 | - Text( | ||
| 309 | - title, | ||
| 310 | - style: const TextStyle( | ||
| 311 | - fontSize: 15, | ||
| 312 | - fontWeight: FontWeight.w600, | ||
| 313 | - color: Color(0xFF0F0F11), | ||
| 314 | - ), | ||
| 315 | - ), | ||
| 316 | - if (showInfo) ...[ | ||
| 317 | - const SizedBox(width: 4), | ||
| 318 | - GestureDetector( | ||
| 319 | - onTap: onInfoTap, | ||
| 320 | - child: const Icon( | ||
| 321 | - Icons.info_outline, | ||
| 322 | - size: 15, | ||
| 323 | - color: Color(0xFF999999), | ||
| 324 | - ), | ||
| 325 | - ), | ||
| 326 | - ], | ||
| 327 | - ], | ||
| 328 | - ); | ||
| 329 | - } | ||
| 330 | -} |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | +import 'package:get/get.dart'; | ||
| 2 | 3 | ||
| 3 | -class TrendTab extends StatelessWidget { | 4 | +import '../../controllers/trend/trend_controller.dart'; |
| 5 | +import '../../widgets/trend/trend_type_tab_bar.dart'; | ||
| 6 | +import '../../widgets/trend/hrv_trend_view.dart'; | ||
| 7 | +import '../../widgets/trend/activity_trend_view.dart'; | ||
| 8 | +import '../../widgets/trend/sleep_trend_view.dart'; | ||
| 9 | + | ||
| 10 | +/// 趋势大 Tab 页面主视图 | ||
| 11 | +/// 采用 StatefulWidget 绑定 SingleTickerProviderStateMixin 并通过 TabController | ||
| 12 | +/// 控制 TabBar 与 TabBarView 的同步滑动,实现极具高级感的跟手划动效果。 | ||
| 13 | +class TrendTab extends StatefulWidget { | ||
| 4 | const TrendTab({super.key}); | 14 | const TrendTab({super.key}); |
| 5 | 15 | ||
| 6 | @override | 16 | @override |
| 17 | + State<TrendTab> createState() => _TrendTabState(); | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +class _TrendTabState extends State<TrendTab> with SingleTickerProviderStateMixin { | ||
| 21 | + late final TabController _tabController; | ||
| 22 | + late final TrendController _trendController; | ||
| 23 | + late final Worker _rxWorker; | ||
| 24 | + | ||
| 25 | + static const _bgColor = Color(0xFFF5F2FF); | ||
| 26 | + static const _h1 = Color(0xFF0F0F11); | ||
| 27 | + | ||
| 28 | + // 三个内容区(包裹 KeepAliveWrapper 以记忆滑动/图表状态,确保子 View 能继续保持纯净的 GetView 规范) | ||
| 29 | + static const _contentViews = [ | ||
| 30 | + KeepAliveWrapper(child: HrvTrendView()), | ||
| 31 | + KeepAliveWrapper(child: ActivityTrendView()), | ||
| 32 | + KeepAliveWrapper(child: SleepTrendView()), | ||
| 33 | + ]; | ||
| 34 | + | ||
| 35 | + @override | ||
| 36 | + void initState() { | ||
| 37 | + super.initState(); | ||
| 38 | + _trendController = Get.find<TrendController>(); | ||
| 39 | + | ||
| 40 | + // 初始化 TabController | ||
| 41 | + _tabController = TabController( | ||
| 42 | + length: 3, | ||
| 43 | + vsync: this, | ||
| 44 | + initialIndex: _trendController.selectedTypeIndex.value, | ||
| 45 | + ); | ||
| 46 | + | ||
| 47 | + // 1. 当滑动/点击切换页面时,同步更新 TrendController 状态 | ||
| 48 | + _tabController.addListener(() { | ||
| 49 | + if (!_tabController.indexIsChanging) { | ||
| 50 | + _trendController.changeType(_tabController.index); | ||
| 51 | + } | ||
| 52 | + }); | ||
| 53 | + | ||
| 54 | + // 2. 当外部修改 selectedTypeIndex 时,同步动画跳转 TabBarView | ||
| 55 | + _rxWorker = ever(_trendController.selectedTypeIndex, (index) { | ||
| 56 | + if (_tabController.index != index) { | ||
| 57 | + _tabController.animateTo(index); | ||
| 58 | + } | ||
| 59 | + }); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + @override | ||
| 63 | + void dispose() { | ||
| 64 | + _rxWorker.dispose(); | ||
| 65 | + _tabController.dispose(); | ||
| 66 | + super.dispose(); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + @override | ||
| 7 | Widget build(BuildContext context) { | 70 | Widget build(BuildContext context) { |
| 8 | - return const Center( | ||
| 9 | - child: Text('趋势', style: TextStyle(fontSize: 20, color: Color(0xFF0F0F11))), | 71 | + final topPadding = MediaQuery.of(context).padding.top; |
| 72 | + | ||
| 73 | + return Container( | ||
| 74 | + color: _bgColor, | ||
| 75 | + child: Column( | ||
| 76 | + children: [ | ||
| 77 | + // ── 状态栏占位 + 标题栏 ── | ||
| 78 | + Container( | ||
| 79 | + color: _bgColor, | ||
| 80 | + padding: EdgeInsets.only(top: topPadding), | ||
| 81 | + child: _buildAppBar(), | ||
| 82 | + ), | ||
| 83 | + | ||
| 84 | + // ── 第1层:原生 TabBar(支持跟手滑动指示器) ── | ||
| 85 | + Container( | ||
| 86 | + color: _bgColor, | ||
| 87 | + child: TrendTypeTabBar(tabController: _tabController), | ||
| 88 | + ), | ||
| 89 | + | ||
| 90 | + // ── 内容区:TabBarView 提供极其平滑、跟手的原生翻页动画 ── | ||
| 91 | + Expanded( | ||
| 92 | + child: TabBarView( | ||
| 93 | + controller: _tabController, | ||
| 94 | + children: _contentViews, | ||
| 95 | + ), | ||
| 96 | + ), | ||
| 97 | + ], | ||
| 98 | + ), | ||
| 99 | + ); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + Widget _buildAppBar() { | ||
| 103 | + return SizedBox( | ||
| 104 | + height: 48, | ||
| 105 | + child: const Padding( | ||
| 106 | + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), | ||
| 107 | + child: Row( | ||
| 108 | + children: [ | ||
| 109 | + Text( | ||
| 110 | + '趋势', | ||
| 111 | + style: TextStyle( | ||
| 112 | + color: _h1, | ||
| 113 | + fontSize: 24, | ||
| 114 | + fontWeight: FontWeight.w600, | ||
| 115 | + ), | ||
| 116 | + ), | ||
| 117 | + ], | ||
| 118 | + ), | ||
| 119 | + ), | ||
| 10 | ); | 120 | ); |
| 11 | } | 121 | } |
| 12 | } | 122 | } |
| 123 | + | ||
| 124 | +/// 专用于在 TabBarView/PageView 中保持子组件状态的轻量级包装器 | ||
| 125 | +/// 使得子 View 可以维持无状态的 GetView 极简设计规范 | ||
| 126 | +class KeepAliveWrapper extends StatefulWidget { | ||
| 127 | + final Widget child; | ||
| 128 | + const KeepAliveWrapper({super.key, required this.child}); | ||
| 129 | + | ||
| 130 | + @override | ||
| 131 | + State<KeepAliveWrapper> createState() => _KeepAliveWrapperState(); | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +class _KeepAliveWrapperState extends State<KeepAliveWrapper> | ||
| 135 | + with AutomaticKeepAliveClientMixin { | ||
| 136 | + @override | ||
| 137 | + bool get wantKeepAlive => true; | ||
| 138 | + | ||
| 139 | + @override | ||
| 140 | + Widget build(BuildContext context) { | ||
| 141 | + super.build(context); | ||
| 142 | + return widget.child; | ||
| 143 | + } | ||
| 144 | +} |
| @@ -47,7 +47,6 @@ class _ActionCard extends StatelessWidget { | @@ -47,7 +47,6 @@ class _ActionCard extends StatelessWidget { | ||
| 47 | 47 | ||
| 48 | const _ActionCard({required this.item}); | 48 | const _ActionCard({required this.item}); |
| 49 | 49 | ||
| 50 | - static const _cardBg = Color(0xFFF7F7F9); | ||
| 51 | static const _brandColor = Color(0xFF845EEE); | 50 | static const _brandColor = Color(0xFF845EEE); |
| 52 | static const _h1 = Color(0xFF0F0F11); | 51 | static const _h1 = Color(0xFF0F0F11); |
| 53 | static const _h2 = Color(0xFF666666); | 52 | static const _h2 = Color(0xFF666666); |
| @@ -58,7 +57,7 @@ class _ActionCard extends StatelessWidget { | @@ -58,7 +57,7 @@ class _ActionCard extends StatelessWidget { | ||
| 58 | width: 140, | 57 | width: 140, |
| 59 | padding: const EdgeInsets.all(14), | 58 | padding: const EdgeInsets.all(14), |
| 60 | decoration: BoxDecoration( | 59 | decoration: BoxDecoration( |
| 61 | - color: _cardBg, | 60 | + color: Colors.white, |
| 62 | borderRadius: BorderRadius.circular(16), | 61 | borderRadius: BorderRadius.circular(16), |
| 63 | ), | 62 | ), |
| 64 | child: Column( | 63 | child: Column( |
| @@ -14,11 +14,33 @@ import '../../controllers/today_controller.dart'; | @@ -14,11 +14,33 @@ import '../../controllers/today_controller.dart'; | ||
| 14 | class TodayHrvChartCard extends GetView<TodayController> { | 14 | class TodayHrvChartCard extends GetView<TodayController> { |
| 15 | const TodayHrvChartCard({super.key}); | 15 | const TodayHrvChartCard({super.key}); |
| 16 | 16 | ||
| 17 | - static const _cardBg = Color(0xFFF7F7F9); | ||
| 18 | - | 17 | + static const _h2 = Color(0xFF78787D); |
| 19 | static const _h3 = Color(0xFF999999); | 18 | static const _h3 = Color(0xFF999999); |
| 20 | static const _h5 = Color(0xFFCCCCCC); | 19 | static const _h5 = Color(0xFFCCCCCC); |
| 21 | 20 | ||
| 21 | + /// 实时压力 mock:按小时 0~18,Y 轴 0~80 | ||
| 22 | + static const List<double> _mockStressByHour = [ | ||
| 23 | + 18, | ||
| 24 | + 22, | ||
| 25 | + 28, | ||
| 26 | + 35, | ||
| 27 | + 32, | ||
| 28 | + 40, | ||
| 29 | + 48, | ||
| 30 | + 55, | ||
| 31 | + 62, | ||
| 32 | + 58, | ||
| 33 | + 52, | ||
| 34 | + 45, | ||
| 35 | + 38, | ||
| 36 | + 42, | ||
| 37 | + 50, | ||
| 38 | + 56, | ||
| 39 | + 64, | ||
| 40 | + 58, | ||
| 41 | + 46, | ||
| 42 | + ]; | ||
| 43 | + | ||
| 22 | @override | 44 | @override |
| 23 | Widget build(BuildContext context) { | 45 | Widget build(BuildContext context) { |
| 24 | final spots = | 46 | final spots = |
| @@ -27,7 +49,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -27,7 +49,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 27 | return Container( | 49 | return Container( |
| 28 | padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), | 50 | padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), |
| 29 | decoration: BoxDecoration( | 51 | decoration: BoxDecoration( |
| 30 | - color: _cardBg, | 52 | + color: Colors.white, |
| 31 | borderRadius: BorderRadius.circular(16), | 53 | borderRadius: BorderRadius.circular(16), |
| 32 | ), | 54 | ), |
| 33 | child: Column( | 55 | child: Column( |
| @@ -48,16 +70,16 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -48,16 +70,16 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 48 | const Spacer(), | 70 | const Spacer(), |
| 49 | ], | 71 | ], |
| 50 | ), | 72 | ), |
| 51 | - const SizedBox(height: 16), | 73 | + const SizedBox(width: 14), |
| 52 | SizedBox( | 74 | SizedBox( |
| 53 | - height: 160, | 75 | + height: 174, |
| 54 | child: LineChart( | 76 | child: LineChart( |
| 55 | LineChartData( | 77 | LineChartData( |
| 56 | minX: 0, | 78 | minX: 0, |
| 57 | maxX: 18, | 79 | maxX: 18, |
| 58 | minY: 0, | 80 | minY: 0, |
| 59 | maxY: 80, | 81 | maxY: 80, |
| 60 | - clipData: const FlClipData.all(), | 82 | + // clipData: const FlClipData.all(), |
| 61 | gridData: FlGridData( | 83 | gridData: FlGridData( |
| 62 | show: true, | 84 | show: true, |
| 63 | drawVerticalLine: true, | 85 | drawVerticalLine: true, |
| @@ -118,7 +140,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -118,7 +140,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 118 | radius: 4, | 140 | radius: 4, |
| 119 | color: Colors.white, | 141 | color: Colors.white, |
| 120 | strokeWidth: 3, | 142 | strokeWidth: 3, |
| 121 | - strokeColor: _getDotStrokeColor(spot), | 143 | + strokeColor: _getColor(spot.y), |
| 122 | ), | 144 | ), |
| 123 | )) | 145 | )) |
| 124 | ], | 146 | ], |
| @@ -138,7 +160,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -138,7 +160,7 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 138 | radius: 5, | 160 | radius: 5, |
| 139 | color: Colors.white, | 161 | color: Colors.white, |
| 140 | strokeWidth: 3.5, | 162 | strokeWidth: 3.5, |
| 141 | - strokeColor: _getDotStrokeColor(spot), | 163 | + strokeColor: _getColor(spot.y), |
| 142 | ); | 164 | ); |
| 143 | }, | 165 | }, |
| 144 | ), | 166 | ), |
| @@ -168,45 +190,218 @@ class TodayHrvChartCard extends GetView<TodayController> { | @@ -168,45 +190,218 @@ class TodayHrvChartCard extends GetView<TodayController> { | ||
| 168 | ), | 190 | ), |
| 169 | ), | 191 | ), |
| 170 | ), | 192 | ), |
| 193 | + Padding( | ||
| 194 | + padding: const EdgeInsets.only(bottom: 14, top: 16), | ||
| 195 | + child: Row( | ||
| 196 | + children: [ | ||
| 197 | + const Text( | ||
| 198 | + '实时压力', | ||
| 199 | + style: TextStyle( | ||
| 200 | + color: _h2, | ||
| 201 | + fontSize: 14, | ||
| 202 | + fontWeight: FontWeight.w600, | ||
| 203 | + ), | ||
| 204 | + ), | ||
| 205 | + const SizedBox(width: 4), | ||
| 206 | + const Icon(Icons.info_outline, size: 14, color: _h3), | ||
| 207 | + const Spacer(), | ||
| 208 | + ], | ||
| 209 | + ), | ||
| 210 | + ), | ||
| 211 | + SizedBox( | ||
| 212 | + height: 174, | ||
| 213 | + child: Stack( | ||
| 214 | + children: [ | ||
| 215 | + BarChart( | ||
| 216 | + BarChartData( | ||
| 217 | + minY: 0, | ||
| 218 | + maxY: 100, | ||
| 219 | + groupsSpace: 1, | ||
| 220 | + alignment: BarChartAlignment.start, | ||
| 221 | + barGroups: List.generate( | ||
| 222 | + _mockStressByHour.length * 5, | ||
| 223 | + (i) => BarChartGroupData( | ||
| 224 | + x: i, | ||
| 225 | + barRods: [ | ||
| 226 | + BarChartRodData( | ||
| 227 | + toY: | ||
| 228 | + _mockStressByHour[i % _mockStressByHour.length], | ||
| 229 | + width: 2, | ||
| 230 | + color: _getColor(_mockStressByHour[ | ||
| 231 | + i % _mockStressByHour.length]), | ||
| 232 | + borderRadius: const BorderRadius.only( | ||
| 233 | + topLeft: Radius.circular(2), | ||
| 234 | + topRight: Radius.circular(2), | ||
| 235 | + ), | ||
| 236 | + ), | ||
| 237 | + ], | ||
| 238 | + ), | ||
| 239 | + ), | ||
| 240 | + gridData: FlGridData( | ||
| 241 | + show: true, | ||
| 242 | + drawVerticalLine: false, | ||
| 243 | + drawHorizontalLine: true, | ||
| 244 | + horizontalInterval: 25, | ||
| 245 | + getDrawingHorizontalLine: (_) => const FlLine( | ||
| 246 | + color: _h5, | ||
| 247 | + strokeWidth: 1, | ||
| 248 | + dashArray: [2, 2], | ||
| 249 | + ), | ||
| 250 | + ), | ||
| 251 | + borderData: FlBorderData(show: false), | ||
| 252 | + titlesData: FlTitlesData( | ||
| 253 | + topTitles: const AxisTitles( | ||
| 254 | + sideTitles: SideTitles(showTitles: false), | ||
| 255 | + ), | ||
| 256 | + rightTitles: AxisTitles( | ||
| 257 | + sideTitles: SideTitles( | ||
| 258 | + showTitles: true, | ||
| 259 | + getTitlesWidget: (val, _) { | ||
| 260 | + return Text( | ||
| 261 | + '${val.toInt()}', | ||
| 262 | + style: const TextStyle(fontSize: 10, color: _h3), | ||
| 263 | + ); | ||
| 264 | + }, | ||
| 265 | + )), | ||
| 266 | + leftTitles: const AxisTitles( | ||
| 267 | + sideTitles: SideTitles(showTitles: false), | ||
| 268 | + ), | ||
| 269 | + bottomTitles: AxisTitles( | ||
| 270 | + sideTitles: SideTitles( | ||
| 271 | + showTitles: true, | ||
| 272 | + reservedSize: 20, | ||
| 273 | + interval: 60, | ||
| 274 | + getTitlesWidget: (val, meta) { | ||
| 275 | + final labels = { | ||
| 276 | + 0: '00:00', | ||
| 277 | + 33: '06:00', | ||
| 278 | + 66: '12:00', | ||
| 279 | + 99: '18:00', | ||
| 280 | + }; | ||
| 281 | + final label = labels[val]; | ||
| 282 | + if (label == null) return const SizedBox.shrink(); | ||
| 283 | + return SideTitleWidget( | ||
| 284 | + meta: meta, | ||
| 285 | + child: Text( | ||
| 286 | + label, | ||
| 287 | + style: | ||
| 288 | + const TextStyle(fontSize: 10, color: _h3), | ||
| 289 | + ), | ||
| 290 | + ); | ||
| 291 | + }, | ||
| 292 | + ), | ||
| 293 | + ), | ||
| 294 | + ), | ||
| 295 | + barTouchData: BarTouchData( | ||
| 296 | + touchTooltipData: BarTouchTooltipData( | ||
| 297 | + tooltipRoundedRadius: 8, | ||
| 298 | + tooltipBorder: BorderSide.none, | ||
| 299 | + getTooltipColor: (touchedSpot) => Color(0xFFF3F3F3), | ||
| 300 | + tooltipPadding: EdgeInsets.only( | ||
| 301 | + left: 12, right: 12, top: 6, bottom: 5), | ||
| 302 | + getTooltipItem: (group, groupIndex, rod, rodIndex) { | ||
| 303 | + return BarTooltipItem( | ||
| 304 | + '', | ||
| 305 | + TextStyle( | ||
| 306 | + color: Colors.white, | ||
| 307 | + fontWeight: FontWeight.bold, | ||
| 308 | + ), | ||
| 309 | + children: _getTooltip(rod), | ||
| 310 | + textAlign: TextAlign.start, | ||
| 311 | + ); | ||
| 312 | + })), | ||
| 313 | + ), | ||
| 314 | + ), | ||
| 315 | + Container( | ||
| 316 | + width: 74, | ||
| 317 | + height: 160, | ||
| 318 | + decoration: BoxDecoration( | ||
| 319 | + gradient: LinearGradient( | ||
| 320 | + begin: Alignment(0.50, -0.00), | ||
| 321 | + end: Alignment(0.50, 1.00), | ||
| 322 | + colors: [ | ||
| 323 | + const Color(0x4C835DED), | ||
| 324 | + const Color(0x00845EEE) | ||
| 325 | + ], | ||
| 326 | + ), | ||
| 327 | + ), | ||
| 328 | + ) | ||
| 329 | + ], | ||
| 330 | + ), | ||
| 331 | + ), | ||
| 171 | ], | 332 | ], |
| 172 | ), | 333 | ), |
| 173 | ); | 334 | ); |
| 174 | } | 335 | } |
| 175 | 336 | ||
| 176 | - Color _getDotStrokeColor(FlSpot spot) { | ||
| 177 | - if (spot.y > 60) { | 337 | + Color _getColor(double value) { |
| 338 | + if (value > 60) { | ||
| 178 | return const Color(0xFF3BD49D); | 339 | return const Color(0xFF3BD49D); |
| 179 | - } else if (spot.y > 50) { | 340 | + } else if (value > 50) { |
| 180 | return const Color(0xFF7B9BFB); | 341 | return const Color(0xFF7B9BFB); |
| 181 | - } else if (spot.y > 30) { | 342 | + } else if (value > 30) { |
| 182 | return const Color(0xFFFF9A6E); | 343 | return const Color(0xFFFF9A6E); |
| 183 | - } else if (spot.y > 20) { | 344 | + } else if (value > 20) { |
| 184 | return const Color(0xFFFF5279); | 345 | return const Color(0xFFFF5279); |
| 185 | } else { | 346 | } else { |
| 186 | return const Color(0xFF7B9BFB); | 347 | return const Color(0xFF7B9BFB); |
| 187 | } | 348 | } |
| 188 | } | 349 | } |
| 189 | 350 | ||
| 190 | - String _getDotStrokeStatus(FlSpot spot) { | ||
| 191 | - if (spot.y > 60) { | 351 | + String _getStatus(double value) { |
| 352 | + if (value > 60) { | ||
| 192 | return '状态优秀'; | 353 | return '状态优秀'; |
| 193 | - } else if (spot.y > 50) { | 354 | + } else if (value > 50) { |
| 194 | return '状态良好'; | 355 | return '状态良好'; |
| 195 | - } else if (spot.y > 30) { | 356 | + } else if (value > 30) { |
| 196 | return '状态一般'; | 357 | return '状态一般'; |
| 197 | - } else if (spot.y > 20) { | 358 | + } else if (value > 20) { |
| 198 | return '状态较差'; | 359 | return '状态较差'; |
| 199 | } else { | 360 | } else { |
| 200 | return '状态极差'; | 361 | return '状态极差'; |
| 201 | } | 362 | } |
| 202 | } | 363 | } |
| 203 | 364 | ||
| 365 | + List<TextSpan>? _getTooltip(BarChartRodData rod) { | ||
| 366 | + return [ | ||
| 367 | + // WidgetSpan( | ||
| 368 | + // child: Image.asset('assets/images/home/today/ic_tooltip_dot.png', | ||
| 369 | + // width: 12, height: 12), | ||
| 370 | + // ), | ||
| 371 | + TextSpan( | ||
| 372 | + text: _getStatus(rod.toY), | ||
| 373 | + style: TextStyle( | ||
| 374 | + color: _getColor(rod.toY), | ||
| 375 | + fontWeight: FontWeight.bold, | ||
| 376 | + ), | ||
| 377 | + ), | ||
| 378 | + TextSpan( | ||
| 379 | + text: '\n', | ||
| 380 | + style: TextStyle( | ||
| 381 | + color: Colors.white, | ||
| 382 | + fontWeight: FontWeight.bold, | ||
| 383 | + ), | ||
| 384 | + ), | ||
| 385 | + TextSpan( | ||
| 386 | + text: '${rod.toY}ms.${rod.fromY}:00', | ||
| 387 | + style: TextStyle( | ||
| 388 | + color: Color(0xFF78787D), | ||
| 389 | + fontWeight: FontWeight.bold, | ||
| 390 | + ), | ||
| 391 | + ), | ||
| 392 | + ]; | ||
| 393 | + } | ||
| 394 | + | ||
| 204 | List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot) { | 395 | List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot) { |
| 205 | return [ | 396 | return [ |
| 397 | + // WidgetSpan( | ||
| 398 | + // child: Image.asset('assets/images/home/today/ic_tooltip_dot.png', | ||
| 399 | + // width: 12, height: 12), | ||
| 400 | + // ), | ||
| 206 | TextSpan( | 401 | TextSpan( |
| 207 | - text: _getDotStrokeStatus(flSpot), | 402 | + text: _getStatus(flSpot.y), |
| 208 | style: TextStyle( | 403 | style: TextStyle( |
| 209 | - color: _getDotStrokeColor(flSpot), | 404 | + color: _getColor(flSpot.y), |
| 210 | fontWeight: FontWeight.bold, | 405 | fontWeight: FontWeight.bold, |
| 211 | ), | 406 | ), |
| 212 | ), | 407 | ), |
| @@ -7,14 +7,12 @@ import '../../controllers/today_controller.dart'; | @@ -7,14 +7,12 @@ import '../../controllers/today_controller.dart'; | ||
| 7 | class TodayHrvNumberCard extends GetView<TodayController> { | 7 | class TodayHrvNumberCard extends GetView<TodayController> { |
| 8 | const TodayHrvNumberCard({super.key}); | 8 | const TodayHrvNumberCard({super.key}); |
| 9 | 9 | ||
| 10 | - static const _cardBg = Color(0xFFF7F7F9); | ||
| 11 | - | ||
| 12 | @override | 10 | @override |
| 13 | Widget build(BuildContext context) { | 11 | Widget build(BuildContext context) { |
| 14 | return Container( | 12 | return Container( |
| 15 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), | 13 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), |
| 16 | decoration: BoxDecoration( | 14 | decoration: BoxDecoration( |
| 17 | - color: _cardBg, | 15 | + color: Colors.white, |
| 18 | borderRadius: BorderRadius.circular(16), | 16 | borderRadius: BorderRadius.circular(16), |
| 19 | ), | 17 | ), |
| 20 | child: Obx(() => Row( | 18 | child: Obx(() => Row( |
| @@ -28,11 +26,11 @@ class TodayHrvNumberCard extends GetView<TodayController> { | @@ -28,11 +26,11 @@ class TodayHrvNumberCard extends GetView<TodayController> { | ||
| 28 | ), | 26 | ), |
| 29 | ), | 27 | ), |
| 30 | // 分割线 | 28 | // 分割线 |
| 31 | - Container( | ||
| 32 | - width: 1, | ||
| 33 | - height: 40, | ||
| 34 | - color: const Color(0xFFE0E0E0), | ||
| 35 | - ), | 29 | + // Container( |
| 30 | + // width: 1, | ||
| 31 | + // height: 40, | ||
| 32 | + // color: const Color(0xFFE0E0E0), | ||
| 33 | + // ), | ||
| 36 | // 心率 | 34 | // 心率 |
| 37 | Expanded( | 35 | Expanded( |
| 38 | child: _NumberItem( | 36 | child: _NumberItem( |
| 1 | +import 'package:fl_chart/fl_chart.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | +import 'package:get/get.dart'; | ||
| 4 | + | ||
| 5 | +import '../../controllers/trend/trend_types.dart'; | ||
| 6 | +import '../../controllers/trend/activity_controller.dart'; | ||
| 7 | +import 'trend_period_tab_bar.dart'; | ||
| 8 | +import 'trend_date_range_bar.dart'; | ||
| 9 | + | ||
| 10 | +/// 活动消耗趋势内容区(含第2、3层 Tab) | ||
| 11 | +class ActivityTrendView extends GetView<ActivityController> { | ||
| 12 | + const ActivityTrendView({super.key}); | ||
| 13 | + | ||
| 14 | + @override | ||
| 15 | + Widget build(BuildContext context) { | ||
| 16 | + return Column( | ||
| 17 | + children: [ | ||
| 18 | + TrendPeriodTabBar(controller: controller), | ||
| 19 | + TrendDateRangeBar(controller: controller), | ||
| 20 | + Expanded( | ||
| 21 | + child: Obx(() { | ||
| 22 | + if (controller.isLoading.value) { | ||
| 23 | + return const Center( | ||
| 24 | + child: CircularProgressIndicator( | ||
| 25 | + valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)), | ||
| 26 | + ), | ||
| 27 | + ); | ||
| 28 | + } | ||
| 29 | + return SingleChildScrollView( | ||
| 30 | + padding: const EdgeInsets.symmetric(horizontal: 16), | ||
| 31 | + child: Column( | ||
| 32 | + children: [ | ||
| 33 | + const SizedBox(height: 8), | ||
| 34 | + const _ActivitySummaryCard(), | ||
| 35 | + const SizedBox(height: 12), | ||
| 36 | + const _ActivityChartCard(), | ||
| 37 | + const SizedBox(height: 180), | ||
| 38 | + ], | ||
| 39 | + ), | ||
| 40 | + ); | ||
| 41 | + }), | ||
| 42 | + ), | ||
| 43 | + ], | ||
| 44 | + ); | ||
| 45 | + } | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +// ─── 摘要卡片 ───────────────────────────────────────── | ||
| 49 | +class _ActivitySummaryCard extends GetView<ActivityController> { | ||
| 50 | + const _ActivitySummaryCard(); | ||
| 51 | + | ||
| 52 | + static const _h1 = Color(0xFF0F0F11); | ||
| 53 | + static const _h3 = Color(0xFF999999); | ||
| 54 | + static const _activeColor = Color(0xFFFF9A6E); | ||
| 55 | + | ||
| 56 | + @override | ||
| 57 | + Widget build(BuildContext context) { | ||
| 58 | + return Container( | ||
| 59 | + padding: const EdgeInsets.all(20), | ||
| 60 | + decoration: BoxDecoration( | ||
| 61 | + color: Colors.white, | ||
| 62 | + borderRadius: BorderRadius.circular(16), | ||
| 63 | + ), | ||
| 64 | + child: Obx(() { | ||
| 65 | + final periodLabel = controller.currentPeriod.value.label; | ||
| 66 | + return Column( | ||
| 67 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 68 | + children: [ | ||
| 69 | + Row( | ||
| 70 | + children: [ | ||
| 71 | + const Text( | ||
| 72 | + '活动消耗趋势', | ||
| 73 | + style: TextStyle( | ||
| 74 | + color: _h1, | ||
| 75 | + fontSize: 16, | ||
| 76 | + fontWeight: FontWeight.w600, | ||
| 77 | + ), | ||
| 78 | + ), | ||
| 79 | + const SizedBox(width: 4), | ||
| 80 | + const Icon(Icons.info_outline, size: 14, color: _h3), | ||
| 81 | + ], | ||
| 82 | + ), | ||
| 83 | + const SizedBox(height: 16), | ||
| 84 | + Row( | ||
| 85 | + children: [ | ||
| 86 | + _StatItem( | ||
| 87 | + label: '本$periodLabel总消耗', | ||
| 88 | + value: controller.totalBurn.value, | ||
| 89 | + unit: 'kcal', | ||
| 90 | + color: _activeColor, | ||
| 91 | + ), | ||
| 92 | + const SizedBox(width: 24), | ||
| 93 | + _StatItem( | ||
| 94 | + label: '日均消耗', | ||
| 95 | + value: controller.averageBurn.value, | ||
| 96 | + unit: 'kcal', | ||
| 97 | + color: const Color(0xFF3BD49D), | ||
| 98 | + ), | ||
| 99 | + ], | ||
| 100 | + ), | ||
| 101 | + ], | ||
| 102 | + ); | ||
| 103 | + }), | ||
| 104 | + ); | ||
| 105 | + } | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +class _StatItem extends StatelessWidget { | ||
| 109 | + final String label; | ||
| 110 | + final String value; | ||
| 111 | + final String unit; | ||
| 112 | + final Color color; | ||
| 113 | + | ||
| 114 | + const _StatItem({ | ||
| 115 | + required this.label, | ||
| 116 | + required this.value, | ||
| 117 | + required this.unit, | ||
| 118 | + required this.color, | ||
| 119 | + }); | ||
| 120 | + | ||
| 121 | + static const _h2 = Color(0xFF78787D); | ||
| 122 | + | ||
| 123 | + @override | ||
| 124 | + Widget build(BuildContext context) { | ||
| 125 | + return Column( | ||
| 126 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 127 | + children: [ | ||
| 128 | + Text(label, style: const TextStyle(color: _h2, fontSize: 12)), | ||
| 129 | + const SizedBox(height: 4), | ||
| 130 | + Row( | ||
| 131 | + crossAxisAlignment: CrossAxisAlignment.end, | ||
| 132 | + children: [ | ||
| 133 | + Text( | ||
| 134 | + value, | ||
| 135 | + style: TextStyle( | ||
| 136 | + color: color, | ||
| 137 | + fontSize: 28, | ||
| 138 | + fontWeight: FontWeight.w600, | ||
| 139 | + height: 1.0, | ||
| 140 | + ), | ||
| 141 | + ), | ||
| 142 | + const SizedBox(width: 2), | ||
| 143 | + Padding( | ||
| 144 | + padding: const EdgeInsets.only(bottom: 3), | ||
| 145 | + child: Text( | ||
| 146 | + unit, | ||
| 147 | + style: TextStyle(color: color, fontSize: 12), | ||
| 148 | + ), | ||
| 149 | + ), | ||
| 150 | + ], | ||
| 151 | + ), | ||
| 152 | + ], | ||
| 153 | + ); | ||
| 154 | + } | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +// ─── 活动柱状图卡片 ──────────────────────────────────── | ||
| 158 | +class _ActivityChartCard extends GetView<ActivityController> { | ||
| 159 | + const _ActivityChartCard(); | ||
| 160 | + | ||
| 161 | + static const _h1 = Color(0xFF0F0F11); | ||
| 162 | + static const _h3 = Color(0xFF999999); | ||
| 163 | + static const _h5 = Color(0xFFCCCCCC); | ||
| 164 | + static const _activeColor = Color(0xFFFF9A6E); | ||
| 165 | + | ||
| 166 | + @override | ||
| 167 | + Widget build(BuildContext context) { | ||
| 168 | + return Container( | ||
| 169 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), | ||
| 170 | + decoration: BoxDecoration( | ||
| 171 | + color: Colors.white, | ||
| 172 | + borderRadius: BorderRadius.circular(16), | ||
| 173 | + ), | ||
| 174 | + child: Obx(() { | ||
| 175 | + final period = controller.currentPeriod.value; | ||
| 176 | + return Column( | ||
| 177 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 178 | + children: [ | ||
| 179 | + Text( | ||
| 180 | + '活动消耗${period.label}趋势图', | ||
| 181 | + style: const TextStyle( | ||
| 182 | + color: _h1, | ||
| 183 | + fontSize: 14, | ||
| 184 | + fontWeight: FontWeight.w600, | ||
| 185 | + ), | ||
| 186 | + ), | ||
| 187 | + const SizedBox(height: 16), | ||
| 188 | + SizedBox( | ||
| 189 | + height: 180, | ||
| 190 | + child: _buildChart(), | ||
| 191 | + ), | ||
| 192 | + ], | ||
| 193 | + ); | ||
| 194 | + }), | ||
| 195 | + ); | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + Widget _buildChart() { | ||
| 199 | + if (controller.chartData.isEmpty) { | ||
| 200 | + return const Center( | ||
| 201 | + child: CircularProgressIndicator( | ||
| 202 | + valueColor: AlwaysStoppedAnimation(Color(0xFFFF9A6E)), | ||
| 203 | + ), | ||
| 204 | + ); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + return BarChart( | ||
| 208 | + BarChartData( | ||
| 209 | + minY: 0, | ||
| 210 | + maxY: 600, | ||
| 211 | + groupsSpace: 8, | ||
| 212 | + alignment: BarChartAlignment.spaceAround, | ||
| 213 | + barGroups: List.generate( | ||
| 214 | + controller.chartData.length, | ||
| 215 | + (i) => BarChartGroupData( | ||
| 216 | + x: i, | ||
| 217 | + barRods: [ | ||
| 218 | + BarChartRodData( | ||
| 219 | + toY: controller.chartData[i], | ||
| 220 | + width: 20, | ||
| 221 | + color: _activeColor, | ||
| 222 | + borderRadius: const BorderRadius.only( | ||
| 223 | + topLeft: Radius.circular(4), | ||
| 224 | + topRight: Radius.circular(4), | ||
| 225 | + ), | ||
| 226 | + ), | ||
| 227 | + ], | ||
| 228 | + ), | ||
| 229 | + ), | ||
| 230 | + gridData: FlGridData( | ||
| 231 | + show: true, | ||
| 232 | + drawVerticalLine: false, | ||
| 233 | + horizontalInterval: 200, | ||
| 234 | + getDrawingHorizontalLine: (_) => const FlLine( | ||
| 235 | + color: _h5, | ||
| 236 | + strokeWidth: 1, | ||
| 237 | + dashArray: [2, 2], | ||
| 238 | + ), | ||
| 239 | + ), | ||
| 240 | + borderData: FlBorderData(show: false), | ||
| 241 | + titlesData: FlTitlesData( | ||
| 242 | + topTitles: const AxisTitles( | ||
| 243 | + sideTitles: SideTitles(showTitles: false)), | ||
| 244 | + rightTitles: const AxisTitles( | ||
| 245 | + sideTitles: SideTitles(showTitles: false)), | ||
| 246 | + leftTitles: AxisTitles( | ||
| 247 | + sideTitles: SideTitles( | ||
| 248 | + showTitles: true, | ||
| 249 | + reservedSize: 36, | ||
| 250 | + interval: 200, | ||
| 251 | + getTitlesWidget: (val, _) => Text( | ||
| 252 | + '${val.toInt()}', | ||
| 253 | + style: const TextStyle(fontSize: 10, color: _h3), | ||
| 254 | + ), | ||
| 255 | + ), | ||
| 256 | + ), | ||
| 257 | + bottomTitles: AxisTitles( | ||
| 258 | + sideTitles: SideTitles( | ||
| 259 | + showTitles: true, | ||
| 260 | + reservedSize: 20, | ||
| 261 | + getTitlesWidget: (val, _) { | ||
| 262 | + final i = val.toInt(); | ||
| 263 | + if (i < 0 || i >= controller.chartLabels.length) { | ||
| 264 | + return const SizedBox.shrink(); | ||
| 265 | + } | ||
| 266 | + return Text( | ||
| 267 | + controller.chartLabels[i], | ||
| 268 | + style: const TextStyle(fontSize: 10, color: _h3), | ||
| 269 | + ); | ||
| 270 | + }, | ||
| 271 | + ), | ||
| 272 | + ), | ||
| 273 | + ), | ||
| 274 | + ), | ||
| 275 | + ); | ||
| 276 | + } | ||
| 277 | +} |
| 1 | +import 'package:fl_chart/fl_chart.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | +import 'package:get/get.dart'; | ||
| 4 | + | ||
| 5 | +import '../../controllers/trend/trend_types.dart'; | ||
| 6 | +import '../../controllers/trend/hrv_controller.dart'; | ||
| 7 | +import 'trend_period_tab_bar.dart'; | ||
| 8 | +import 'trend_date_range_bar.dart'; | ||
| 9 | + | ||
| 10 | +/// HRV 趋势内容区(含第2、3层 Tab) | ||
| 11 | +class HrvTrendView extends GetView<HrvController> { | ||
| 12 | + const HrvTrendView({super.key}); | ||
| 13 | + | ||
| 14 | + @override | ||
| 15 | + Widget build(BuildContext context) { | ||
| 16 | + return Column( | ||
| 17 | + children: [ | ||
| 18 | + // 第2层:周/月/年 | ||
| 19 | + TrendPeriodTabBar(controller: controller), | ||
| 20 | + // 第3层:日期范围 | ||
| 21 | + TrendDateRangeBar(controller: controller), | ||
| 22 | + // 图表内容 | ||
| 23 | + Expanded( | ||
| 24 | + child: Obx(() { | ||
| 25 | + if (controller.isLoading.value) { | ||
| 26 | + return const Center( | ||
| 27 | + child: CircularProgressIndicator( | ||
| 28 | + valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)), | ||
| 29 | + ), | ||
| 30 | + ); | ||
| 31 | + } | ||
| 32 | + return SingleChildScrollView( | ||
| 33 | + padding: const EdgeInsets.symmetric(horizontal: 16), | ||
| 34 | + child: Column( | ||
| 35 | + children: [ | ||
| 36 | + const SizedBox(height: 8), | ||
| 37 | + const _HrvSummaryCard(), | ||
| 38 | + const SizedBox(height: 12), | ||
| 39 | + const _HrvChartCard(), | ||
| 40 | + const SizedBox(height: 180), | ||
| 41 | + ], | ||
| 42 | + ), | ||
| 43 | + ); | ||
| 44 | + }), | ||
| 45 | + ), | ||
| 46 | + ], | ||
| 47 | + ); | ||
| 48 | + } | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +// ─── 摘要卡片 ──────────────────────────────────────────── | ||
| 52 | +class _HrvSummaryCard extends GetView<HrvController> { | ||
| 53 | + const _HrvSummaryCard(); | ||
| 54 | + | ||
| 55 | + static const _h1 = Color(0xFF0F0F11); | ||
| 56 | + static const _h3 = Color(0xFF999999); | ||
| 57 | + static const _brandColor = Color(0xFF845EEE); | ||
| 58 | + | ||
| 59 | + @override | ||
| 60 | + Widget build(BuildContext context) { | ||
| 61 | + return Container( | ||
| 62 | + padding: const EdgeInsets.all(20), | ||
| 63 | + decoration: BoxDecoration( | ||
| 64 | + color: Colors.white, | ||
| 65 | + borderRadius: BorderRadius.circular(16), | ||
| 66 | + ), | ||
| 67 | + child: Obx(() { | ||
| 68 | + final periodLabel = controller.currentPeriod.value.label; | ||
| 69 | + return Column( | ||
| 70 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 71 | + children: [ | ||
| 72 | + Row( | ||
| 73 | + children: [ | ||
| 74 | + const Text( | ||
| 75 | + 'HRV趋势', | ||
| 76 | + style: TextStyle( | ||
| 77 | + color: _h1, | ||
| 78 | + fontSize: 16, | ||
| 79 | + fontWeight: FontWeight.w600, | ||
| 80 | + ), | ||
| 81 | + ), | ||
| 82 | + const SizedBox(width: 4), | ||
| 83 | + const Icon(Icons.info_outline, size: 14, color: _h3), | ||
| 84 | + ], | ||
| 85 | + ), | ||
| 86 | + const SizedBox(height: 16), | ||
| 87 | + Row( | ||
| 88 | + children: [ | ||
| 89 | + _StatItem( | ||
| 90 | + label: '本$periodLabel平均', | ||
| 91 | + value: controller.averageHrv.value, | ||
| 92 | + unit: 'ms', | ||
| 93 | + color: _brandColor, | ||
| 94 | + ), | ||
| 95 | + const SizedBox(width: 24), | ||
| 96 | + _StatItem( | ||
| 97 | + label: '较上$periodLabel', | ||
| 98 | + value: controller.changeHrv.value, | ||
| 99 | + unit: 'ms', | ||
| 100 | + color: const Color(0xFF3BD49D), | ||
| 101 | + ), | ||
| 102 | + ], | ||
| 103 | + ), | ||
| 104 | + ], | ||
| 105 | + ); | ||
| 106 | + }), | ||
| 107 | + ); | ||
| 108 | + } | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +class _StatItem extends StatelessWidget { | ||
| 112 | + final String label; | ||
| 113 | + final String value; | ||
| 114 | + final String unit; | ||
| 115 | + final Color color; | ||
| 116 | + | ||
| 117 | + const _StatItem({ | ||
| 118 | + required this.label, | ||
| 119 | + required this.value, | ||
| 120 | + required this.unit, | ||
| 121 | + required this.color, | ||
| 122 | + }); | ||
| 123 | + | ||
| 124 | + static const _h2 = Color(0xFF78787D); | ||
| 125 | + | ||
| 126 | + @override | ||
| 127 | + Widget build(BuildContext context) { | ||
| 128 | + return Column( | ||
| 129 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 130 | + children: [ | ||
| 131 | + Text( | ||
| 132 | + label, | ||
| 133 | + style: const TextStyle(color: _h2, fontSize: 12), | ||
| 134 | + ), | ||
| 135 | + const SizedBox(height: 4), | ||
| 136 | + Row( | ||
| 137 | + crossAxisAlignment: CrossAxisAlignment.end, | ||
| 138 | + children: [ | ||
| 139 | + Text( | ||
| 140 | + value, | ||
| 141 | + style: TextStyle( | ||
| 142 | + color: color, | ||
| 143 | + fontSize: 32, | ||
| 144 | + fontWeight: FontWeight.w600, | ||
| 145 | + height: 1.0, | ||
| 146 | + ), | ||
| 147 | + ), | ||
| 148 | + const SizedBox(width: 2), | ||
| 149 | + Padding( | ||
| 150 | + padding: const EdgeInsets.only(bottom: 4), | ||
| 151 | + child: Text( | ||
| 152 | + unit, | ||
| 153 | + style: TextStyle(color: color, fontSize: 14), | ||
| 154 | + ), | ||
| 155 | + ), | ||
| 156 | + ], | ||
| 157 | + ), | ||
| 158 | + ], | ||
| 159 | + ); | ||
| 160 | + } | ||
| 161 | +} | ||
| 162 | + | ||
| 163 | +// ─── HRV 折线图卡片 ────────────────────────────────────── | ||
| 164 | +class _HrvChartCard extends GetView<HrvController> { | ||
| 165 | + const _HrvChartCard(); | ||
| 166 | + | ||
| 167 | + static const _h1 = Color(0xFF0F0F11); | ||
| 168 | + static const _h3 = Color(0xFF999999); | ||
| 169 | + static const _h5 = Color(0xFFCCCCCC); | ||
| 170 | + static const _brandColor = Color(0xFF845EEE); | ||
| 171 | + | ||
| 172 | + @override | ||
| 173 | + Widget build(BuildContext context) { | ||
| 174 | + return Container( | ||
| 175 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), | ||
| 176 | + decoration: BoxDecoration( | ||
| 177 | + color: Colors.white, | ||
| 178 | + borderRadius: BorderRadius.circular(16), | ||
| 179 | + ), | ||
| 180 | + child: Obx(() { | ||
| 181 | + final period = controller.currentPeriod.value; | ||
| 182 | + return Column( | ||
| 183 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 184 | + children: [ | ||
| 185 | + Text( | ||
| 186 | + 'HRV ${period.label}趋势图', | ||
| 187 | + style: const TextStyle( | ||
| 188 | + color: _h1, | ||
| 189 | + fontSize: 14, | ||
| 190 | + fontWeight: FontWeight.w600, | ||
| 191 | + ), | ||
| 192 | + ), | ||
| 193 | + const SizedBox(height: 16), | ||
| 194 | + SizedBox( | ||
| 195 | + height: 180, | ||
| 196 | + child: _buildChart(period), | ||
| 197 | + ), | ||
| 198 | + ], | ||
| 199 | + ); | ||
| 200 | + }), | ||
| 201 | + ); | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + Widget _buildChart(TrendPeriod period) { | ||
| 205 | + if (controller.chartData.isEmpty) { | ||
| 206 | + return const Center( | ||
| 207 | + child: CircularProgressIndicator( | ||
| 208 | + valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)), | ||
| 209 | + ), | ||
| 210 | + ); | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + final spots = List.generate( | ||
| 214 | + controller.chartData.length, | ||
| 215 | + (i) => FlSpot(i.toDouble(), controller.chartData[i]), | ||
| 216 | + ); | ||
| 217 | + | ||
| 218 | + return LineChart( | ||
| 219 | + LineChartData( | ||
| 220 | + minX: 0, | ||
| 221 | + maxX: (controller.chartData.length - 1).toDouble(), | ||
| 222 | + minY: 20, | ||
| 223 | + maxY: 80, | ||
| 224 | + gridData: FlGridData( | ||
| 225 | + show: true, | ||
| 226 | + drawVerticalLine: false, | ||
| 227 | + horizontalInterval: 20, | ||
| 228 | + getDrawingHorizontalLine: (_) => const FlLine( | ||
| 229 | + color: _h5, | ||
| 230 | + strokeWidth: 1, | ||
| 231 | + dashArray: [2, 2], | ||
| 232 | + ), | ||
| 233 | + ), | ||
| 234 | + borderData: FlBorderData(show: false), | ||
| 235 | + titlesData: FlTitlesData( | ||
| 236 | + topTitles: | ||
| 237 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 238 | + rightTitles: | ||
| 239 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 240 | + leftTitles: AxisTitles( | ||
| 241 | + sideTitles: SideTitles( | ||
| 242 | + showTitles: true, | ||
| 243 | + reservedSize: 30, | ||
| 244 | + interval: 20, | ||
| 245 | + getTitlesWidget: (val, _) => Text( | ||
| 246 | + '${val.toInt()}', | ||
| 247 | + style: const TextStyle(fontSize: 10, color: _h3), | ||
| 248 | + ), | ||
| 249 | + ), | ||
| 250 | + ), | ||
| 251 | + bottomTitles: AxisTitles( | ||
| 252 | + sideTitles: SideTitles( | ||
| 253 | + showTitles: true, | ||
| 254 | + reservedSize: 20, | ||
| 255 | + getTitlesWidget: (val, _) { | ||
| 256 | + final i = val.toInt(); | ||
| 257 | + if (i < 0 || i >= controller.chartLabels.length) { | ||
| 258 | + return const SizedBox.shrink(); | ||
| 259 | + } | ||
| 260 | + return Text( | ||
| 261 | + controller.chartLabels[i], | ||
| 262 | + style: const TextStyle(fontSize: 10, color: _h3), | ||
| 263 | + ); | ||
| 264 | + }, | ||
| 265 | + ), | ||
| 266 | + ), | ||
| 267 | + ), | ||
| 268 | + lineBarsData: [ | ||
| 269 | + LineChartBarData( | ||
| 270 | + spots: spots, | ||
| 271 | + isCurved: true, | ||
| 272 | + curveSmoothness: 0.3, | ||
| 273 | + color: _brandColor, | ||
| 274 | + barWidth: 2, | ||
| 275 | + belowBarData: BarAreaData( | ||
| 276 | + show: true, | ||
| 277 | + gradient: LinearGradient( | ||
| 278 | + begin: Alignment.topCenter, | ||
| 279 | + end: Alignment.bottomCenter, | ||
| 280 | + colors: [ | ||
| 281 | + _brandColor.withValues(alpha: 0.15), | ||
| 282 | + _brandColor.withValues(alpha: 0.0), | ||
| 283 | + ], | ||
| 284 | + ), | ||
| 285 | + ), | ||
| 286 | + dotData: FlDotData( | ||
| 287 | + getDotPainter: (spot, percent, barData, index) => | ||
| 288 | + FlDotCirclePainter( | ||
| 289 | + radius: 4, | ||
| 290 | + color: Colors.white, | ||
| 291 | + strokeWidth: 2.5, | ||
| 292 | + strokeColor: _brandColor, | ||
| 293 | + ), | ||
| 294 | + ), | ||
| 295 | + ), | ||
| 296 | + ], | ||
| 297 | + lineTouchData: LineTouchData( | ||
| 298 | + touchTooltipData: LineTouchTooltipData( | ||
| 299 | + tooltipRoundedRadius: 8, | ||
| 300 | + getTooltipColor: (_) => const Color(0xFFF3F3F3), | ||
| 301 | + getTooltipItems: (spots) => spots.map((s) { | ||
| 302 | + return LineTooltipItem( | ||
| 303 | + '${s.y.toInt()}ms', | ||
| 304 | + const TextStyle( | ||
| 305 | + color: _brandColor, | ||
| 306 | + fontWeight: FontWeight.w600, | ||
| 307 | + fontSize: 12, | ||
| 308 | + ), | ||
| 309 | + ); | ||
| 310 | + }).toList(), | ||
| 311 | + ), | ||
| 312 | + ), | ||
| 313 | + ), | ||
| 314 | + ); | ||
| 315 | + } | ||
| 316 | +} |
| 1 | +import 'package:fl_chart/fl_chart.dart'; | ||
| 2 | +import 'package:flutter/material.dart'; | ||
| 3 | +import 'package:get/get.dart'; | ||
| 4 | + | ||
| 5 | +import '../../controllers/trend/trend_types.dart'; | ||
| 6 | +import '../../controllers/trend/sleep_controller.dart'; | ||
| 7 | +import 'trend_period_tab_bar.dart'; | ||
| 8 | +import 'trend_date_range_bar.dart'; | ||
| 9 | + | ||
| 10 | +/// 睡眠报告趋势内容区(含第2、3层 Tab) | ||
| 11 | +class SleepTrendView extends GetView<SleepController> { | ||
| 12 | + const SleepTrendView({super.key}); | ||
| 13 | + | ||
| 14 | + @override | ||
| 15 | + Widget build(BuildContext context) { | ||
| 16 | + return Column( | ||
| 17 | + children: [ | ||
| 18 | + TrendPeriodTabBar(controller: controller), | ||
| 19 | + TrendDateRangeBar(controller: controller), | ||
| 20 | + Expanded( | ||
| 21 | + child: Obx(() { | ||
| 22 | + if (controller.isLoading.value) { | ||
| 23 | + return const Center( | ||
| 24 | + child: CircularProgressIndicator( | ||
| 25 | + valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)), | ||
| 26 | + ), | ||
| 27 | + ); | ||
| 28 | + } | ||
| 29 | + return SingleChildScrollView( | ||
| 30 | + padding: const EdgeInsets.symmetric(horizontal: 16), | ||
| 31 | + child: Column( | ||
| 32 | + children: [ | ||
| 33 | + const SizedBox(height: 8), | ||
| 34 | + const _SleepSummaryCard(), | ||
| 35 | + const SizedBox(height: 12), | ||
| 36 | + const _SleepChartCard(), | ||
| 37 | + const SizedBox(height: 180), | ||
| 38 | + ], | ||
| 39 | + ), | ||
| 40 | + ); | ||
| 41 | + }), | ||
| 42 | + ), | ||
| 43 | + ], | ||
| 44 | + ); | ||
| 45 | + } | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +// ─── 摘要卡片 ───────────────────────────────────────── | ||
| 49 | +class _SleepSummaryCard extends GetView<SleepController> { | ||
| 50 | + const _SleepSummaryCard(); | ||
| 51 | + | ||
| 52 | + static const _h1 = Color(0xFF0F0F11); | ||
| 53 | + static const _h3 = Color(0xFF999999); | ||
| 54 | + static const _sleepColor = Color(0xFF7B9BFB); | ||
| 55 | + | ||
| 56 | + @override | ||
| 57 | + Widget build(BuildContext context) { | ||
| 58 | + return Container( | ||
| 59 | + padding: const EdgeInsets.all(20), | ||
| 60 | + decoration: BoxDecoration( | ||
| 61 | + color: Colors.white, | ||
| 62 | + borderRadius: BorderRadius.circular(16), | ||
| 63 | + ), | ||
| 64 | + child: Obx(() { | ||
| 65 | + final periodLabel = controller.currentPeriod.value.label; | ||
| 66 | + return Column( | ||
| 67 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 68 | + children: [ | ||
| 69 | + Row( | ||
| 70 | + children: [ | ||
| 71 | + const Text( | ||
| 72 | + '睡眠报告趋势', | ||
| 73 | + style: TextStyle( | ||
| 74 | + color: _h1, | ||
| 75 | + fontSize: 16, | ||
| 76 | + fontWeight: FontWeight.w600, | ||
| 77 | + ), | ||
| 78 | + ), | ||
| 79 | + const SizedBox(width: 4), | ||
| 80 | + const Icon(Icons.info_outline, size: 14, color: _h3), | ||
| 81 | + ], | ||
| 82 | + ), | ||
| 83 | + const SizedBox(height: 16), | ||
| 84 | + Row( | ||
| 85 | + children: [ | ||
| 86 | + _StatItem( | ||
| 87 | + label: '本$periodLabel均睡眠', | ||
| 88 | + value: controller.averageSleep.value, | ||
| 89 | + unit: 'h', | ||
| 90 | + color: _sleepColor, | ||
| 91 | + ), | ||
| 92 | + const SizedBox(width: 24), | ||
| 93 | + _StatItem( | ||
| 94 | + label: '深睡占比', | ||
| 95 | + value: controller.deepSleepRatio.value, | ||
| 96 | + unit: '%', | ||
| 97 | + color: const Color(0xFF845EEE), | ||
| 98 | + ), | ||
| 99 | + ], | ||
| 100 | + ), | ||
| 101 | + ], | ||
| 102 | + ); | ||
| 103 | + }), | ||
| 104 | + ); | ||
| 105 | + } | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +class _StatItem extends StatelessWidget { | ||
| 109 | + final String label; | ||
| 110 | + final String value; | ||
| 111 | + final String unit; | ||
| 112 | + final Color color; | ||
| 113 | + | ||
| 114 | + const _StatItem({ | ||
| 115 | + required this.label, | ||
| 116 | + required this.value, | ||
| 117 | + required this.unit, | ||
| 118 | + required this.color, | ||
| 119 | + }); | ||
| 120 | + | ||
| 121 | + static const _h2 = Color(0xFF78787D); | ||
| 122 | + | ||
| 123 | + @override | ||
| 124 | + Widget build(BuildContext context) { | ||
| 125 | + return Column( | ||
| 126 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 127 | + children: [ | ||
| 128 | + Text(label, style: const TextStyle(color: _h2, fontSize: 12)), | ||
| 129 | + const SizedBox(height: 4), | ||
| 130 | + Row( | ||
| 131 | + crossAxisAlignment: CrossAxisAlignment.end, | ||
| 132 | + children: [ | ||
| 133 | + Text( | ||
| 134 | + value, | ||
| 135 | + style: TextStyle( | ||
| 136 | + color: color, | ||
| 137 | + fontSize: 32, | ||
| 138 | + fontWeight: FontWeight.w600, | ||
| 139 | + height: 1.0, | ||
| 140 | + ), | ||
| 141 | + ), | ||
| 142 | + const SizedBox(width: 2), | ||
| 143 | + Padding( | ||
| 144 | + padding: const EdgeInsets.only(bottom: 4), | ||
| 145 | + child: Text(unit, style: TextStyle(color: color, fontSize: 14)), | ||
| 146 | + ), | ||
| 147 | + ], | ||
| 148 | + ), | ||
| 149 | + ], | ||
| 150 | + ); | ||
| 151 | + } | ||
| 152 | +} | ||
| 153 | + | ||
| 154 | +// ─── 睡眠时长折线图卡片 ──────────────────────────────── | ||
| 155 | +class _SleepChartCard extends GetView<SleepController> { | ||
| 156 | + const _SleepChartCard(); | ||
| 157 | + | ||
| 158 | + static const _h1 = Color(0xFF0F0F11); | ||
| 159 | + static const _h3 = Color(0xFF999999); | ||
| 160 | + static const _h5 = Color(0xFFCCCCCC); | ||
| 161 | + static const _sleepColor = Color(0xFF7B9BFB); | ||
| 162 | + | ||
| 163 | + @override | ||
| 164 | + Widget build(BuildContext context) { | ||
| 165 | + return Container( | ||
| 166 | + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), | ||
| 167 | + decoration: BoxDecoration( | ||
| 168 | + color: Colors.white, | ||
| 169 | + borderRadius: BorderRadius.circular(16), | ||
| 170 | + ), | ||
| 171 | + child: Obx(() { | ||
| 172 | + final period = controller.currentPeriod.value; | ||
| 173 | + return Column( | ||
| 174 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 175 | + children: [ | ||
| 176 | + Text( | ||
| 177 | + '睡眠时长${period.label}趋势图', | ||
| 178 | + style: const TextStyle( | ||
| 179 | + color: _h1, | ||
| 180 | + fontSize: 14, | ||
| 181 | + fontWeight: FontWeight.w600, | ||
| 182 | + ), | ||
| 183 | + ), | ||
| 184 | + const SizedBox(height: 16), | ||
| 185 | + SizedBox( | ||
| 186 | + height: 180, | ||
| 187 | + child: _buildChart(), | ||
| 188 | + ), | ||
| 189 | + ], | ||
| 190 | + ); | ||
| 191 | + }), | ||
| 192 | + ); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + Widget _buildChart() { | ||
| 196 | + if (controller.chartData.isEmpty) { | ||
| 197 | + return const Center( | ||
| 198 | + child: CircularProgressIndicator( | ||
| 199 | + valueColor: AlwaysStoppedAnimation(Color(0xFF7B9BFB)), | ||
| 200 | + ), | ||
| 201 | + ); | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + return LineChart( | ||
| 205 | + LineChartData( | ||
| 206 | + minX: 0, | ||
| 207 | + maxX: (controller.chartData.length - 1).toDouble(), | ||
| 208 | + minY: 4, | ||
| 209 | + maxY: 10, | ||
| 210 | + gridData: FlGridData( | ||
| 211 | + show: true, | ||
| 212 | + drawVerticalLine: false, | ||
| 213 | + horizontalInterval: 2, | ||
| 214 | + getDrawingHorizontalLine: (_) => const FlLine( | ||
| 215 | + color: _h5, | ||
| 216 | + strokeWidth: 1, | ||
| 217 | + dashArray: [2, 2], | ||
| 218 | + ), | ||
| 219 | + ), | ||
| 220 | + borderData: FlBorderData(show: false), | ||
| 221 | + titlesData: FlTitlesData( | ||
| 222 | + topTitles: | ||
| 223 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 224 | + rightTitles: | ||
| 225 | + const AxisTitles(sideTitles: SideTitles(showTitles: false)), | ||
| 226 | + leftTitles: AxisTitles( | ||
| 227 | + sideTitles: SideTitles( | ||
| 228 | + showTitles: true, | ||
| 229 | + reservedSize: 30, | ||
| 230 | + interval: 2, | ||
| 231 | + getTitlesWidget: (val, _) => Text( | ||
| 232 | + '${val.toInt()}h', | ||
| 233 | + style: const TextStyle(fontSize: 10, color: _h3), | ||
| 234 | + ), | ||
| 235 | + ), | ||
| 236 | + ), | ||
| 237 | + bottomTitles: AxisTitles( | ||
| 238 | + sideTitles: SideTitles( | ||
| 239 | + showTitles: true, | ||
| 240 | + reservedSize: 20, | ||
| 241 | + getTitlesWidget: (val, _) { | ||
| 242 | + final i = val.toInt(); | ||
| 243 | + if (i < 0 || i >= controller.chartLabels.length) { | ||
| 244 | + return const SizedBox.shrink(); | ||
| 245 | + } | ||
| 246 | + return Text( | ||
| 247 | + controller.chartLabels[i], | ||
| 248 | + style: const TextStyle(fontSize: 10, color: _h3), | ||
| 249 | + ); | ||
| 250 | + }, | ||
| 251 | + ), | ||
| 252 | + ), | ||
| 253 | + ), | ||
| 254 | + lineBarsData: [ | ||
| 255 | + LineChartBarData( | ||
| 256 | + spots: List.generate( | ||
| 257 | + controller.chartData.length, | ||
| 258 | + (i) => FlSpot(i.toDouble(), controller.chartData[i]), | ||
| 259 | + ), | ||
| 260 | + isCurved: true, | ||
| 261 | + curveSmoothness: 0.3, | ||
| 262 | + color: _sleepColor, | ||
| 263 | + barWidth: 2, | ||
| 264 | + belowBarData: BarAreaData( | ||
| 265 | + show: true, | ||
| 266 | + gradient: LinearGradient( | ||
| 267 | + begin: Alignment.topCenter, | ||
| 268 | + end: Alignment.bottomCenter, | ||
| 269 | + colors: [ | ||
| 270 | + _sleepColor.withValues(alpha: 0.15), | ||
| 271 | + _sleepColor.withValues(alpha: 0.0), | ||
| 272 | + ], | ||
| 273 | + ), | ||
| 274 | + ), | ||
| 275 | + dotData: FlDotData( | ||
| 276 | + getDotPainter: (spot, percent, barData, index) => | ||
| 277 | + FlDotCirclePainter( | ||
| 278 | + radius: 4, | ||
| 279 | + color: Colors.white, | ||
| 280 | + strokeWidth: 2.5, | ||
| 281 | + strokeColor: _sleepColor, | ||
| 282 | + ), | ||
| 283 | + ), | ||
| 284 | + ), | ||
| 285 | + ], | ||
| 286 | + lineTouchData: LineTouchData( | ||
| 287 | + touchTooltipData: LineTouchTooltipData( | ||
| 288 | + tooltipRoundedRadius: 8, | ||
| 289 | + getTooltipColor: (_) => const Color(0xFFF3F3F3), | ||
| 290 | + getTooltipItems: (spots) => spots.map((s) { | ||
| 291 | + return LineTooltipItem( | ||
| 292 | + '${s.y}h', | ||
| 293 | + const TextStyle( | ||
| 294 | + color: _sleepColor, | ||
| 295 | + fontWeight: FontWeight.w600, | ||
| 296 | + fontSize: 12, | ||
| 297 | + ), | ||
| 298 | + ); | ||
| 299 | + }).toList(), | ||
| 300 | + ), | ||
| 301 | + ), | ||
| 302 | + ), | ||
| 303 | + ); | ||
| 304 | + } | ||
| 305 | +} |
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:get/get.dart'; | ||
| 3 | + | ||
| 4 | +import '../../controllers/trend/trend_period_controller.dart'; | ||
| 5 | + | ||
| 6 | +/// 第3层:日期范围导航条 | ||
| 7 | +/// ← [5月4日 - 5月10日 ↓] → | ||
| 8 | +/// 点击左右箭头切换周期;点击中间日期打开日历选择器 | ||
| 9 | +class TrendDateRangeBar extends StatelessWidget { | ||
| 10 | + final TrendPeriodController controller; | ||
| 11 | + const TrendDateRangeBar({super.key, required this.controller}); | ||
| 12 | + | ||
| 13 | + static const _brandColor = Color(0xFF845EEE); | ||
| 14 | + static const _arrowActiveColor = Color(0xFF0F0F11); | ||
| 15 | + static const _arrowDisabledColor = Color(0xFFCCCCCC); | ||
| 16 | + | ||
| 17 | + @override | ||
| 18 | + Widget build(BuildContext context) { | ||
| 19 | + return Obx( | ||
| 20 | + () => SizedBox( | ||
| 21 | + height: 40, | ||
| 22 | + child: Padding( | ||
| 23 | + padding: const EdgeInsets.symmetric(horizontal: 4), | ||
| 24 | + child: Row( | ||
| 25 | + mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||
| 26 | + children: [ | ||
| 27 | + // ← 上一个周期 | ||
| 28 | + _ArrowButton( | ||
| 29 | + icon: Icons.chevron_left, | ||
| 30 | + color: _arrowActiveColor, | ||
| 31 | + onTap: controller.goToPrevious, | ||
| 32 | + ), | ||
| 33 | + | ||
| 34 | + // 日期文字 + 下拉箭头(点击打开日历) | ||
| 35 | + Expanded( | ||
| 36 | + child: GestureDetector( | ||
| 37 | + onTap: () => _showDatePicker(context), | ||
| 38 | + behavior: HitTestBehavior.opaque, | ||
| 39 | + child: Row( | ||
| 40 | + mainAxisAlignment: MainAxisAlignment.center, | ||
| 41 | + children: [ | ||
| 42 | + Text( | ||
| 43 | + controller.dateRangeLabel, | ||
| 44 | + style: const TextStyle( | ||
| 45 | + fontFamily: 'PingFang SC', | ||
| 46 | + fontSize: 14, | ||
| 47 | + fontWeight: FontWeight.w500, | ||
| 48 | + color: _brandColor, | ||
| 49 | + height: 1.4, | ||
| 50 | + ), | ||
| 51 | + ), | ||
| 52 | + const SizedBox(width: 4), | ||
| 53 | + const Icon( | ||
| 54 | + Icons.keyboard_arrow_down, | ||
| 55 | + size: 12, | ||
| 56 | + color: _brandColor, | ||
| 57 | + ), | ||
| 58 | + ], | ||
| 59 | + ), | ||
| 60 | + ), | ||
| 61 | + ), | ||
| 62 | + | ||
| 63 | + // → 下一个周期(到达今日则禁用) | ||
| 64 | + _ArrowButton( | ||
| 65 | + icon: Icons.chevron_right, | ||
| 66 | + color: controller.canGoNext | ||
| 67 | + ? _arrowActiveColor | ||
| 68 | + : _arrowDisabledColor, | ||
| 69 | + onTap: controller.canGoNext ? controller.goToNext : null, | ||
| 70 | + ), | ||
| 71 | + ], | ||
| 72 | + ), | ||
| 73 | + ), | ||
| 74 | + ), | ||
| 75 | + ); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + void _showDatePicker(BuildContext context) { | ||
| 79 | + // TODO: 弹出自定义日历选择器 | ||
| 80 | + // 这里先留空,后续实现 | ||
| 81 | + } | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +class _ArrowButton extends StatelessWidget { | ||
| 85 | + final IconData icon; | ||
| 86 | + final Color color; | ||
| 87 | + final VoidCallback? onTap; | ||
| 88 | + | ||
| 89 | + const _ArrowButton({ | ||
| 90 | + required this.icon, | ||
| 91 | + required this.color, | ||
| 92 | + required this.onTap, | ||
| 93 | + }); | ||
| 94 | + | ||
| 95 | + @override | ||
| 96 | + Widget build(BuildContext context) { | ||
| 97 | + return GestureDetector( | ||
| 98 | + onTap: onTap, | ||
| 99 | + behavior: HitTestBehavior.opaque, | ||
| 100 | + child: SizedBox( | ||
| 101 | + width: 40, | ||
| 102 | + height: 40, | ||
| 103 | + child: Icon(icon, size: 28, color: color), | ||
| 104 | + ), | ||
| 105 | + ); | ||
| 106 | + } | ||
| 107 | +} |
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:get/get.dart'; | ||
| 3 | + | ||
| 4 | +import '../../controllers/trend/trend_types.dart'; | ||
| 5 | +import '../../controllers/trend/trend_period_controller.dart'; | ||
| 6 | + | ||
| 7 | +/// 第2层:周 / 月 / 年 胶囊 Tab | ||
| 8 | +/// 设计:整体 rgba(132,94,238,0.2) 胶囊背景,选中项白色圆角块 | ||
| 9 | +/// 仅支持点击切换(不滑动,避免与第1层手势冲突) | ||
| 10 | +class TrendPeriodTabBar extends StatelessWidget { | ||
| 11 | + final TrendPeriodController controller; | ||
| 12 | + const TrendPeriodTabBar({super.key, required this.controller}); | ||
| 13 | + | ||
| 14 | + // Figma token | ||
| 15 | + static const _capsuleBg = Color(0x33845EEE); // rgba(132,94,238,0.2) | ||
| 16 | + static const _selectedBg = Colors.white; | ||
| 17 | + static const _h1 = Color(0xFF0F0F11); | ||
| 18 | + static const _h2 = Color(0xFF78787D); | ||
| 19 | + | ||
| 20 | + @override | ||
| 21 | + Widget build(BuildContext context) { | ||
| 22 | + return Padding( | ||
| 23 | + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||
| 24 | + child: Obx( | ||
| 25 | + () => Container( | ||
| 26 | + height: 36, | ||
| 27 | + decoration: BoxDecoration( | ||
| 28 | + color: _capsuleBg, | ||
| 29 | + borderRadius: BorderRadius.circular(25), | ||
| 30 | + ), | ||
| 31 | + padding: const EdgeInsets.all(4), | ||
| 32 | + child: Row( | ||
| 33 | + children: TrendPeriod.values.map((period) { | ||
| 34 | + final isSelected = controller.currentPeriod.value == period; | ||
| 35 | + return Expanded( | ||
| 36 | + child: GestureDetector( | ||
| 37 | + onTap: () => controller.changePeriod(period), | ||
| 38 | + behavior: HitTestBehavior.opaque, | ||
| 39 | + child: AnimatedContainer( | ||
| 40 | + duration: const Duration(milliseconds: 200), | ||
| 41 | + curve: Curves.easeInOut, | ||
| 42 | + decoration: BoxDecoration( | ||
| 43 | + color: isSelected ? _selectedBg : Colors.transparent, | ||
| 44 | + borderRadius: BorderRadius.circular(21), | ||
| 45 | + boxShadow: isSelected | ||
| 46 | + ? [ | ||
| 47 | + const BoxShadow( | ||
| 48 | + color: Color(0x14000000), | ||
| 49 | + blurRadius: 4, | ||
| 50 | + offset: Offset(0, 1), | ||
| 51 | + ) | ||
| 52 | + ] | ||
| 53 | + : null, | ||
| 54 | + ), | ||
| 55 | + alignment: Alignment.center, | ||
| 56 | + child: AnimatedDefaultTextStyle( | ||
| 57 | + duration: const Duration(milliseconds: 200), | ||
| 58 | + style: TextStyle( | ||
| 59 | + fontFamily: 'PingFang SC', | ||
| 60 | + fontSize: 14, | ||
| 61 | + fontWeight: FontWeight.w500, | ||
| 62 | + color: isSelected ? _h1 : _h2, | ||
| 63 | + height: 1.4, | ||
| 64 | + ), | ||
| 65 | + child: Text(period.label), | ||
| 66 | + ), | ||
| 67 | + ), | ||
| 68 | + ), | ||
| 69 | + ); | ||
| 70 | + }).toList(), | ||
| 71 | + ), | ||
| 72 | + ), | ||
| 73 | + ), | ||
| 74 | + ); | ||
| 75 | + } | ||
| 76 | +} |
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | + | ||
| 3 | +/// 第1层:HRV心率 / 活动消耗 / 睡眠报告 Tab | ||
| 4 | +/// 使用 Flutter 原生 TabBar 与 TabController 实现极其平滑、跟手的指示线滑动效果 | ||
| 5 | +class TrendTypeTabBar extends StatelessWidget { | ||
| 6 | + final TabController tabController; | ||
| 7 | + const TrendTypeTabBar({super.key, required this.tabController}); | ||
| 8 | + | ||
| 9 | + static const _selectedColor = Color(0xFF0F0F11); | ||
| 10 | + static const _unselectedColor = Color(0xFF78787D); | ||
| 11 | + | ||
| 12 | + @override | ||
| 13 | + Widget build(BuildContext context) { | ||
| 14 | + return SizedBox( | ||
| 15 | + height: 40, | ||
| 16 | + child: TabBar( | ||
| 17 | + controller: tabController, | ||
| 18 | + dividerColor: Colors.transparent, // 移除底部默认分割线 | ||
| 19 | + indicatorColor: _selectedColor, | ||
| 20 | + indicatorSize: TabBarIndicatorSize.tab, // 指示线与 Tab 等宽 | ||
| 21 | + indicatorWeight: 3, // 指示线粗细为 3px | ||
| 22 | + labelColor: _selectedColor, | ||
| 23 | + unselectedLabelColor: _unselectedColor, | ||
| 24 | + labelStyle: const TextStyle( | ||
| 25 | + fontFamily: 'PingFang SC', | ||
| 26 | + fontSize: 18, | ||
| 27 | + fontWeight: FontWeight.w600, | ||
| 28 | + ), | ||
| 29 | + unselectedLabelStyle: const TextStyle( | ||
| 30 | + fontFamily: 'PingFang SC', | ||
| 31 | + fontSize: 18, | ||
| 32 | + fontWeight: FontWeight.w500, | ||
| 33 | + ), | ||
| 34 | + tabs: const [ | ||
| 35 | + Tab(text: 'HRV心率'), | ||
| 36 | + Tab(text: '活动消耗'), | ||
| 37 | + Tab(text: '睡眠报告'), | ||
| 38 | + ], | ||
| 39 | + ), | ||
| 40 | + ); | ||
| 41 | + } | ||
| 42 | +} |
lib/core/theme/app_colors.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | + | ||
| 3 | +/// Raw hex color constants directly from Double Feel Figma Design System. | ||
| 4 | +/// Avoid using these directly in UI. Instead, use `context.colors` to access | ||
| 5 | +/// semantic colors that automatically adjust to light/dark themes. | ||
| 6 | +class AppColors { | ||
| 7 | + AppColors._(); | ||
| 8 | + | ||
| 9 | + // ========================================== | ||
| 10 | + // 品牌色 (Brand Colors) | ||
| 11 | + // ========================================== | ||
| 12 | + static const primary = Color(0xFF845EEE); | ||
| 13 | + | ||
| 14 | + // ========================================== | ||
| 15 | + // 字色与分割线 (Text & Dividers) | ||
| 16 | + // ========================================== | ||
| 17 | + static const textPrimary = Color(0xFF0F0F11); | ||
| 18 | + static const textSecondary = Color(0xFF78787D); | ||
| 19 | + static const textTertiary = Color(0xFFB0B0B6); | ||
| 20 | + static const disabled = Color(0xFFD0D0D2); | ||
| 21 | + static const border = Color(0xFFEBEBED); | ||
| 22 | + | ||
| 23 | + // ========================================== | ||
| 24 | + // 图表与功能色 (Chart & Features) | ||
| 25 | + // ========================================== | ||
| 26 | + static const chartPink = Color(0xFFFF5279); | ||
| 27 | + static const chartOrange = Color(0xFFFF9A6E); | ||
| 28 | + static const chartBlue = Color(0xFF7B9AFB); | ||
| 29 | + static const chartGreen = Color(0xFF3BD49D); | ||
| 30 | + static const chartPurple = Color(0xFFA084EF); | ||
| 31 | + static const chartMagenta = Color(0xFFF775E1); | ||
| 32 | + | ||
| 33 | + // ========================================== | ||
| 34 | + // 会员色 (VIP Colors) | ||
| 35 | + // ========================================== | ||
| 36 | + static const vipText = Color(0xFF0F0F11); | ||
| 37 | + static const vipGold = Color(0xFFFFCE51); | ||
| 38 | + static const vipPurple = Color(0xFF916DF5); | ||
| 39 | + | ||
| 40 | + // ========================================== | ||
| 41 | + // 辅助与背景色 (Auxiliary & Backgrounds) | ||
| 42 | + // ========================================== | ||
| 43 | + static const backgroundLight = Color(0xFFFAFAFE); | ||
| 44 | + static const brandBackgroundLight = Color(0xFFEAE3FF); | ||
| 45 | + | ||
| 46 | + // ========================================== | ||
| 47 | + // 渐变基础色 (Gradient Components) | ||
| 48 | + // ========================================== | ||
| 49 | + static const gradientStart = Color(0xFFECE3FF); | ||
| 50 | + static const gradientEnd = Color(0xFFF9F6FF); | ||
| 51 | +} |
lib/core/theme/app_colors_extension.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'app_colors.dart'; | ||
| 3 | + | ||
| 4 | +/// Semantic colors extension for Double Feel application. | ||
| 5 | +/// Exposes structured colors and gradients supporting Light and Dark modes. | ||
| 6 | +class AppColorsExtension extends ThemeExtension<AppColorsExtension> { | ||
| 7 | + // Brand Colors | ||
| 8 | + final Color primary; | ||
| 9 | + | ||
| 10 | + // Text & Border Colors | ||
| 11 | + final Color textPrimary; | ||
| 12 | + final Color textSecondary; | ||
| 13 | + final Color textTertiary; | ||
| 14 | + final Color disabled; | ||
| 15 | + final Color border; | ||
| 16 | + | ||
| 17 | + // Chart & Features Colors | ||
| 18 | + final Color chartPink; | ||
| 19 | + final Color chartOrange; | ||
| 20 | + final Color chartBlue; | ||
| 21 | + final Color chartGreen; | ||
| 22 | + final Color chartPurple; | ||
| 23 | + final Color chartMagenta; | ||
| 24 | + | ||
| 25 | + // VIP Colors | ||
| 26 | + final Color vipText; | ||
| 27 | + final Color vipGold; | ||
| 28 | + final Color vipPurple; | ||
| 29 | + | ||
| 30 | + // Auxiliary Colors | ||
| 31 | + final Color backgroundLight; | ||
| 32 | + final Color brandBackgroundLight; | ||
| 33 | + | ||
| 34 | + // Gradients | ||
| 35 | + final LinearGradient brandBackgroundGradient; | ||
| 36 | + | ||
| 37 | + const AppColorsExtension({ | ||
| 38 | + required this.primary, | ||
| 39 | + required this.textPrimary, | ||
| 40 | + required this.textSecondary, | ||
| 41 | + required this.textTertiary, | ||
| 42 | + required this.disabled, | ||
| 43 | + required this.border, | ||
| 44 | + required this.chartPink, | ||
| 45 | + required this.chartOrange, | ||
| 46 | + required this.chartBlue, | ||
| 47 | + required this.chartGreen, | ||
| 48 | + required this.chartPurple, | ||
| 49 | + required this.chartMagenta, | ||
| 50 | + required this.vipText, | ||
| 51 | + required this.vipGold, | ||
| 52 | + required this.vipPurple, | ||
| 53 | + required this.backgroundLight, | ||
| 54 | + required this.brandBackgroundLight, | ||
| 55 | + required this.brandBackgroundGradient, | ||
| 56 | + }); | ||
| 57 | + | ||
| 58 | + /// The standard light palette derived directly from Figma. | ||
| 59 | + factory AppColorsExtension.light() { | ||
| 60 | + return const AppColorsExtension( | ||
| 61 | + primary: AppColors.primary, | ||
| 62 | + textPrimary: AppColors.textPrimary, | ||
| 63 | + textSecondary: AppColors.textSecondary, | ||
| 64 | + textTertiary: AppColors.textTertiary, | ||
| 65 | + disabled: AppColors.disabled, | ||
| 66 | + border: AppColors.border, | ||
| 67 | + chartPink: AppColors.chartPink, | ||
| 68 | + chartOrange: AppColors.chartOrange, | ||
| 69 | + chartBlue: AppColors.chartBlue, | ||
| 70 | + chartGreen: AppColors.chartGreen, | ||
| 71 | + chartPurple: AppColors.chartPurple, | ||
| 72 | + chartMagenta: AppColors.chartMagenta, | ||
| 73 | + vipText: AppColors.vipText, | ||
| 74 | + vipGold: AppColors.vipGold, | ||
| 75 | + vipPurple: AppColors.vipPurple, | ||
| 76 | + backgroundLight: AppColors.backgroundLight, | ||
| 77 | + brandBackgroundLight: AppColors.brandBackgroundLight, | ||
| 78 | + brandBackgroundGradient: LinearGradient( | ||
| 79 | + colors: [AppColors.gradientStart, AppColors.gradientEnd], | ||
| 80 | + begin: Alignment.centerLeft, | ||
| 81 | + end: Alignment.centerRight, | ||
| 82 | + ), | ||
| 83 | + ); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /// Initial dark mode mapping. Semantic mappings adapt nicely when Dark mode is toggled. | ||
| 87 | + factory AppColorsExtension.dark() { | ||
| 88 | + return const AppColorsExtension( | ||
| 89 | + primary: AppColors.primary, | ||
| 90 | + textPrimary: Color(0xFFEEEEEE), | ||
| 91 | + textSecondary: Color(0xFFAAAAAA), | ||
| 92 | + textTertiary: Color(0xFF666666), | ||
| 93 | + disabled: Color(0xFF444444), | ||
| 94 | + border: Color(0xFF333333), | ||
| 95 | + chartPink: AppColors.chartPink, | ||
| 96 | + chartOrange: AppColors.chartOrange, | ||
| 97 | + chartBlue: AppColors.chartBlue, | ||
| 98 | + chartGreen: AppColors.chartGreen, | ||
| 99 | + chartPurple: AppColors.chartPurple, | ||
| 100 | + chartMagenta: AppColors.chartMagenta, | ||
| 101 | + vipText: Color(0xFFEEEEEE), | ||
| 102 | + vipGold: AppColors.vipGold, | ||
| 103 | + vipPurple: AppColors.vipPurple, | ||
| 104 | + backgroundLight: Color(0xFF121214), | ||
| 105 | + brandBackgroundLight: Color(0xFF241C35), | ||
| 106 | + brandBackgroundGradient: LinearGradient( | ||
| 107 | + colors: [Color(0xFF241C35), Color(0xFF1A1525)], | ||
| 108 | + begin: Alignment.centerLeft, | ||
| 109 | + end: Alignment.centerRight, | ||
| 110 | + ), | ||
| 111 | + ); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + @override | ||
| 115 | + AppColorsExtension copyWith({ | ||
| 116 | + Color? primary, | ||
| 117 | + Color? textPrimary, | ||
| 118 | + Color? textSecondary, | ||
| 119 | + Color? textTertiary, | ||
| 120 | + Color? disabled, | ||
| 121 | + Color? border, | ||
| 122 | + Color? chartPink, | ||
| 123 | + Color? chartOrange, | ||
| 124 | + Color? chartBlue, | ||
| 125 | + Color? chartGreen, | ||
| 126 | + Color? chartPurple, | ||
| 127 | + Color? chartMagenta, | ||
| 128 | + Color? vipText, | ||
| 129 | + Color? vipGold, | ||
| 130 | + Color? vipPurple, | ||
| 131 | + Color? backgroundLight, | ||
| 132 | + Color? brandBackgroundLight, | ||
| 133 | + LinearGradient? brandBackgroundGradient, | ||
| 134 | + }) { | ||
| 135 | + return AppColorsExtension( | ||
| 136 | + primary: primary ?? this.primary, | ||
| 137 | + textPrimary: textPrimary ?? this.textPrimary, | ||
| 138 | + textSecondary: textSecondary ?? this.textSecondary, | ||
| 139 | + textTertiary: textTertiary ?? this.textTertiary, | ||
| 140 | + disabled: disabled ?? this.disabled, | ||
| 141 | + border: border ?? this.border, | ||
| 142 | + chartPink: chartPink ?? this.chartPink, | ||
| 143 | + chartOrange: chartOrange ?? this.chartOrange, | ||
| 144 | + chartBlue: chartBlue ?? this.chartBlue, | ||
| 145 | + chartGreen: chartGreen ?? this.chartGreen, | ||
| 146 | + chartPurple: chartPurple ?? this.chartPurple, | ||
| 147 | + chartMagenta: chartMagenta ?? this.chartMagenta, | ||
| 148 | + vipText: vipText ?? this.vipText, | ||
| 149 | + vipGold: vipGold ?? this.vipGold, | ||
| 150 | + vipPurple: vipPurple ?? this.vipPurple, | ||
| 151 | + backgroundLight: backgroundLight ?? this.backgroundLight, | ||
| 152 | + brandBackgroundLight: brandBackgroundLight ?? this.brandBackgroundLight, | ||
| 153 | + brandBackgroundGradient: brandBackgroundGradient ?? this.brandBackgroundGradient, | ||
| 154 | + ); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + @override | ||
| 158 | + AppColorsExtension lerp(ThemeExtension<AppColorsExtension>? other, double t) { | ||
| 159 | + if (other is! AppColorsExtension) { | ||
| 160 | + return this; | ||
| 161 | + } | ||
| 162 | + return AppColorsExtension( | ||
| 163 | + primary: Color.lerp(primary, other.primary, t)!, | ||
| 164 | + textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!, | ||
| 165 | + textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!, | ||
| 166 | + textTertiary: Color.lerp(textTertiary, other.textTertiary, t)!, | ||
| 167 | + disabled: Color.lerp(disabled, other.disabled, t)!, | ||
| 168 | + border: Color.lerp(border, other.border, t)!, | ||
| 169 | + chartPink: Color.lerp(chartPink, other.chartPink, t)!, | ||
| 170 | + chartOrange: Color.lerp(chartOrange, other.chartOrange, t)!, | ||
| 171 | + chartBlue: Color.lerp(chartBlue, other.chartBlue, t)!, | ||
| 172 | + chartGreen: Color.lerp(chartGreen, other.chartGreen, t)!, | ||
| 173 | + chartPurple: Color.lerp(chartPurple, other.chartPurple, t)!, | ||
| 174 | + chartMagenta: Color.lerp(chartMagenta, other.chartMagenta, t)!, | ||
| 175 | + vipText: Color.lerp(vipText, other.vipText, t)!, | ||
| 176 | + vipGold: Color.lerp(vipGold, other.vipGold, t)!, | ||
| 177 | + vipPurple: Color.lerp(vipPurple, other.vipPurple, t)!, | ||
| 178 | + backgroundLight: Color.lerp(backgroundLight, other.backgroundLight, t)!, | ||
| 179 | + brandBackgroundLight: Color.lerp(brandBackgroundLight, other.brandBackgroundLight, t)!, | ||
| 180 | + brandBackgroundGradient: LinearGradient.lerp(brandBackgroundGradient, other.brandBackgroundGradient, t)!, | ||
| 181 | + ); | ||
| 182 | + } | ||
| 183 | +} |
lib/core/theme/app_theme.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'app_colors_extension.dart'; | ||
| 3 | + | ||
| 4 | +/// Class managing the Application ThemeData for both Light and Dark themes. | ||
| 5 | +/// Automatically hooks up our custom Figma colors system as a ThemeExtension. | ||
| 6 | +class AppTheme { | ||
| 7 | + AppTheme._(); | ||
| 8 | + | ||
| 9 | + /// The standard light theme configuration. | ||
| 10 | + static ThemeData get lightTheme { | ||
| 11 | + final colors = AppColorsExtension.light(); | ||
| 12 | + return ThemeData( | ||
| 13 | + useMaterial3: true, | ||
| 14 | + brightness: Brightness.light, | ||
| 15 | + primaryColor: colors.primary, | ||
| 16 | + scaffoldBackgroundColor: colors.backgroundLight, | ||
| 17 | + | ||
| 18 | + // Clean modern AppBar theme using Figma colors | ||
| 19 | + appBarTheme: AppBarTheme( | ||
| 20 | + backgroundColor: colors.backgroundLight, | ||
| 21 | + elevation: 0, | ||
| 22 | + centerTitle: true, | ||
| 23 | + iconTheme: IconThemeData(color: colors.textPrimary), | ||
| 24 | + actionsIconTheme: IconThemeData(color: colors.textPrimary), | ||
| 25 | + titleTextStyle: TextStyle( | ||
| 26 | + color: colors.textPrimary, | ||
| 27 | + fontSize: 18, | ||
| 28 | + fontWeight: FontWeight.w600, | ||
| 29 | + ), | ||
| 30 | + ), | ||
| 31 | + | ||
| 32 | + // Configure default ColorScheme using Figma specs | ||
| 33 | + colorScheme: ColorScheme.light( | ||
| 34 | + primary: colors.primary, | ||
| 35 | + secondary: colors.primary, | ||
| 36 | + surface: colors.backgroundLight, | ||
| 37 | + onPrimary: Colors.white, | ||
| 38 | + onSecondary: Colors.white, | ||
| 39 | + onSurface: colors.textPrimary, | ||
| 40 | + outline: colors.border, | ||
| 41 | + ), | ||
| 42 | + | ||
| 43 | + // Register the custom theme extension | ||
| 44 | + extensions: [ | ||
| 45 | + colors, | ||
| 46 | + ], | ||
| 47 | + ); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + /// Initial dark theme configuration. | ||
| 51 | + static ThemeData get darkTheme { | ||
| 52 | + final colors = AppColorsExtension.dark(); | ||
| 53 | + return ThemeData( | ||
| 54 | + useMaterial3: true, | ||
| 55 | + brightness: Brightness.dark, | ||
| 56 | + primaryColor: colors.primary, | ||
| 57 | + scaffoldBackgroundColor: colors.backgroundLight, | ||
| 58 | + | ||
| 59 | + appBarTheme: AppBarTheme( | ||
| 60 | + backgroundColor: colors.backgroundLight, | ||
| 61 | + elevation: 0, | ||
| 62 | + centerTitle: true, | ||
| 63 | + iconTheme: IconThemeData(color: colors.textPrimary), | ||
| 64 | + actionsIconTheme: IconThemeData(color: colors.textPrimary), | ||
| 65 | + titleTextStyle: TextStyle( | ||
| 66 | + color: colors.textPrimary, | ||
| 67 | + fontSize: 18, | ||
| 68 | + fontWeight: FontWeight.w600, | ||
| 69 | + ), | ||
| 70 | + ), | ||
| 71 | + | ||
| 72 | + colorScheme: ColorScheme.dark( | ||
| 73 | + primary: colors.primary, | ||
| 74 | + secondary: colors.primary, | ||
| 75 | + surface: colors.backgroundLight, | ||
| 76 | + onPrimary: Colors.white, | ||
| 77 | + onSecondary: Colors.white, | ||
| 78 | + onSurface: colors.textPrimary, | ||
| 79 | + outline: colors.border, | ||
| 80 | + ), | ||
| 81 | + | ||
| 82 | + extensions: [ | ||
| 83 | + colors, | ||
| 84 | + ], | ||
| 85 | + ); | ||
| 86 | + } | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +/// Helper extension to easily access custom Figma colors inside widgets | ||
| 90 | +/// by using `context.colors.<semanticName>` instead of verbose lookups. | ||
| 91 | +extension AppThemeContextExtension on BuildContext { | ||
| 92 | + AppColorsExtension get colors => Theme.of(this).extension<AppColorsExtension>()!; | ||
| 93 | + ThemeData get theme => Theme.of(this); | ||
| 94 | +} |
| @@ -62,8 +62,7 @@ import 'app_localizations_zh.dart'; | @@ -62,8 +62,7 @@ import 'app_localizations_zh.dart'; | ||
| 62 | /// be consistent with the languages listed in the AppLocalizations.supportedLocales | 62 | /// be consistent with the languages listed in the AppLocalizations.supportedLocales |
| 63 | /// property. | 63 | /// property. |
| 64 | abstract class AppLocalizations { | 64 | abstract class AppLocalizations { |
| 65 | - AppLocalizations(String locale) | ||
| 66 | - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); | 65 | + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); |
| 67 | 66 | ||
| 68 | final String localeName; | 67 | final String localeName; |
| 69 | 68 | ||
| @@ -71,8 +70,7 @@ abstract class AppLocalizations { | @@ -71,8 +70,7 @@ abstract class AppLocalizations { | ||
| 71 | return Localizations.of<AppLocalizations>(context, AppLocalizations); | 70 | return Localizations.of<AppLocalizations>(context, AppLocalizations); |
| 72 | } | 71 | } |
| 73 | 72 | ||
| 74 | - static const LocalizationsDelegate<AppLocalizations> delegate = | ||
| 75 | - _AppLocalizationsDelegate(); | 73 | + static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate(); |
| 76 | 74 | ||
| 77 | /// A list of this localizations delegate along with the default localizations | 75 | /// A list of this localizations delegate along with the default localizations |
| 78 | /// delegates. | 76 | /// delegates. |
| @@ -84,8 +82,7 @@ abstract class AppLocalizations { | @@ -84,8 +82,7 @@ abstract class AppLocalizations { | ||
| 84 | /// Additional delegates can be added by appending to this list in | 82 | /// Additional delegates can be added by appending to this list in |
| 85 | /// MaterialApp. This list does not have to be used at all if a custom list | 83 | /// MaterialApp. This list does not have to be used at all if a custom list |
| 86 | /// of delegates is preferred or required. | 84 | /// of delegates is preferred or required. |
| 87 | - static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = | ||
| 88 | - <LocalizationsDelegate<dynamic>>[ | 85 | + static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[ |
| 89 | delegate, | 86 | delegate, |
| 90 | GlobalMaterialLocalizations.delegate, | 87 | GlobalMaterialLocalizations.delegate, |
| 91 | GlobalCupertinoLocalizations.delegate, | 88 | GlobalCupertinoLocalizations.delegate, |
| @@ -528,8 +525,7 @@ abstract class AppLocalizations { | @@ -528,8 +525,7 @@ abstract class AppLocalizations { | ||
| 528 | /// | 525 | /// |
| 529 | /// In zh, this message translates to: | 526 | /// In zh, this message translates to: |
| 530 | /// **'你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'** | 527 | /// **'你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'** |
| 531 | - String | ||
| 532 | - get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired; | 528 | + String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired; |
| 533 | 529 | ||
| 534 | /// No description provided for @bindPartnerTitle. | 530 | /// No description provided for @bindPartnerTitle. |
| 535 | /// | 531 | /// |
| @@ -610,8 +606,7 @@ abstract class AppLocalizations { | @@ -610,8 +606,7 @@ abstract class AppLocalizations { | ||
| 610 | String get onboardingResearchGoodSleep; | 606 | String get onboardingResearchGoodSleep; |
| 611 | } | 607 | } |
| 612 | 608 | ||
| 613 | -class _AppLocalizationsDelegate | ||
| 614 | - extends LocalizationsDelegate<AppLocalizations> { | 609 | +class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { |
| 615 | const _AppLocalizationsDelegate(); | 610 | const _AppLocalizationsDelegate(); |
| 616 | 611 | ||
| 617 | @override | 612 | @override |
| @@ -620,25 +615,25 @@ class _AppLocalizationsDelegate | @@ -620,25 +615,25 @@ class _AppLocalizationsDelegate | ||
| 620 | } | 615 | } |
| 621 | 616 | ||
| 622 | @override | 617 | @override |
| 623 | - bool isSupported(Locale locale) => | ||
| 624 | - <String>['en', 'zh'].contains(locale.languageCode); | 618 | + bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode); |
| 625 | 619 | ||
| 626 | @override | 620 | @override |
| 627 | bool shouldReload(_AppLocalizationsDelegate old) => false; | 621 | bool shouldReload(_AppLocalizationsDelegate old) => false; |
| 628 | } | 622 | } |
| 629 | 623 | ||
| 630 | AppLocalizations lookupAppLocalizations(Locale locale) { | 624 | AppLocalizations lookupAppLocalizations(Locale locale) { |
| 625 | + | ||
| 626 | + | ||
| 631 | // Lookup logic when only language code is specified. | 627 | // Lookup logic when only language code is specified. |
| 632 | switch (locale.languageCode) { | 628 | switch (locale.languageCode) { |
| 633 | - case 'en': | ||
| 634 | - return AppLocalizationsEn(); | ||
| 635 | - case 'zh': | ||
| 636 | - return AppLocalizationsZh(); | 629 | + case 'en': return AppLocalizationsEn(); |
| 630 | + case 'zh': return AppLocalizationsZh(); | ||
| 637 | } | 631 | } |
| 638 | 632 | ||
| 639 | throw FlutterError( | 633 | throw FlutterError( |
| 640 | - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' | ||
| 641 | - 'an issue with the localizations generation tool. Please file an issue ' | ||
| 642 | - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' | ||
| 643 | - 'that was used.'); | 634 | + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' |
| 635 | + 'an issue with the localizations generation tool. Please file an issue ' | ||
| 636 | + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' | ||
| 637 | + 'that was used.' | ||
| 638 | + ); | ||
| 644 | } | 639 | } |
| 1 | -// ignore: unused_import | ||
| 2 | -import 'package:intl/intl.dart' as intl; | ||
| 3 | import 'app_localizations.dart'; | 1 | import 'app_localizations.dart'; |
| 4 | 2 | ||
| 5 | // ignore_for_file: type=lint | 3 | // ignore_for_file: type=lint |
| @@ -69,12 +67,10 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -69,12 +67,10 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 69 | String get settings => 'Settings'; | 67 | String get settings => 'Settings'; |
| 70 | 68 | ||
| 71 | @override | 69 | @override |
| 72 | - String get onboardingIntroTitle => | ||
| 73 | - 'DoubleFeel is a health companion app built for Apple Watch'; | 70 | + String get onboardingIntroTitle => 'DoubleFeel is a health companion app built for Apple Watch'; |
| 74 | 71 | ||
| 75 | @override | 72 | @override |
| 76 | - String get onboardingIntroBody => | ||
| 77 | - 'We hope to help you\n<em>notice changes in your mind and body, and help the people who love you</em> see when you are <em>tired or need support</em>'; | 73 | + String get onboardingIntroBody => 'We hope to help you\n<em>notice changes in your mind and body, and help the people who love you</em> see when you are <em>tired or need support</em>'; |
| 78 | 74 | ||
| 79 | @override | 75 | @override |
| 80 | String get onboardingStateQuestion => 'Which of these often happens to you?'; | 76 | String get onboardingStateQuestion => 'Which of these often happens to you?'; |
| @@ -86,19 +82,16 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -86,19 +82,16 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 86 | String get onboardingStateTired => 'I get tired easily'; | 82 | String get onboardingStateTired => 'I get tired easily'; |
| 87 | 83 | ||
| 88 | @override | 84 | @override |
| 89 | - String get onboardingStatePoorRest => | ||
| 90 | - 'I wake up but still do not feel rested'; | 85 | + String get onboardingStatePoorRest => 'I wake up but still do not feel rested'; |
| 91 | 86 | ||
| 92 | @override | 87 | @override |
| 93 | - String get onboardingStateNeedStimulants => | ||
| 94 | - 'I rely on cigarettes, alcohol, coffee, or other stimulants to stay alert'; | 88 | + String get onboardingStateNeedStimulants => 'I rely on cigarettes, alcohol, coffee, or other stimulants to stay alert'; |
| 95 | 89 | ||
| 96 | @override | 90 | @override |
| 97 | String get onboardingStateNone => 'None of the above'; | 91 | String get onboardingStateNone => 'None of the above'; |
| 98 | 92 | ||
| 99 | @override | 93 | @override |
| 100 | - String get onboardingStressGoalQuestion => | ||
| 101 | - 'What do you want to learn by understanding stress?'; | 94 | + String get onboardingStressGoalQuestion => 'What do you want to learn by understanding stress?'; |
| 102 | 95 | ||
| 103 | @override | 96 | @override |
| 104 | String get onboardingStressGoalSource => 'Understand where stress comes from'; | 97 | String get onboardingStressGoalSource => 'Understand where stress comes from'; |
| @@ -107,8 +100,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -107,8 +100,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 107 | String get onboardingStressGoalReminder => 'Get reminded when stress appears'; | 100 | String get onboardingStressGoalReminder => 'Get reminded when stress appears'; |
| 108 | 101 | ||
| 109 | @override | 102 | @override |
| 110 | - String get onboardingStressGoalLovedOnes => | ||
| 111 | - 'Let people who care about me know my stress state'; | 103 | + String get onboardingStressGoalLovedOnes => 'Let people who care about me know my stress state'; |
| 112 | 104 | ||
| 113 | @override | 105 | @override |
| 114 | String get onboardingStressGoalRelax => 'Understand stress and feel lighter'; | 106 | String get onboardingStressGoalRelax => 'Understand stress and feel lighter'; |
| @@ -117,8 +109,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -117,8 +109,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 117 | String get onboardingStressGoalBodyTalk => 'Communicate better with my body'; | 109 | String get onboardingStressGoalBodyTalk => 'Communicate better with my body'; |
| 118 | 110 | ||
| 119 | @override | 111 | @override |
| 120 | - String get onboardingReliefQuestion => | ||
| 121 | - 'Which methods do you think can ease stress?'; | 112 | + String get onboardingReliefQuestion => 'Which methods do you think can ease stress?'; |
| 122 | 113 | ||
| 123 | @override | 114 | @override |
| 124 | String get onboardingReliefSleep => 'Regular sleep'; | 115 | String get onboardingReliefSleep => 'Regular sleep'; |
| @@ -142,8 +133,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -142,8 +133,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 142 | String get onboardingKeyDataTitle => 'Did you know?'; | 133 | String get onboardingKeyDataTitle => 'Did you know?'; |
| 143 | 134 | ||
| 144 | @override | 135 | @override |
| 145 | - String get onboardingKeyDataSubtitle => | ||
| 146 | - 'Everyone has a magical and important body metric that can help us:'; | 136 | + String get onboardingKeyDataSubtitle => 'Everyone has a magical and important body metric that can help us:'; |
| 147 | 137 | ||
| 148 | @override | 138 | @override |
| 149 | String get onboardingKeyDataStress => 'Monitor stress'; | 139 | String get onboardingKeyDataStress => 'Monitor stress'; |
| @@ -158,8 +148,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -158,8 +148,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 158 | String get onboardingKeyDataHabits => 'Build healthy habits'; | 148 | String get onboardingKeyDataHabits => 'Build healthy habits'; |
| 159 | 149 | ||
| 160 | @override | 150 | @override |
| 161 | - String get onboardingKeyDataLovedOnes => | ||
| 162 | - 'Help important people care about your state in time'; | 151 | + String get onboardingKeyDataLovedOnes => 'Help important people care about your state in time'; |
| 163 | 152 | ||
| 164 | @override | 153 | @override |
| 165 | String get onboardingTellMeWhatItIs => 'Tell me what it is!'; | 154 | String get onboardingTellMeWhatItIs => 'Tell me what it is!'; |
| @@ -168,19 +157,16 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -168,19 +157,16 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 168 | String get onboardingHrvTitle => 'It is HRV, heart rate variability'; | 157 | String get onboardingHrvTitle => 'It is HRV, heart rate variability'; |
| 169 | 158 | ||
| 170 | @override | 159 | @override |
| 171 | - String get onboardingHrvSubtitle => | ||
| 172 | - 'It helps us measure overall stress and health'; | 160 | + String get onboardingHrvSubtitle => 'It helps us measure overall stress and health'; |
| 173 | 161 | ||
| 174 | @override | 162 | @override |
| 175 | - String get onboardingHrvDescription => | ||
| 176 | - 'Heart rate variability (HRV) is the tiny variation in time between heartbeats. It reflects autonomic nervous system activity and how the body responds to stress.'; | 163 | + String get onboardingHrvDescription => 'Heart rate variability (HRV) is the tiny variation in time between heartbeats. It reflects autonomic nervous system activity and how the body responds to stress.'; |
| 177 | 164 | ||
| 178 | @override | 165 | @override |
| 179 | String get onboardingTellMeMore => 'Tell me more'; | 166 | String get onboardingTellMeMore => 'Tell me more'; |
| 180 | 167 | ||
| 181 | @override | 168 | @override |
| 182 | - String get onboardingResearchTitle => | ||
| 183 | - 'Many studies show that HRV changes are closely related to how our body and mind feel'; | 169 | + String get onboardingResearchTitle => 'Many studies show that HRV changes are closely related to how our body and mind feel'; |
| 184 | 170 | ||
| 185 | @override | 171 | @override |
| 186 | String get onboardingResearchFatigue => 'Physical fatigue'; | 172 | String get onboardingResearchFatigue => 'Physical fatigue'; |
| @@ -198,30 +184,25 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -198,30 +184,25 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 198 | String get onboardingHealthPermissionTitle => 'Allow health data access'; | 184 | String get onboardingHealthPermissionTitle => 'Allow health data access'; |
| 199 | 185 | ||
| 200 | @override | 186 | @override |
| 201 | - String get onboardingHealthPermissionBody => | ||
| 202 | - 'DoubleFeel needs connected wearable health data to send reminders, count stress moments, and provide suggestions.'; | 187 | + String get onboardingHealthPermissionBody => 'DoubleFeel needs connected wearable health data to send reminders, count stress moments, and provide suggestions.'; |
| 203 | 188 | ||
| 204 | @override | 189 | @override |
| 205 | - String get onboardingHealthPermissionPrivacy => | ||
| 206 | - 'Your health data is stored locally. We do not upload any related data.'; | 190 | + String get onboardingHealthPermissionPrivacy => 'Your health data is stored locally. We do not upload any related data.'; |
| 207 | 191 | ||
| 208 | @override | 192 | @override |
| 209 | String get onboardingNotificationTitle => 'Turn on notifications'; | 193 | String get onboardingNotificationTitle => 'Turn on notifications'; |
| 210 | 194 | ||
| 211 | @override | 195 | @override |
| 212 | - String get onboardingNotificationSubtitle => | ||
| 213 | - 'Learn about every body change in time'; | 196 | + String get onboardingNotificationSubtitle => 'Learn about every body change in time'; |
| 214 | 197 | ||
| 215 | @override | 198 | @override |
| 216 | - String get onboardingNotificationBody => | ||
| 217 | - 'After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.'; | 199 | + String get onboardingNotificationBody => 'After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.'; |
| 218 | 200 | ||
| 219 | @override | 201 | @override |
| 220 | String get onboardingMemberTitle => 'Get an annual membership offer'; | 202 | String get onboardingMemberTitle => 'Get an annual membership offer'; |
| 221 | 203 | ||
| 222 | @override | 204 | @override |
| 223 | - String get onboardingMemberBody => | ||
| 224 | - 'Start your pressure alert and health companion journey, so love and care are always present.'; | 205 | + String get onboardingMemberBody => 'Start your pressure alert and health companion journey, so love and care are always present.'; |
| 225 | 206 | ||
| 226 | @override | 207 | @override |
| 227 | String get onboardingMemberOriginalPrice => 'Original ¥72.00/year'; | 208 | String get onboardingMemberOriginalPrice => 'Original ¥72.00/year'; |
| @@ -236,16 +217,13 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -236,16 +217,13 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 236 | String get onboardingMemberAllOptions => 'View all purchase options'; | 217 | String get onboardingMemberAllOptions => 'View all purchase options'; |
| 237 | 218 | ||
| 238 | @override | 219 | @override |
| 239 | - String get healthCompanionIsNowAvailable => | ||
| 240 | - 'Health Companion is now available'; | 220 | + String get healthCompanionIsNowAvailable => 'Health Companion is now available'; |
| 241 | 221 | ||
| 242 | @override | 222 | @override |
| 243 | - String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => | ||
| 244 | - 'You can now view each other\'s HRV, stress levels, and sleep patterns, and reach out to check in when the other person seems tired.'; | 223 | + String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => 'You can now view each other\'s HRV, stress levels, and sleep patterns, and reach out to check in when the other person seems tired.'; |
| 245 | 224 | ||
| 246 | @override | 225 | @override |
| 247 | - String get bindPartnerTitle => | ||
| 248 | - 'Add a Close Contact\nOne more person to care about your health'; | 226 | + String get bindPartnerTitle => 'Add a Close Contact\nOne more person to care about your health'; |
| 249 | 227 | ||
| 250 | @override | 228 | @override |
| 251 | String get bindPartnerMyId => 'My ID'; | 229 | String get bindPartnerMyId => 'My ID'; |
| 1 | -// ignore: unused_import | ||
| 2 | -import 'package:intl/intl.dart' as intl; | ||
| 3 | import 'app_localizations.dart'; | 1 | import 'app_localizations.dart'; |
| 4 | 2 | ||
| 5 | // ignore_for_file: type=lint | 3 | // ignore_for_file: type=lint |
| @@ -72,8 +70,7 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -72,8 +70,7 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 72 | String get onboardingIntroTitle => 'DoubleFeel 是专为 Apple Watch 打造的健康陪伴app'; | 70 | String get onboardingIntroTitle => 'DoubleFeel 是专为 Apple Watch 打造的健康陪伴app'; |
| 73 | 71 | ||
| 74 | @override | 72 | @override |
| 75 | - String get onboardingIntroBody => | ||
| 76 | - '我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>'; | 73 | + String get onboardingIntroBody => '我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>'; |
| 77 | 74 | ||
| 78 | @override | 75 | @override |
| 79 | String get onboardingStateQuestion => '请问以下哪些描述,经常发生在你身上?'; | 76 | String get onboardingStateQuestion => '请问以下哪些描述,经常发生在你身上?'; |
| @@ -163,8 +160,7 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -163,8 +160,7 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 163 | String get onboardingHrvSubtitle => '它能帮助我们衡量整体的压力和健康状态'; | 160 | String get onboardingHrvSubtitle => '它能帮助我们衡量整体的压力和健康状态'; |
| 164 | 161 | ||
| 165 | @override | 162 | @override |
| 166 | - String get onboardingHrvDescription => | ||
| 167 | - '心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力'; | 163 | + String get onboardingHrvDescription => '心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力'; |
| 168 | 164 | ||
| 169 | @override | 165 | @override |
| 170 | String get onboardingTellMeMore => '展开说说'; | 166 | String get onboardingTellMeMore => '展开说说'; |
| @@ -188,12 +184,10 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -188,12 +184,10 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 188 | String get onboardingHealthPermissionTitle => '允许访问健康数据'; | 184 | String get onboardingHealthPermissionTitle => '允许访问健康数据'; |
| 189 | 185 | ||
| 190 | @override | 186 | @override |
| 191 | - String get onboardingHealthPermissionBody => | ||
| 192 | - 'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。'; | 187 | + String get onboardingHealthPermissionBody => 'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。'; |
| 193 | 188 | ||
| 194 | @override | 189 | @override |
| 195 | - String get onboardingHealthPermissionPrivacy => | ||
| 196 | - '请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。'; | 190 | + String get onboardingHealthPermissionPrivacy => '请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。'; |
| 197 | 191 | ||
| 198 | @override | 192 | @override |
| 199 | String get onboardingNotificationTitle => '开启通知'; | 193 | String get onboardingNotificationTitle => '开启通知'; |
| @@ -202,8 +196,7 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -202,8 +196,7 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 202 | String get onboardingNotificationSubtitle => '及时了解身体每一次异动'; | 196 | String get onboardingNotificationSubtitle => '及时了解身体每一次异动'; |
| 203 | 197 | ||
| 204 | @override | 198 | @override |
| 205 | - String get onboardingNotificationBody => | ||
| 206 | - 'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态'; | 199 | + String get onboardingNotificationBody => 'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态'; |
| 207 | 200 | ||
| 208 | @override | 201 | @override |
| 209 | String get onboardingMemberTitle => '获得年度会员优惠'; | 202 | String get onboardingMemberTitle => '获得年度会员优惠'; |
| @@ -227,9 +220,7 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -227,9 +220,7 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 227 | String get healthCompanionIsNowAvailable => '健康陪伴已开启'; | 220 | String get healthCompanionIsNowAvailable => '健康陪伴已开启'; |
| 228 | 221 | ||
| 229 | @override | 222 | @override |
| 230 | - String | ||
| 231 | - get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => | ||
| 232 | - '你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'; | 223 | + String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => '你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'; |
| 233 | 224 | ||
| 234 | @override | 225 | @override |
| 235 | String get bindPartnerTitle => '添加亲密联系人\n多一个人关注你的健康'; | 226 | String get bindPartnerTitle => '添加亲密联系人\n多一个人关注你的健康'; |
| @@ -133,10 +133,10 @@ packages: | @@ -133,10 +133,10 @@ packages: | ||
| 133 | dependency: transitive | 133 | dependency: transitive |
| 134 | description: | 134 | description: |
| 135 | name: characters | 135 | name: characters |
| 136 | - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 | 136 | + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" |
| 137 | url: "https://pub.dev" | 137 | url: "https://pub.dev" |
| 138 | source: hosted | 138 | source: hosted |
| 139 | - version: "1.4.0" | 139 | + version: "1.3.0" |
| 140 | checked_yaml: | 140 | checked_yaml: |
| 141 | dependency: transitive | 141 | dependency: transitive |
| 142 | description: | 142 | description: |
| @@ -149,10 +149,10 @@ packages: | @@ -149,10 +149,10 @@ packages: | ||
| 149 | dependency: transitive | 149 | dependency: transitive |
| 150 | description: | 150 | description: |
| 151 | name: clock | 151 | name: clock |
| 152 | - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b | 152 | + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf |
| 153 | url: "https://pub.dev" | 153 | url: "https://pub.dev" |
| 154 | source: hosted | 154 | source: hosted |
| 155 | - version: "1.1.2" | 155 | + version: "1.1.1" |
| 156 | code_builder: | 156 | code_builder: |
| 157 | dependency: transitive | 157 | dependency: transitive |
| 158 | description: | 158 | description: |
| @@ -165,10 +165,10 @@ packages: | @@ -165,10 +165,10 @@ packages: | ||
| 165 | dependency: transitive | 165 | dependency: transitive |
| 166 | description: | 166 | description: |
| 167 | name: collection | 167 | name: collection |
| 168 | - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" | 168 | + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf |
| 169 | url: "https://pub.dev" | 169 | url: "https://pub.dev" |
| 170 | source: hosted | 170 | source: hosted |
| 171 | - version: "1.19.1" | 171 | + version: "1.19.0" |
| 172 | convert: | 172 | convert: |
| 173 | dependency: transitive | 173 | dependency: transitive |
| 174 | description: | 174 | description: |
| @@ -237,10 +237,10 @@ packages: | @@ -237,10 +237,10 @@ packages: | ||
| 237 | dependency: transitive | 237 | dependency: transitive |
| 238 | description: | 238 | description: |
| 239 | name: fake_async | 239 | name: fake_async |
| 240 | - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" | 240 | + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" |
| 241 | url: "https://pub.dev" | 241 | url: "https://pub.dev" |
| 242 | source: hosted | 242 | source: hosted |
| 243 | - version: "1.3.3" | 243 | + version: "1.3.1" |
| 244 | ffi: | 244 | ffi: |
| 245 | dependency: transitive | 245 | dependency: transitive |
| 246 | description: | 246 | description: |
| @@ -418,7 +418,7 @@ packages: | @@ -418,7 +418,7 @@ packages: | ||
| 418 | dependency: transitive | 418 | dependency: transitive |
| 419 | description: | 419 | description: |
| 420 | path: image_cropper_for_web | 420 | path: image_cropper_for_web |
| 421 | - ref: "65c2c99891882ea59732959a672f3d5993a837bb" | 421 | + ref: "br_v9.1.0_ohos" |
| 422 | resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb" | 422 | resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb" |
| 423 | url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git" | 423 | url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git" |
| 424 | source: git | 424 | source: git |
| @@ -427,7 +427,7 @@ packages: | @@ -427,7 +427,7 @@ packages: | ||
| 427 | dependency: transitive | 427 | dependency: transitive |
| 428 | description: | 428 | description: |
| 429 | path: image_cropper_platform_interface | 429 | path: image_cropper_platform_interface |
| 430 | - ref: "65c2c99891882ea59732959a672f3d5993a837bb" | 430 | + ref: "br_v9.1.0_ohos" |
| 431 | resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb" | 431 | resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb" |
| 432 | url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git" | 432 | url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git" |
| 433 | source: git | 433 | source: git |
| @@ -509,10 +509,10 @@ packages: | @@ -509,10 +509,10 @@ packages: | ||
| 509 | dependency: "direct main" | 509 | dependency: "direct main" |
| 510 | description: | 510 | description: |
| 511 | name: intl | 511 | name: intl |
| 512 | - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" | 512 | + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf |
| 513 | url: "https://pub.dev" | 513 | url: "https://pub.dev" |
| 514 | source: hosted | 514 | source: hosted |
| 515 | - version: "0.20.2" | 515 | + version: "0.19.0" |
| 516 | io: | 516 | io: |
| 517 | dependency: transitive | 517 | dependency: transitive |
| 518 | description: | 518 | description: |
| @@ -549,26 +549,26 @@ packages: | @@ -549,26 +549,26 @@ packages: | ||
| 549 | dependency: transitive | 549 | dependency: transitive |
| 550 | description: | 550 | description: |
| 551 | name: leak_tracker | 551 | name: leak_tracker |
| 552 | - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" | 552 | + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" |
| 553 | url: "https://pub.dev" | 553 | url: "https://pub.dev" |
| 554 | source: hosted | 554 | source: hosted |
| 555 | - version: "11.0.2" | 555 | + version: "10.0.7" |
| 556 | leak_tracker_flutter_testing: | 556 | leak_tracker_flutter_testing: |
| 557 | dependency: transitive | 557 | dependency: transitive |
| 558 | description: | 558 | description: |
| 559 | name: leak_tracker_flutter_testing | 559 | name: leak_tracker_flutter_testing |
| 560 | - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" | 560 | + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" |
| 561 | url: "https://pub.dev" | 561 | url: "https://pub.dev" |
| 562 | source: hosted | 562 | source: hosted |
| 563 | - version: "3.0.10" | 563 | + version: "3.0.8" |
| 564 | leak_tracker_testing: | 564 | leak_tracker_testing: |
| 565 | dependency: transitive | 565 | dependency: transitive |
| 566 | description: | 566 | description: |
| 567 | name: leak_tracker_testing | 567 | name: leak_tracker_testing |
| 568 | - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" | 568 | + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" |
| 569 | url: "https://pub.dev" | 569 | url: "https://pub.dev" |
| 570 | source: hosted | 570 | source: hosted |
| 571 | - version: "3.0.2" | 571 | + version: "3.0.1" |
| 572 | lints: | 572 | lints: |
| 573 | dependency: transitive | 573 | dependency: transitive |
| 574 | description: | 574 | description: |
| @@ -597,10 +597,10 @@ packages: | @@ -597,10 +597,10 @@ packages: | ||
| 597 | dependency: transitive | 597 | dependency: transitive |
| 598 | description: | 598 | description: |
| 599 | name: matcher | 599 | name: matcher |
| 600 | - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 | 600 | + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb |
| 601 | url: "https://pub.dev" | 601 | url: "https://pub.dev" |
| 602 | source: hosted | 602 | source: hosted |
| 603 | - version: "0.12.17" | 603 | + version: "0.12.16+1" |
| 604 | material_color_utilities: | 604 | material_color_utilities: |
| 605 | dependency: transitive | 605 | dependency: transitive |
| 606 | description: | 606 | description: |
| @@ -613,10 +613,10 @@ packages: | @@ -613,10 +613,10 @@ packages: | ||
| 613 | dependency: transitive | 613 | dependency: transitive |
| 614 | description: | 614 | description: |
| 615 | name: meta | 615 | name: meta |
| 616 | - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" | 616 | + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 |
| 617 | url: "https://pub.dev" | 617 | url: "https://pub.dev" |
| 618 | source: hosted | 618 | source: hosted |
| 619 | - version: "1.17.0" | 619 | + version: "1.15.0" |
| 620 | mime: | 620 | mime: |
| 621 | dependency: transitive | 621 | dependency: transitive |
| 622 | description: | 622 | description: |
| @@ -645,10 +645,10 @@ packages: | @@ -645,10 +645,10 @@ packages: | ||
| 645 | dependency: transitive | 645 | dependency: transitive |
| 646 | description: | 646 | description: |
| 647 | name: path | 647 | name: path |
| 648 | - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" | 648 | + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" |
| 649 | url: "https://pub.dev" | 649 | url: "https://pub.dev" |
| 650 | source: hosted | 650 | source: hosted |
| 651 | - version: "1.9.1" | 651 | + version: "1.9.0" |
| 652 | path_provider: | 652 | path_provider: |
| 653 | dependency: transitive | 653 | dependency: transitive |
| 654 | description: | 654 | description: |
| @@ -982,18 +982,18 @@ packages: | @@ -982,18 +982,18 @@ packages: | ||
| 982 | dependency: transitive | 982 | dependency: transitive |
| 983 | description: | 983 | description: |
| 984 | name: stack_trace | 984 | name: stack_trace |
| 985 | - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" | 985 | + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" |
| 986 | url: "https://pub.dev" | 986 | url: "https://pub.dev" |
| 987 | source: hosted | 987 | source: hosted |
| 988 | - version: "1.12.1" | 988 | + version: "1.12.0" |
| 989 | stream_channel: | 989 | stream_channel: |
| 990 | dependency: transitive | 990 | dependency: transitive |
| 991 | description: | 991 | description: |
| 992 | name: stream_channel | 992 | name: stream_channel |
| 993 | - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" | 993 | + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 |
| 994 | url: "https://pub.dev" | 994 | url: "https://pub.dev" |
| 995 | source: hosted | 995 | source: hosted |
| 996 | - version: "2.1.4" | 996 | + version: "2.1.2" |
| 997 | stream_transform: | 997 | stream_transform: |
| 998 | dependency: transitive | 998 | dependency: transitive |
| 999 | description: | 999 | description: |
| @@ -1022,10 +1022,10 @@ packages: | @@ -1022,10 +1022,10 @@ packages: | ||
| 1022 | dependency: "direct main" | 1022 | dependency: "direct main" |
| 1023 | description: | 1023 | description: |
| 1024 | name: table_calendar | 1024 | name: table_calendar |
| 1025 | - sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" | 1025 | + sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 |
| 1026 | url: "https://pub.dev" | 1026 | url: "https://pub.dev" |
| 1027 | source: hosted | 1027 | source: hosted |
| 1028 | - version: "3.2.0" | 1028 | + version: "3.1.3" |
| 1029 | term_glyph: | 1029 | term_glyph: |
| 1030 | dependency: transitive | 1030 | dependency: transitive |
| 1031 | description: | 1031 | description: |
| @@ -1038,10 +1038,10 @@ packages: | @@ -1038,10 +1038,10 @@ packages: | ||
| 1038 | dependency: transitive | 1038 | dependency: transitive |
| 1039 | description: | 1039 | description: |
| 1040 | name: test_api | 1040 | name: test_api |
| 1041 | - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 | 1041 | + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" |
| 1042 | url: "https://pub.dev" | 1042 | url: "https://pub.dev" |
| 1043 | source: hosted | 1043 | source: hosted |
| 1044 | - version: "0.7.7" | 1044 | + version: "0.7.3" |
| 1045 | timing: | 1045 | timing: |
| 1046 | dependency: transitive | 1046 | dependency: transitive |
| 1047 | description: | 1047 | description: |
| @@ -1070,10 +1070,10 @@ packages: | @@ -1070,10 +1070,10 @@ packages: | ||
| 1070 | dependency: transitive | 1070 | dependency: transitive |
| 1071 | description: | 1071 | description: |
| 1072 | name: vector_math | 1072 | name: vector_math |
| 1073 | - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b | 1073 | + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" |
| 1074 | url: "https://pub.dev" | 1074 | url: "https://pub.dev" |
| 1075 | source: hosted | 1075 | source: hosted |
| 1076 | - version: "2.2.0" | 1076 | + version: "2.1.4" |
| 1077 | vm_service: | 1077 | vm_service: |
| 1078 | dependency: transitive | 1078 | dependency: transitive |
| 1079 | description: | 1079 | description: |
| @@ -1127,8 +1127,8 @@ packages: | @@ -1127,8 +1127,8 @@ packages: | ||
| 1127 | dependency: transitive | 1127 | dependency: transitive |
| 1128 | description: | 1128 | description: |
| 1129 | path: "packages/webview_flutter/webview_flutter_android" | 1129 | path: "packages/webview_flutter/webview_flutter_android" |
| 1130 | - ref: de942e79c9057b32ad31106508bd87c0d60aef83 | ||
| 1131 | - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 | 1130 | + ref: "br_webview_flutter-v4.13.0_ohos" |
| 1131 | + resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba" | ||
| 1132 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" | 1132 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" |
| 1133 | source: git | 1133 | source: git |
| 1134 | version: "4.7.0" | 1134 | version: "4.7.0" |
| @@ -1136,8 +1136,8 @@ packages: | @@ -1136,8 +1136,8 @@ packages: | ||
| 1136 | dependency: transitive | 1136 | dependency: transitive |
| 1137 | description: | 1137 | description: |
| 1138 | path: "packages/webview_flutter/webview_flutter_ohos" | 1138 | path: "packages/webview_flutter/webview_flutter_ohos" |
| 1139 | - ref: de942e79c9057b32ad31106508bd87c0d60aef83 | ||
| 1140 | - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 | 1139 | + ref: "br_webview_flutter-v4.13.0_ohos" |
| 1140 | + resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba" | ||
| 1141 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" | 1141 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" |
| 1142 | source: git | 1142 | source: git |
| 1143 | version: "4.7.0" | 1143 | version: "4.7.0" |
| @@ -1145,8 +1145,8 @@ packages: | @@ -1145,8 +1145,8 @@ packages: | ||
| 1145 | dependency: transitive | 1145 | dependency: transitive |
| 1146 | description: | 1146 | description: |
| 1147 | path: "packages/webview_flutter/webview_flutter_platform_interface" | 1147 | path: "packages/webview_flutter/webview_flutter_platform_interface" |
| 1148 | - ref: de942e79c9057b32ad31106508bd87c0d60aef83 | ||
| 1149 | - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 | 1148 | + ref: "br_webview_flutter-v4.13.0_ohos" |
| 1149 | + resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba" | ||
| 1150 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" | 1150 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" |
| 1151 | source: git | 1151 | source: git |
| 1152 | version: "2.13.1" | 1152 | version: "2.13.1" |
| @@ -1154,8 +1154,8 @@ packages: | @@ -1154,8 +1154,8 @@ packages: | ||
| 1154 | dependency: transitive | 1154 | dependency: transitive |
| 1155 | description: | 1155 | description: |
| 1156 | path: "packages/webview_flutter/webview_flutter_wkwebview" | 1156 | path: "packages/webview_flutter/webview_flutter_wkwebview" |
| 1157 | - ref: de942e79c9057b32ad31106508bd87c0d60aef83 | ||
| 1158 | - resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83 | 1157 | + ref: "br_webview_flutter-v4.13.0_ohos" |
| 1158 | + resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba" | ||
| 1159 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" | 1159 | url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" |
| 1160 | source: git | 1160 | source: git |
| 1161 | version: "3.22.0" | 1161 | version: "3.22.0" |
| @@ -1176,5 +1176,5 @@ packages: | @@ -1176,5 +1176,5 @@ packages: | ||
| 1176 | source: hosted | 1176 | source: hosted |
| 1177 | version: "3.1.3" | 1177 | version: "3.1.3" |
| 1178 | sdks: | 1178 | sdks: |
| 1179 | - dart: ">=3.8.0-0 <4.0.0" | 1179 | + dart: ">=3.6.2 <4.0.0" |
| 1180 | flutter: ">=3.27.0" | 1180 | flutter: ">=3.27.0" |
-
Please register or login to post a comment