Commit 962df483813238ecb9542f4a02c388fd5e8556f5

Authored by 常守达
1 parent 1ac8d16e

feat(today): 初始资源颜色

Showing 57 changed files with 2157 additions and 212 deletions

230 Bytes | W: | H:

139 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin

224 Bytes | W: | H:

142 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin

842 Bytes | W: | H:

799 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
... ... @@ -54,7 +54,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
... ...
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -11,6 +12,9 @@ class DoubleFeelApp extends StatelessWidget {
Widget build(BuildContext context) {
return GetMaterialApp(
title: context.l10n.appName,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.light,
initialRoute: AppPages.initialRoute,
getPages: AppPages.routes,
localizationsDelegates: AppLocalizations.localizationsDelegates,
... ...
... ... @@ -2,15 +2,19 @@ import 'package:get/get.dart';
import '../controllers/home_controller.dart';
import '../controllers/today_controller.dart';
import '../controllers/trend/trend_controller.dart';
import '../controllers/trend/hrv_controller.dart';
import '../controllers/trend/activity_controller.dart';
import '../controllers/trend/sleep_controller.dart';
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(() => HomeController(), fenix: true);
Get.lazyPut<TodayController>(() => TodayController(), fenix: true);
// 其他 tab 的 controller 后续在此添加
// Get.lazyPut<TrendController>(() => TrendController(), fenix: true);
// Get.lazyPut<FriendsController>(() => FriendsController(), fenix: true);
// Get.lazyPut<MyController>(() => MyController(), fenix: true);
Get.lazyPut<TrendController>(() => TrendController(), fenix: true);
Get.lazyPut<HrvController>(() => HrvController(), fenix: true);
Get.lazyPut<ActivityController>(() => ActivityController(), fenix: true);
Get.lazyPut<SleepController>(() => SleepController(), fenix: true);
}
}
... ...
import 'package:get/get.dart';
import 'trend_period_controller.dart';
/// 活动消耗 专属 Controller
class ActivityController extends TrendPeriodController {
// ── 响应式数据源 ──────────────────────────────────────
final totalBurn = '0'.obs;
final averageBurn = '0'.obs;
final chartData = <double>[].obs;
final chartLabels = <String>[].obs;
@override
void loadData() {
isLoading.value = true;
// 模拟 API 延迟加载
Future.delayed(const Duration(milliseconds: 300), () {
final offset = dateOffset.value;
// 依据时间范围偏移动态渲染不同数据,模拟真实 API 拉取效果
totalBurn.value = '${2340 + offset * 120}';
averageBurn.value = '${334 + offset * 18}';
chartData.value = [
280.0 + offset * 15,
350.0 - offset * 25,
420.0 + offset * 35,
310.0 - offset * 15,
390.0 + offset * 20,
480.0 + offset * 45,
260.0 - offset * 30,
].map((e) => e.clamp(50.0, 600.0)).toList();
chartLabels.value = ['一', '二', '三', '四', '五', '六', '日'];
isLoading.value = false;
});
}
}
... ...
import 'package:get/get.dart';
import 'trend_period_controller.dart';
/// HRV 心率变异性 专属 Controller
class HrvController extends TrendPeriodController {
// ── 响应式数据源 ──────────────────────────────────────
final averageHrv = '0'.obs;
final changeHrv = '0'.obs;
final chartData = <double>[].obs;
final chartLabels = <String>[].obs;
@override
void loadData() {
isLoading.value = true;
// 模拟 API 延迟加载
Future.delayed(const Duration(milliseconds: 300), () {
final offset = dateOffset.value;
// 依据时间范围偏移动态渲染不同数据,模拟真实 API 拉取效果
averageHrv.value = '${46 + offset}';
changeHrv.value = offset >= 0 ? '+3' : '${offset * 2}';
chartData.value = [
42.0 + offset,
48.0 - offset,
55.0 + offset * 2,
46.0 - offset,
52.0 + offset,
58.0 + offset * 3,
50.0 - offset * 2,
].map((e) => e.clamp(20.0, 80.0)).toList();
chartLabels.value = ['一', '二', '三', '四', '五', '六', '日'];
isLoading.value = false;
});
}
}
... ...
import 'package:get/get.dart';
import 'trend_period_controller.dart';
/// 睡眠报告 专属 Controller
class SleepController extends TrendPeriodController {
// ── 响应式数据源 ──────────────────────────────────────
final averageSleep = '0.0'.obs;
final deepSleepRatio = '0'.obs;
final chartData = <double>[].obs;
final chartLabels = <String>[].obs;
@override
void loadData() {
isLoading.value = true;
// 模拟 API 延迟加载
Future.delayed(const Duration(milliseconds: 300), () {
final offset = dateOffset.value;
// 依据时间范围偏移动态渲染不同数据,模拟真实 API 拉取效果
final avg = (7.2 + offset * 0.15).clamp(4.0, 10.0);
averageSleep.value = avg.toStringAsFixed(1);
deepSleepRatio.value = '${(28 + offset).clamp(10, 50)}';
chartData.value = [
6.5 + offset * 0.1,
7.0 - offset * 0.2,
8.2 + offset * 0.3,
6.8 - offset * 0.1,
7.5 + offset * 0.2,
9.0 + offset * 0.4,
7.2 - offset * 0.3,
].map((e) => e.clamp(4.0, 10.0)).toList();
chartLabels.value = ['一', '二', '三', '四', '五', '六', '日'];
isLoading.value = false;
});
}
}
... ...
import 'package:get/get.dart';
/// 趋势页顶层 Controller,仅负责:
/// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex)
class TrendController extends GetxController {
// 第1层类型 Tab(0 = HRV, 1 = 活动, 2 = 睡眠)
final selectedTypeIndex = 0.obs;
void changeType(int index) {
if (index == selectedTypeIndex.value) return;
selectedTypeIndex.value = index;
}
}
... ...
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'trend_types.dart';
/// TrendPeriodController — 抽象基类
///
/// 封装 HRV / 活动 / 睡眠 三个 Controller 共用的:
/// - 第2层:周 / 月 / 年 period 切换
/// - 第3层:日期范围偏移(每个 period 各自记忆 offset)
/// - isLoading 状态
/// - dateRangeLabel 计算
///
/// 子类只需实现 [loadData()],专注自己的数据逻辑。
abstract class TrendPeriodController extends GetxController {
// ── 第2层:时间维度 ──────────────────────────────────
final currentPeriod = TrendPeriod.week.obs;
// ── 第3层:日期偏移(每个 period 独立记忆)──────────
// key = TrendPeriod,value = offset(0=当前,-1=上一个…)
final _offsetByPeriod = <TrendPeriod, int>{};
final dateOffset = 0.obs;
// ── 加载状态 ─────────────────────────────────────────
final isLoading = false.obs;
// ────────────────────────────────────────────────────
// 公开 API
// ────────────────────────────────────────────────────
void changePeriod(TrendPeriod period) {
if (currentPeriod.value == period) return;
// 保存当前 period 的 offset,再切换
_offsetByPeriod[currentPeriod.value] = dateOffset.value;
currentPeriod.value = period;
// 恢复新 period 上次记忆 of offset(默认 0)
dateOffset.value = _offsetByPeriod[period] ?? 0;
loadData();
}
void goToPrevious() {
final key = currentPeriod.value;
_offsetByPeriod[key] = (_offsetByPeriod[key] ?? 0) - 1;
dateOffset.value = _offsetByPeriod[key]!;
loadData();
}
void goToNext() {
if (!canGoNext) return;
final key = currentPeriod.value;
_offsetByPeriod[key] = (_offsetByPeriod[key] ?? 0) + 1;
dateOffset.value = _offsetByPeriod[key]!;
loadData();
}
bool get canGoNext => dateOffset.value < 0;
/// 当前 period + offset 对应的日期显示文字
String get dateRangeLabel =>
buildDateRangeLabel(currentPeriod.value, dateOffset.value);
// ────────────────────────────────────────────────────
// 子类必须实现:各自的数据加载逻辑
// ────────────────────────────────────────────────────
@protected
void loadData();
@override
void onInit() {
super.onInit();
loadData();
}
}
... ...
import 'package:intl/intl.dart';
/// 时间维度枚举(共享给所有 trend controller 使用)
enum TrendPeriod { week, month, year }
extension TrendPeriodLabel on TrendPeriod {
String get label => switch (this) {
TrendPeriod.week => '周',
TrendPeriod.month => '月',
TrendPeriod.year => '年',
};
}
/// 根据 period + offset 计算显示标签
String buildDateRangeLabel(TrendPeriod period, int offset) {
final now = DateTime.now();
switch (period) {
case TrendPeriod.week:
final monday = now.subtract(Duration(days: now.weekday - 1));
final start = monday.add(Duration(days: offset * 7));
final end = start.add(const Duration(days: 6));
final fmt = DateFormat('M月d日');
return '${fmt.format(start)} - ${fmt.format(end)}';
case TrendPeriod.month:
final month = DateTime(now.year, now.month + offset);
return DateFormat('yyyy年M月').format(month);
case TrendPeriod.year:
return '${now.year + offset}年';
}
}
... ...
... ... @@ -175,14 +175,6 @@ class TodayTab extends GetView<TodayController> {
const TodayHrvAdBanner(),
const SizedBox(height: 12),
// 2. 该日压力状态行
_SectionHeader(
title: '该日压力状态',
showInfo: true,
onInfoTap: () {},
),
const SizedBox(height: 8),
// 3. HRV + 心率数字卡
const TodayHrvNumberCard(),
const SizedBox(height: 12),
... ... @@ -190,16 +182,27 @@ class TodayTab extends GetView<TodayController> {
// 4. HRV 趋势图
const TodayHrvChartCard(),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'立即测量HRV',
style: TextStyle(
color: const Color(0xFF0F0F11),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
// 5. 每日行动
const TodayActionsCard(),
const TodayHrvChartCard(),
const TodayHrvChartCard(),
const TodayHrvChartCard(),
const TodayHrvChartCard(),
// 底部安全间距(留给 TabBar 高度)
const SizedBox(height: 80),
const SizedBox(height: 180),
]),
),
),
... ... @@ -288,43 +291,3 @@ class TodayTab extends GetView<TodayController> {
bool _isSameDay(DateTime a, DateTime b) =>
a.year == b.year && a.month == b.month && a.day == b.day;
}
/// 通用 Section 标题行
class _SectionHeader extends StatelessWidget {
final String title;
final bool showInfo;
final VoidCallback? onInfoTap;
const _SectionHeader({
required this.title,
this.showInfo = false,
this.onInfoTap,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF0F0F11),
),
),
if (showInfo) ...[
const SizedBox(width: 4),
GestureDetector(
onTap: onInfoTap,
child: const Icon(
Icons.info_outline,
size: 15,
color: Color(0xFF999999),
),
),
],
],
);
}
}
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class TrendTab extends StatelessWidget {
import '../../controllers/trend/trend_controller.dart';
import '../../widgets/trend/trend_type_tab_bar.dart';
import '../../widgets/trend/hrv_trend_view.dart';
import '../../widgets/trend/activity_trend_view.dart';
import '../../widgets/trend/sleep_trend_view.dart';
/// 趋势大 Tab 页面主视图
/// 采用 StatefulWidget 绑定 SingleTickerProviderStateMixin 并通过 TabController
/// 控制 TabBar 与 TabBarView 的同步滑动,实现极具高级感的跟手划动效果。
class TrendTab extends StatefulWidget {
const TrendTab({super.key});
@override
State<TrendTab> createState() => _TrendTabState();
}
class _TrendTabState extends State<TrendTab> with SingleTickerProviderStateMixin {
late final TabController _tabController;
late final TrendController _trendController;
late final Worker _rxWorker;
static const _bgColor = Color(0xFFF5F2FF);
static const _h1 = Color(0xFF0F0F11);
// 三个内容区(包裹 KeepAliveWrapper 以记忆滑动/图表状态,确保子 View 能继续保持纯净的 GetView 规范)
static const _contentViews = [
KeepAliveWrapper(child: HrvTrendView()),
KeepAliveWrapper(child: ActivityTrendView()),
KeepAliveWrapper(child: SleepTrendView()),
];
@override
void initState() {
super.initState();
_trendController = Get.find<TrendController>();
// 初始化 TabController
_tabController = TabController(
length: 3,
vsync: this,
initialIndex: _trendController.selectedTypeIndex.value,
);
// 1. 当滑动/点击切换页面时,同步更新 TrendController 状态
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
_trendController.changeType(_tabController.index);
}
});
// 2. 当外部修改 selectedTypeIndex 时,同步动画跳转 TabBarView
_rxWorker = ever(_trendController.selectedTypeIndex, (index) {
if (_tabController.index != index) {
_tabController.animateTo(index);
}
});
}
@override
void dispose() {
_rxWorker.dispose();
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Center(
child: Text('趋势', style: TextStyle(fontSize: 20, color: Color(0xFF0F0F11))),
final topPadding = MediaQuery.of(context).padding.top;
return Container(
color: _bgColor,
child: Column(
children: [
// ── 状态栏占位 + 标题栏 ──
Container(
color: _bgColor,
padding: EdgeInsets.only(top: topPadding),
child: _buildAppBar(),
),
// ── 第1层:原生 TabBar(支持跟手滑动指示器) ──
Container(
color: _bgColor,
child: TrendTypeTabBar(tabController: _tabController),
),
// ── 内容区:TabBarView 提供极其平滑、跟手的原生翻页动画 ──
Expanded(
child: TabBarView(
controller: _tabController,
children: _contentViews,
),
),
],
),
);
}
Widget _buildAppBar() {
return SizedBox(
height: 48,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
children: [
Text(
'趋势',
style: TextStyle(
color: _h1,
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}
/// 专用于在 TabBarView/PageView 中保持子组件状态的轻量级包装器
/// 使得子 View 可以维持无状态的 GetView 极简设计规范
class KeepAliveWrapper extends StatefulWidget {
final Widget child;
const KeepAliveWrapper({super.key, required this.child});
@override
State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}
class _KeepAliveWrapperState extends State<KeepAliveWrapper>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
}
... ...
... ... @@ -47,7 +47,6 @@ class _ActionCard extends StatelessWidget {
const _ActionCard({required this.item});
static const _cardBg = Color(0xFFF7F7F9);
static const _brandColor = Color(0xFF845EEE);
static const _h1 = Color(0xFF0F0F11);
static const _h2 = Color(0xFF666666);
... ... @@ -58,7 +57,7 @@ class _ActionCard extends StatelessWidget {
width: 140,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: _cardBg,
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
... ...
... ... @@ -14,11 +14,33 @@ import '../../controllers/today_controller.dart';
class TodayHrvChartCard extends GetView<TodayController> {
const TodayHrvChartCard({super.key});
static const _cardBg = Color(0xFFF7F7F9);
static const _h2 = Color(0xFF78787D);
static const _h3 = Color(0xFF999999);
static const _h5 = Color(0xFFCCCCCC);
/// 实时压力 mock:按小时 0~18,Y 轴 0~80
static const List<double> _mockStressByHour = [
18,
22,
28,
35,
32,
40,
48,
55,
62,
58,
52,
45,
38,
42,
50,
56,
64,
58,
46,
];
@override
Widget build(BuildContext context) {
final spots =
... ... @@ -27,7 +49,7 @@ class TodayHrvChartCard extends GetView<TodayController> {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
color: _cardBg,
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
... ... @@ -48,16 +70,16 @@ class TodayHrvChartCard extends GetView<TodayController> {
const Spacer(),
],
),
const SizedBox(height: 16),
const SizedBox(width: 14),
SizedBox(
height: 160,
height: 174,
child: LineChart(
LineChartData(
minX: 0,
maxX: 18,
minY: 0,
maxY: 80,
clipData: const FlClipData.all(),
// clipData: const FlClipData.all(),
gridData: FlGridData(
show: true,
drawVerticalLine: true,
... ... @@ -118,7 +140,7 @@ class TodayHrvChartCard extends GetView<TodayController> {
radius: 4,
color: Colors.white,
strokeWidth: 3,
strokeColor: _getDotStrokeColor(spot),
strokeColor: _getColor(spot.y),
),
))
],
... ... @@ -138,7 +160,7 @@ class TodayHrvChartCard extends GetView<TodayController> {
radius: 5,
color: Colors.white,
strokeWidth: 3.5,
strokeColor: _getDotStrokeColor(spot),
strokeColor: _getColor(spot.y),
);
},
),
... ... @@ -168,45 +190,218 @@ class TodayHrvChartCard extends GetView<TodayController> {
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 14, top: 16),
child: Row(
children: [
const Text(
'实时压力',
style: TextStyle(
color: _h2,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
const Icon(Icons.info_outline, size: 14, color: _h3),
const Spacer(),
],
),
),
SizedBox(
height: 174,
child: Stack(
children: [
BarChart(
BarChartData(
minY: 0,
maxY: 100,
groupsSpace: 1,
alignment: BarChartAlignment.start,
barGroups: List.generate(
_mockStressByHour.length * 5,
(i) => BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY:
_mockStressByHour[i % _mockStressByHour.length],
width: 2,
color: _getColor(_mockStressByHour[
i % _mockStressByHour.length]),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(2),
topRight: Radius.circular(2),
),
),
],
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
horizontalInterval: 25,
getDrawingHorizontalLine: (_) => const FlLine(
color: _h5,
strokeWidth: 1,
dashArray: [2, 2],
),
),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (val, _) {
return Text(
'${val.toInt()}',
style: const TextStyle(fontSize: 10, color: _h3),
);
},
)),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 20,
interval: 60,
getTitlesWidget: (val, meta) {
final labels = {
0: '00:00',
33: '06:00',
66: '12:00',
99: '18:00',
};
final label = labels[val];
if (label == null) return const SizedBox.shrink();
return SideTitleWidget(
meta: meta,
child: Text(
label,
style:
const TextStyle(fontSize: 10, color: _h3),
),
);
},
),
),
),
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
tooltipRoundedRadius: 8,
tooltipBorder: BorderSide.none,
getTooltipColor: (touchedSpot) => Color(0xFFF3F3F3),
tooltipPadding: EdgeInsets.only(
left: 12, right: 12, top: 6, bottom: 5),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
'',
TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
children: _getTooltip(rod),
textAlign: TextAlign.start,
);
})),
),
),
Container(
width: 74,
height: 160,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0.50, -0.00),
end: Alignment(0.50, 1.00),
colors: [
const Color(0x4C835DED),
const Color(0x00845EEE)
],
),
),
)
],
),
),
],
),
);
}
Color _getDotStrokeColor(FlSpot spot) {
if (spot.y > 60) {
Color _getColor(double value) {
if (value > 60) {
return const Color(0xFF3BD49D);
} else if (spot.y > 50) {
} else if (value > 50) {
return const Color(0xFF7B9BFB);
} else if (spot.y > 30) {
} else if (value > 30) {
return const Color(0xFFFF9A6E);
} else if (spot.y > 20) {
} else if (value > 20) {
return const Color(0xFFFF5279);
} else {
return const Color(0xFF7B9BFB);
}
}
String _getDotStrokeStatus(FlSpot spot) {
if (spot.y > 60) {
String _getStatus(double value) {
if (value > 60) {
return '状态优秀';
} else if (spot.y > 50) {
} else if (value > 50) {
return '状态良好';
} else if (spot.y > 30) {
} else if (value > 30) {
return '状态一般';
} else if (spot.y > 20) {
} else if (value > 20) {
return '状态较差';
} else {
return '状态极差';
}
}
List<TextSpan>? _getTooltip(BarChartRodData rod) {
return [
// WidgetSpan(
// child: Image.asset('assets/images/home/today/ic_tooltip_dot.png',
// width: 12, height: 12),
// ),
TextSpan(
text: _getStatus(rod.toY),
style: TextStyle(
color: _getColor(rod.toY),
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: '\n',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: '${rod.toY}ms.${rod.fromY}:00',
style: TextStyle(
color: Color(0xFF78787D),
fontWeight: FontWeight.bold,
),
),
];
}
List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot) {
return [
// WidgetSpan(
// child: Image.asset('assets/images/home/today/ic_tooltip_dot.png',
// width: 12, height: 12),
// ),
TextSpan(
text: _getDotStrokeStatus(flSpot),
text: _getStatus(flSpot.y),
style: TextStyle(
color: _getDotStrokeColor(flSpot),
color: _getColor(flSpot.y),
fontWeight: FontWeight.bold,
),
),
... ...
... ... @@ -7,14 +7,12 @@ import '../../controllers/today_controller.dart';
class TodayHrvNumberCard extends GetView<TodayController> {
const TodayHrvNumberCard({super.key});
static const _cardBg = Color(0xFFF7F7F9);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: _cardBg,
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Obx(() => Row(
... ... @@ -28,11 +26,11 @@ class TodayHrvNumberCard extends GetView<TodayController> {
),
),
// 分割线
Container(
width: 1,
height: 40,
color: const Color(0xFFE0E0E0),
),
// Container(
// width: 1,
// height: 40,
// color: const Color(0xFFE0E0E0),
// ),
// 心率
Expanded(
child: _NumberItem(
... ...
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controllers/trend/trend_types.dart';
import '../../controllers/trend/activity_controller.dart';
import 'trend_period_tab_bar.dart';
import 'trend_date_range_bar.dart';
/// 活动消耗趋势内容区(含第2、3层 Tab)
class ActivityTrendView extends GetView<ActivityController> {
const ActivityTrendView({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
TrendPeriodTabBar(controller: controller),
TrendDateRangeBar(controller: controller),
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
const SizedBox(height: 8),
const _ActivitySummaryCard(),
const SizedBox(height: 12),
const _ActivityChartCard(),
const SizedBox(height: 180),
],
),
);
}),
),
],
);
}
}
// ─── 摘要卡片 ─────────────────────────────────────────
class _ActivitySummaryCard extends GetView<ActivityController> {
const _ActivitySummaryCard();
static const _h1 = Color(0xFF0F0F11);
static const _h3 = Color(0xFF999999);
static const _activeColor = Color(0xFFFF9A6E);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final periodLabel = controller.currentPeriod.value.label;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'活动消耗趋势',
style: TextStyle(
color: _h1,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
const Icon(Icons.info_outline, size: 14, color: _h3),
],
),
const SizedBox(height: 16),
Row(
children: [
_StatItem(
label: '本$periodLabel总消耗',
value: controller.totalBurn.value,
unit: 'kcal',
color: _activeColor,
),
const SizedBox(width: 24),
_StatItem(
label: '日均消耗',
value: controller.averageBurn.value,
unit: 'kcal',
color: const Color(0xFF3BD49D),
),
],
),
],
);
}),
);
}
}
class _StatItem extends StatelessWidget {
final String label;
final String value;
final String unit;
final Color color;
const _StatItem({
required this.label,
required this.value,
required this.unit,
required this.color,
});
static const _h2 = Color(0xFF78787D);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(color: _h2, fontSize: 12)),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
color: color,
fontSize: 28,
fontWeight: FontWeight.w600,
height: 1.0,
),
),
const SizedBox(width: 2),
Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Text(
unit,
style: TextStyle(color: color, fontSize: 12),
),
),
],
),
],
);
}
}
// ─── 活动柱状图卡片 ────────────────────────────────────
class _ActivityChartCard extends GetView<ActivityController> {
const _ActivityChartCard();
static const _h1 = Color(0xFF0F0F11);
static const _h3 = Color(0xFF999999);
static const _h5 = Color(0xFFCCCCCC);
static const _activeColor = Color(0xFFFF9A6E);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final period = controller.currentPeriod.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'活动消耗${period.label}趋势图',
style: const TextStyle(
color: _h1,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
SizedBox(
height: 180,
child: _buildChart(),
),
],
);
}),
);
}
Widget _buildChart() {
if (controller.chartData.isEmpty) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Color(0xFFFF9A6E)),
),
);
}
return BarChart(
BarChartData(
minY: 0,
maxY: 600,
groupsSpace: 8,
alignment: BarChartAlignment.spaceAround,
barGroups: List.generate(
controller.chartData.length,
(i) => BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: controller.chartData[i],
width: 20,
color: _activeColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
],
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 200,
getDrawingHorizontalLine: (_) => const FlLine(
color: _h5,
strokeWidth: 1,
dashArray: [2, 2],
),
),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 36,
interval: 200,
getTitlesWidget: (val, _) => Text(
'${val.toInt()}',
style: const TextStyle(fontSize: 10, color: _h3),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 20,
getTitlesWidget: (val, _) {
final i = val.toInt();
if (i < 0 || i >= controller.chartLabels.length) {
return const SizedBox.shrink();
}
return Text(
controller.chartLabels[i],
style: const TextStyle(fontSize: 10, color: _h3),
);
},
),
),
),
),
);
}
}
... ...
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controllers/trend/trend_types.dart';
import '../../controllers/trend/hrv_controller.dart';
import 'trend_period_tab_bar.dart';
import 'trend_date_range_bar.dart';
/// HRV 趋势内容区(含第2、3层 Tab)
class HrvTrendView extends GetView<HrvController> {
const HrvTrendView({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 第2层:周/月/年
TrendPeriodTabBar(controller: controller),
// 第3层:日期范围
TrendDateRangeBar(controller: controller),
// 图表内容
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
const SizedBox(height: 8),
const _HrvSummaryCard(),
const SizedBox(height: 12),
const _HrvChartCard(),
const SizedBox(height: 180),
],
),
);
}),
),
],
);
}
}
// ─── 摘要卡片 ────────────────────────────────────────────
class _HrvSummaryCard extends GetView<HrvController> {
const _HrvSummaryCard();
static const _h1 = Color(0xFF0F0F11);
static const _h3 = Color(0xFF999999);
static const _brandColor = Color(0xFF845EEE);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final periodLabel = controller.currentPeriod.value.label;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'HRV趋势',
style: TextStyle(
color: _h1,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
const Icon(Icons.info_outline, size: 14, color: _h3),
],
),
const SizedBox(height: 16),
Row(
children: [
_StatItem(
label: '本$periodLabel平均',
value: controller.averageHrv.value,
unit: 'ms',
color: _brandColor,
),
const SizedBox(width: 24),
_StatItem(
label: '较上$periodLabel',
value: controller.changeHrv.value,
unit: 'ms',
color: const Color(0xFF3BD49D),
),
],
),
],
);
}),
);
}
}
class _StatItem extends StatelessWidget {
final String label;
final String value;
final String unit;
final Color color;
const _StatItem({
required this.label,
required this.value,
required this.unit,
required this.color,
});
static const _h2 = Color(0xFF78787D);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(color: _h2, fontSize: 12),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
color: color,
fontSize: 32,
fontWeight: FontWeight.w600,
height: 1.0,
),
),
const SizedBox(width: 2),
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
unit,
style: TextStyle(color: color, fontSize: 14),
),
),
],
),
],
);
}
}
// ─── HRV 折线图卡片 ──────────────────────────────────────
class _HrvChartCard extends GetView<HrvController> {
const _HrvChartCard();
static const _h1 = Color(0xFF0F0F11);
static const _h3 = Color(0xFF999999);
static const _h5 = Color(0xFFCCCCCC);
static const _brandColor = Color(0xFF845EEE);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final period = controller.currentPeriod.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'HRV ${period.label}趋势图',
style: const TextStyle(
color: _h1,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
SizedBox(
height: 180,
child: _buildChart(period),
),
],
);
}),
);
}
Widget _buildChart(TrendPeriod period) {
if (controller.chartData.isEmpty) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)),
),
);
}
final spots = List.generate(
controller.chartData.length,
(i) => FlSpot(i.toDouble(), controller.chartData[i]),
);
return LineChart(
LineChartData(
minX: 0,
maxX: (controller.chartData.length - 1).toDouble(),
minY: 20,
maxY: 80,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 20,
getDrawingHorizontalLine: (_) => const FlLine(
color: _h5,
strokeWidth: 1,
dashArray: [2, 2],
),
),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 20,
getTitlesWidget: (val, _) => Text(
'${val.toInt()}',
style: const TextStyle(fontSize: 10, color: _h3),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 20,
getTitlesWidget: (val, _) {
final i = val.toInt();
if (i < 0 || i >= controller.chartLabels.length) {
return const SizedBox.shrink();
}
return Text(
controller.chartLabels[i],
style: const TextStyle(fontSize: 10, color: _h3),
);
},
),
),
),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
curveSmoothness: 0.3,
color: _brandColor,
barWidth: 2,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
_brandColor.withValues(alpha: 0.15),
_brandColor.withValues(alpha: 0.0),
],
),
),
dotData: FlDotData(
getDotPainter: (spot, percent, barData, index) =>
FlDotCirclePainter(
radius: 4,
color: Colors.white,
strokeWidth: 2.5,
strokeColor: _brandColor,
),
),
),
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipRoundedRadius: 8,
getTooltipColor: (_) => const Color(0xFFF3F3F3),
getTooltipItems: (spots) => spots.map((s) {
return LineTooltipItem(
'${s.y.toInt()}ms',
const TextStyle(
color: _brandColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
);
}).toList(),
),
),
),
);
}
}
... ...
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controllers/trend/trend_types.dart';
import '../../controllers/trend/sleep_controller.dart';
import 'trend_period_tab_bar.dart';
import 'trend_date_range_bar.dart';
/// 睡眠报告趋势内容区(含第2、3层 Tab)
class SleepTrendView extends GetView<SleepController> {
const SleepTrendView({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
TrendPeriodTabBar(controller: controller),
TrendDateRangeBar(controller: controller),
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Color(0xFF845EEE)),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
const SizedBox(height: 8),
const _SleepSummaryCard(),
const SizedBox(height: 12),
const _SleepChartCard(),
const SizedBox(height: 180),
],
),
);
}),
),
],
);
}
}
// ─── 摘要卡片 ─────────────────────────────────────────
class _SleepSummaryCard extends GetView<SleepController> {
const _SleepSummaryCard();
static const _h1 = Color(0xFF0F0F11);
static const _h3 = Color(0xFF999999);
static const _sleepColor = Color(0xFF7B9BFB);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final periodLabel = controller.currentPeriod.value.label;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'睡眠报告趋势',
style: TextStyle(
color: _h1,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
const Icon(Icons.info_outline, size: 14, color: _h3),
],
),
const SizedBox(height: 16),
Row(
children: [
_StatItem(
label: '本$periodLabel均睡眠',
value: controller.averageSleep.value,
unit: 'h',
color: _sleepColor,
),
const SizedBox(width: 24),
_StatItem(
label: '深睡占比',
value: controller.deepSleepRatio.value,
unit: '%',
color: const Color(0xFF845EEE),
),
],
),
],
);
}),
);
}
}
class _StatItem extends StatelessWidget {
final String label;
final String value;
final String unit;
final Color color;
const _StatItem({
required this.label,
required this.value,
required this.unit,
required this.color,
});
static const _h2 = Color(0xFF78787D);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(color: _h2, fontSize: 12)),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
color: color,
fontSize: 32,
fontWeight: FontWeight.w600,
height: 1.0,
),
),
const SizedBox(width: 2),
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(unit, style: TextStyle(color: color, fontSize: 14)),
),
],
),
],
);
}
}
// ─── 睡眠时长折线图卡片 ────────────────────────────────
class _SleepChartCard extends GetView<SleepController> {
const _SleepChartCard();
static const _h1 = Color(0xFF0F0F11);
static const _h3 = Color(0xFF999999);
static const _h5 = Color(0xFFCCCCCC);
static const _sleepColor = Color(0xFF7B9BFB);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Obx(() {
final period = controller.currentPeriod.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'睡眠时长${period.label}趋势图',
style: const TextStyle(
color: _h1,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
SizedBox(
height: 180,
child: _buildChart(),
),
],
);
}),
);
}
Widget _buildChart() {
if (controller.chartData.isEmpty) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Color(0xFF7B9BFB)),
),
);
}
return LineChart(
LineChartData(
minX: 0,
maxX: (controller.chartData.length - 1).toDouble(),
minY: 4,
maxY: 10,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 2,
getDrawingHorizontalLine: (_) => const FlLine(
color: _h5,
strokeWidth: 1,
dashArray: [2, 2],
),
),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 2,
getTitlesWidget: (val, _) => Text(
'${val.toInt()}h',
style: const TextStyle(fontSize: 10, color: _h3),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 20,
getTitlesWidget: (val, _) {
final i = val.toInt();
if (i < 0 || i >= controller.chartLabels.length) {
return const SizedBox.shrink();
}
return Text(
controller.chartLabels[i],
style: const TextStyle(fontSize: 10, color: _h3),
);
},
),
),
),
lineBarsData: [
LineChartBarData(
spots: List.generate(
controller.chartData.length,
(i) => FlSpot(i.toDouble(), controller.chartData[i]),
),
isCurved: true,
curveSmoothness: 0.3,
color: _sleepColor,
barWidth: 2,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
_sleepColor.withValues(alpha: 0.15),
_sleepColor.withValues(alpha: 0.0),
],
),
),
dotData: FlDotData(
getDotPainter: (spot, percent, barData, index) =>
FlDotCirclePainter(
radius: 4,
color: Colors.white,
strokeWidth: 2.5,
strokeColor: _sleepColor,
),
),
),
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipRoundedRadius: 8,
getTooltipColor: (_) => const Color(0xFFF3F3F3),
getTooltipItems: (spots) => spots.map((s) {
return LineTooltipItem(
'${s.y}h',
const TextStyle(
color: _sleepColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
);
}).toList(),
),
),
),
);
}
}
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controllers/trend/trend_period_controller.dart';
/// 第3层:日期范围导航条
/// ← [5月4日 - 5月10日 ↓] →
/// 点击左右箭头切换周期;点击中间日期打开日历选择器
class TrendDateRangeBar extends StatelessWidget {
final TrendPeriodController controller;
const TrendDateRangeBar({super.key, required this.controller});
static const _brandColor = Color(0xFF845EEE);
static const _arrowActiveColor = Color(0xFF0F0F11);
static const _arrowDisabledColor = Color(0xFFCCCCCC);
@override
Widget build(BuildContext context) {
return Obx(
() => SizedBox(
height: 40,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// ← 上一个周期
_ArrowButton(
icon: Icons.chevron_left,
color: _arrowActiveColor,
onTap: controller.goToPrevious,
),
// 日期文字 + 下拉箭头(点击打开日历)
Expanded(
child: GestureDetector(
onTap: () => _showDatePicker(context),
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.dateRangeLabel,
style: const TextStyle(
fontFamily: 'PingFang SC',
fontSize: 14,
fontWeight: FontWeight.w500,
color: _brandColor,
height: 1.4,
),
),
const SizedBox(width: 4),
const Icon(
Icons.keyboard_arrow_down,
size: 12,
color: _brandColor,
),
],
),
),
),
// → 下一个周期(到达今日则禁用)
_ArrowButton(
icon: Icons.chevron_right,
color: controller.canGoNext
? _arrowActiveColor
: _arrowDisabledColor,
onTap: controller.canGoNext ? controller.goToNext : null,
),
],
),
),
),
);
}
void _showDatePicker(BuildContext context) {
// TODO: 弹出自定义日历选择器
// 这里先留空,后续实现
}
}
class _ArrowButton extends StatelessWidget {
final IconData icon;
final Color color;
final VoidCallback? onTap;
const _ArrowButton({
required this.icon,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 40,
height: 40,
child: Icon(icon, size: 28, color: color),
),
);
}
}
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controllers/trend/trend_types.dart';
import '../../controllers/trend/trend_period_controller.dart';
/// 第2层:周 / 月 / 年 胶囊 Tab
/// 设计:整体 rgba(132,94,238,0.2) 胶囊背景,选中项白色圆角块
/// 仅支持点击切换(不滑动,避免与第1层手势冲突)
class TrendPeriodTabBar extends StatelessWidget {
final TrendPeriodController controller;
const TrendPeriodTabBar({super.key, required this.controller});
// Figma token
static const _capsuleBg = Color(0x33845EEE); // rgba(132,94,238,0.2)
static const _selectedBg = Colors.white;
static const _h1 = Color(0xFF0F0F11);
static const _h2 = Color(0xFF78787D);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Obx(
() => Container(
height: 36,
decoration: BoxDecoration(
color: _capsuleBg,
borderRadius: BorderRadius.circular(25),
),
padding: const EdgeInsets.all(4),
child: Row(
children: TrendPeriod.values.map((period) {
final isSelected = controller.currentPeriod.value == period;
return Expanded(
child: GestureDetector(
onTap: () => controller.changePeriod(period),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: isSelected ? _selectedBg : Colors.transparent,
borderRadius: BorderRadius.circular(21),
boxShadow: isSelected
? [
const BoxShadow(
color: Color(0x14000000),
blurRadius: 4,
offset: Offset(0, 1),
)
]
: null,
),
alignment: Alignment.center,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: TextStyle(
fontFamily: 'PingFang SC',
fontSize: 14,
fontWeight: FontWeight.w500,
color: isSelected ? _h1 : _h2,
height: 1.4,
),
child: Text(period.label),
),
),
),
);
}).toList(),
),
),
),
);
}
}
... ...
import 'package:flutter/material.dart';
/// 第1层:HRV心率 / 活动消耗 / 睡眠报告 Tab
/// 使用 Flutter 原生 TabBar 与 TabController 实现极其平滑、跟手的指示线滑动效果
class TrendTypeTabBar extends StatelessWidget {
final TabController tabController;
const TrendTypeTabBar({super.key, required this.tabController});
static const _selectedColor = Color(0xFF0F0F11);
static const _unselectedColor = Color(0xFF78787D);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: TabBar(
controller: tabController,
dividerColor: Colors.transparent, // 移除底部默认分割线
indicatorColor: _selectedColor,
indicatorSize: TabBarIndicatorSize.tab, // 指示线与 Tab 等宽
indicatorWeight: 3, // 指示线粗细为 3px
labelColor: _selectedColor,
unselectedLabelColor: _unselectedColor,
labelStyle: const TextStyle(
fontFamily: 'PingFang SC',
fontSize: 18,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontFamily: 'PingFang SC',
fontSize: 18,
fontWeight: FontWeight.w500,
),
tabs: const [
Tab(text: 'HRV心率'),
Tab(text: '活动消耗'),
Tab(text: '睡眠报告'),
],
),
);
}
}
... ...
import 'package:flutter/material.dart';
/// Raw hex color constants directly from Double Feel Figma Design System.
/// Avoid using these directly in UI. Instead, use `context.colors` to access
/// semantic colors that automatically adjust to light/dark themes.
class AppColors {
AppColors._();
// ==========================================
// 品牌色 (Brand Colors)
// ==========================================
static const primary = Color(0xFF845EEE);
// ==========================================
// 字色与分割线 (Text & Dividers)
// ==========================================
static const textPrimary = Color(0xFF0F0F11);
static const textSecondary = Color(0xFF78787D);
static const textTertiary = Color(0xFFB0B0B6);
static const disabled = Color(0xFFD0D0D2);
static const border = Color(0xFFEBEBED);
// ==========================================
// 图表与功能色 (Chart & Features)
// ==========================================
static const chartPink = Color(0xFFFF5279);
static const chartOrange = Color(0xFFFF9A6E);
static const chartBlue = Color(0xFF7B9AFB);
static const chartGreen = Color(0xFF3BD49D);
static const chartPurple = Color(0xFFA084EF);
static const chartMagenta = Color(0xFFF775E1);
// ==========================================
// 会员色 (VIP Colors)
// ==========================================
static const vipText = Color(0xFF0F0F11);
static const vipGold = Color(0xFFFFCE51);
static const vipPurple = Color(0xFF916DF5);
// ==========================================
// 辅助与背景色 (Auxiliary & Backgrounds)
// ==========================================
static const backgroundLight = Color(0xFFFAFAFE);
static const brandBackgroundLight = Color(0xFFEAE3FF);
// ==========================================
// 渐变基础色 (Gradient Components)
// ==========================================
static const gradientStart = Color(0xFFECE3FF);
static const gradientEnd = Color(0xFFF9F6FF);
}
... ...
import 'package:flutter/material.dart';
import 'app_colors.dart';
/// Semantic colors extension for Double Feel application.
/// Exposes structured colors and gradients supporting Light and Dark modes.
class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
// Brand Colors
final Color primary;
// Text & Border Colors
final Color textPrimary;
final Color textSecondary;
final Color textTertiary;
final Color disabled;
final Color border;
// Chart & Features Colors
final Color chartPink;
final Color chartOrange;
final Color chartBlue;
final Color chartGreen;
final Color chartPurple;
final Color chartMagenta;
// VIP Colors
final Color vipText;
final Color vipGold;
final Color vipPurple;
// Auxiliary Colors
final Color backgroundLight;
final Color brandBackgroundLight;
// Gradients
final LinearGradient brandBackgroundGradient;
const AppColorsExtension({
required this.primary,
required this.textPrimary,
required this.textSecondary,
required this.textTertiary,
required this.disabled,
required this.border,
required this.chartPink,
required this.chartOrange,
required this.chartBlue,
required this.chartGreen,
required this.chartPurple,
required this.chartMagenta,
required this.vipText,
required this.vipGold,
required this.vipPurple,
required this.backgroundLight,
required this.brandBackgroundLight,
required this.brandBackgroundGradient,
});
/// The standard light palette derived directly from Figma.
factory AppColorsExtension.light() {
return const AppColorsExtension(
primary: AppColors.primary,
textPrimary: AppColors.textPrimary,
textSecondary: AppColors.textSecondary,
textTertiary: AppColors.textTertiary,
disabled: AppColors.disabled,
border: AppColors.border,
chartPink: AppColors.chartPink,
chartOrange: AppColors.chartOrange,
chartBlue: AppColors.chartBlue,
chartGreen: AppColors.chartGreen,
chartPurple: AppColors.chartPurple,
chartMagenta: AppColors.chartMagenta,
vipText: AppColors.vipText,
vipGold: AppColors.vipGold,
vipPurple: AppColors.vipPurple,
backgroundLight: AppColors.backgroundLight,
brandBackgroundLight: AppColors.brandBackgroundLight,
brandBackgroundGradient: LinearGradient(
colors: [AppColors.gradientStart, AppColors.gradientEnd],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
);
}
/// Initial dark mode mapping. Semantic mappings adapt nicely when Dark mode is toggled.
factory AppColorsExtension.dark() {
return const AppColorsExtension(
primary: AppColors.primary,
textPrimary: Color(0xFFEEEEEE),
textSecondary: Color(0xFFAAAAAA),
textTertiary: Color(0xFF666666),
disabled: Color(0xFF444444),
border: Color(0xFF333333),
chartPink: AppColors.chartPink,
chartOrange: AppColors.chartOrange,
chartBlue: AppColors.chartBlue,
chartGreen: AppColors.chartGreen,
chartPurple: AppColors.chartPurple,
chartMagenta: AppColors.chartMagenta,
vipText: Color(0xFFEEEEEE),
vipGold: AppColors.vipGold,
vipPurple: AppColors.vipPurple,
backgroundLight: Color(0xFF121214),
brandBackgroundLight: Color(0xFF241C35),
brandBackgroundGradient: LinearGradient(
colors: [Color(0xFF241C35), Color(0xFF1A1525)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
);
}
@override
AppColorsExtension copyWith({
Color? primary,
Color? textPrimary,
Color? textSecondary,
Color? textTertiary,
Color? disabled,
Color? border,
Color? chartPink,
Color? chartOrange,
Color? chartBlue,
Color? chartGreen,
Color? chartPurple,
Color? chartMagenta,
Color? vipText,
Color? vipGold,
Color? vipPurple,
Color? backgroundLight,
Color? brandBackgroundLight,
LinearGradient? brandBackgroundGradient,
}) {
return AppColorsExtension(
primary: primary ?? this.primary,
textPrimary: textPrimary ?? this.textPrimary,
textSecondary: textSecondary ?? this.textSecondary,
textTertiary: textTertiary ?? this.textTertiary,
disabled: disabled ?? this.disabled,
border: border ?? this.border,
chartPink: chartPink ?? this.chartPink,
chartOrange: chartOrange ?? this.chartOrange,
chartBlue: chartBlue ?? this.chartBlue,
chartGreen: chartGreen ?? this.chartGreen,
chartPurple: chartPurple ?? this.chartPurple,
chartMagenta: chartMagenta ?? this.chartMagenta,
vipText: vipText ?? this.vipText,
vipGold: vipGold ?? this.vipGold,
vipPurple: vipPurple ?? this.vipPurple,
backgroundLight: backgroundLight ?? this.backgroundLight,
brandBackgroundLight: brandBackgroundLight ?? this.brandBackgroundLight,
brandBackgroundGradient: brandBackgroundGradient ?? this.brandBackgroundGradient,
);
}
@override
AppColorsExtension lerp(ThemeExtension<AppColorsExtension>? other, double t) {
if (other is! AppColorsExtension) {
return this;
}
return AppColorsExtension(
primary: Color.lerp(primary, other.primary, t)!,
textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!,
textTertiary: Color.lerp(textTertiary, other.textTertiary, t)!,
disabled: Color.lerp(disabled, other.disabled, t)!,
border: Color.lerp(border, other.border, t)!,
chartPink: Color.lerp(chartPink, other.chartPink, t)!,
chartOrange: Color.lerp(chartOrange, other.chartOrange, t)!,
chartBlue: Color.lerp(chartBlue, other.chartBlue, t)!,
chartGreen: Color.lerp(chartGreen, other.chartGreen, t)!,
chartPurple: Color.lerp(chartPurple, other.chartPurple, t)!,
chartMagenta: Color.lerp(chartMagenta, other.chartMagenta, t)!,
vipText: Color.lerp(vipText, other.vipText, t)!,
vipGold: Color.lerp(vipGold, other.vipGold, t)!,
vipPurple: Color.lerp(vipPurple, other.vipPurple, t)!,
backgroundLight: Color.lerp(backgroundLight, other.backgroundLight, t)!,
brandBackgroundLight: Color.lerp(brandBackgroundLight, other.brandBackgroundLight, t)!,
brandBackgroundGradient: LinearGradient.lerp(brandBackgroundGradient, other.brandBackgroundGradient, t)!,
);
}
}
... ...
import 'package:flutter/material.dart';
import 'app_colors_extension.dart';
/// Class managing the Application ThemeData for both Light and Dark themes.
/// Automatically hooks up our custom Figma colors system as a ThemeExtension.
class AppTheme {
AppTheme._();
/// The standard light theme configuration.
static ThemeData get lightTheme {
final colors = AppColorsExtension.light();
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primaryColor: colors.primary,
scaffoldBackgroundColor: colors.backgroundLight,
// Clean modern AppBar theme using Figma colors
appBarTheme: AppBarTheme(
backgroundColor: colors.backgroundLight,
elevation: 0,
centerTitle: true,
iconTheme: IconThemeData(color: colors.textPrimary),
actionsIconTheme: IconThemeData(color: colors.textPrimary),
titleTextStyle: TextStyle(
color: colors.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
// Configure default ColorScheme using Figma specs
colorScheme: ColorScheme.light(
primary: colors.primary,
secondary: colors.primary,
surface: colors.backgroundLight,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: colors.textPrimary,
outline: colors.border,
),
// Register the custom theme extension
extensions: [
colors,
],
);
}
/// Initial dark theme configuration.
static ThemeData get darkTheme {
final colors = AppColorsExtension.dark();
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
primaryColor: colors.primary,
scaffoldBackgroundColor: colors.backgroundLight,
appBarTheme: AppBarTheme(
backgroundColor: colors.backgroundLight,
elevation: 0,
centerTitle: true,
iconTheme: IconThemeData(color: colors.textPrimary),
actionsIconTheme: IconThemeData(color: colors.textPrimary),
titleTextStyle: TextStyle(
color: colors.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
colorScheme: ColorScheme.dark(
primary: colors.primary,
secondary: colors.primary,
surface: colors.backgroundLight,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: colors.textPrimary,
outline: colors.border,
),
extensions: [
colors,
],
);
}
}
/// Helper extension to easily access custom Figma colors inside widgets
/// by using `context.colors.<semanticName>` instead of verbose lookups.
extension AppThemeContextExtension on BuildContext {
AppColorsExtension get colors => Theme.of(this).extension<AppColorsExtension>()!;
ThemeData get theme => Theme.of(this);
}
... ...
... ... @@ -62,8 +62,7 @@ import 'app_localizations_zh.dart';
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
... ... @@ -71,8 +70,7 @@ abstract class AppLocalizations {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
... ... @@ -84,8 +82,7 @@ abstract class AppLocalizations {
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
... ... @@ -528,8 +525,7 @@ abstract class AppLocalizations {
///
/// In zh, this message translates to:
/// **'你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'**
String
get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired;
String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired;
/// No description provided for @bindPartnerTitle.
///
... ... @@ -610,8 +606,7 @@ abstract class AppLocalizations {
String get onboardingResearchGoodSleep;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
... ... @@ -620,25 +615,25 @@ class _AppLocalizationsDelegate
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'zh'].contains(locale.languageCode);
bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'zh':
return AppLocalizationsZh();
case 'en': return AppLocalizationsEn();
case 'zh': return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.');
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'
);
}
... ...
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
... ... @@ -69,12 +67,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings => 'Settings';
@override
String get onboardingIntroTitle =>
'DoubleFeel is a health companion app built for Apple Watch';
String get onboardingIntroTitle => 'DoubleFeel is a health companion app built for Apple Watch';
@override
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>';
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>';
@override
String get onboardingStateQuestion => 'Which of these often happens to you?';
... ... @@ -86,19 +82,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingStateTired => 'I get tired easily';
@override
String get onboardingStatePoorRest =>
'I wake up but still do not feel rested';
String get onboardingStatePoorRest => 'I wake up but still do not feel rested';
@override
String get onboardingStateNeedStimulants =>
'I rely on cigarettes, alcohol, coffee, or other stimulants to stay alert';
String get onboardingStateNeedStimulants => 'I rely on cigarettes, alcohol, coffee, or other stimulants to stay alert';
@override
String get onboardingStateNone => 'None of the above';
@override
String get onboardingStressGoalQuestion =>
'What do you want to learn by understanding stress?';
String get onboardingStressGoalQuestion => 'What do you want to learn by understanding stress?';
@override
String get onboardingStressGoalSource => 'Understand where stress comes from';
... ... @@ -107,8 +100,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingStressGoalReminder => 'Get reminded when stress appears';
@override
String get onboardingStressGoalLovedOnes =>
'Let people who care about me know my stress state';
String get onboardingStressGoalLovedOnes => 'Let people who care about me know my stress state';
@override
String get onboardingStressGoalRelax => 'Understand stress and feel lighter';
... ... @@ -117,8 +109,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingStressGoalBodyTalk => 'Communicate better with my body';
@override
String get onboardingReliefQuestion =>
'Which methods do you think can ease stress?';
String get onboardingReliefQuestion => 'Which methods do you think can ease stress?';
@override
String get onboardingReliefSleep => 'Regular sleep';
... ... @@ -142,8 +133,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingKeyDataTitle => 'Did you know?';
@override
String get onboardingKeyDataSubtitle =>
'Everyone has a magical and important body metric that can help us:';
String get onboardingKeyDataSubtitle => 'Everyone has a magical and important body metric that can help us:';
@override
String get onboardingKeyDataStress => 'Monitor stress';
... ... @@ -158,8 +148,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingKeyDataHabits => 'Build healthy habits';
@override
String get onboardingKeyDataLovedOnes =>
'Help important people care about your state in time';
String get onboardingKeyDataLovedOnes => 'Help important people care about your state in time';
@override
String get onboardingTellMeWhatItIs => 'Tell me what it is!';
... ... @@ -168,19 +157,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingHrvTitle => 'It is HRV, heart rate variability';
@override
String get onboardingHrvSubtitle =>
'It helps us measure overall stress and health';
String get onboardingHrvSubtitle => 'It helps us measure overall stress and health';
@override
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.';
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.';
@override
String get onboardingTellMeMore => 'Tell me more';
@override
String get onboardingResearchTitle =>
'Many studies show that HRV changes are closely related to how our body and mind feel';
String get onboardingResearchTitle => 'Many studies show that HRV changes are closely related to how our body and mind feel';
@override
String get onboardingResearchFatigue => 'Physical fatigue';
... ... @@ -198,30 +184,25 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingHealthPermissionTitle => 'Allow health data access';
@override
String get onboardingHealthPermissionBody =>
'DoubleFeel needs connected wearable health data to send reminders, count stress moments, and provide suggestions.';
String get onboardingHealthPermissionBody => 'DoubleFeel needs connected wearable health data to send reminders, count stress moments, and provide suggestions.';
@override
String get onboardingHealthPermissionPrivacy =>
'Your health data is stored locally. We do not upload any related data.';
String get onboardingHealthPermissionPrivacy => 'Your health data is stored locally. We do not upload any related data.';
@override
String get onboardingNotificationTitle => 'Turn on notifications';
@override
String get onboardingNotificationSubtitle =>
'Learn about every body change in time';
String get onboardingNotificationSubtitle => 'Learn about every body change in time';
@override
String get onboardingNotificationBody =>
'After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.';
String get onboardingNotificationBody => 'After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.';
@override
String get onboardingMemberTitle => 'Get an annual membership offer';
@override
String get onboardingMemberBody =>
'Start your pressure alert and health companion journey, so love and care are always present.';
String get onboardingMemberBody => 'Start your pressure alert and health companion journey, so love and care are always present.';
@override
String get onboardingMemberOriginalPrice => 'Original ¥72.00/year';
... ... @@ -236,16 +217,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingMemberAllOptions => 'View all purchase options';
@override
String get healthCompanionIsNowAvailable =>
'Health Companion is now available';
String get healthCompanionIsNowAvailable => 'Health Companion is now available';
@override
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.';
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.';
@override
String get bindPartnerTitle =>
'Add a Close Contact\nOne more person to care about your health';
String get bindPartnerTitle => 'Add a Close Contact\nOne more person to care about your health';
@override
String get bindPartnerMyId => 'My ID';
... ...
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
... ... @@ -72,8 +70,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get onboardingIntroTitle => 'DoubleFeel 是专为 Apple Watch 打造的健康陪伴app';
@override
String get onboardingIntroBody =>
'我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>';
String get onboardingIntroBody => '我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>';
@override
String get onboardingStateQuestion => '请问以下哪些描述,经常发生在你身上?';
... ... @@ -163,8 +160,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get onboardingHrvSubtitle => '它能帮助我们衡量整体的压力和健康状态';
@override
String get onboardingHrvDescription =>
'心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力';
String get onboardingHrvDescription => '心率变异性(HRV, Heart Rate Variability)即心跳之间间隔时间的微小变化,反映了自主神经系统活动和身体对压力的反应能力';
@override
String get onboardingTellMeMore => '展开说说';
... ... @@ -188,12 +184,10 @@ class AppLocalizationsZh extends AppLocalizations {
String get onboardingHealthPermissionTitle => '允许访问健康数据';
@override
String get onboardingHealthPermissionBody =>
'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。';
String get onboardingHealthPermissionBody => 'DoubleFeel需要连接健康穿戴设备数据,以提醒、统计压力时刻、提供建议。';
@override
String get onboardingHealthPermissionPrivacy =>
'请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。';
String get onboardingHealthPermissionPrivacy => '请放心,你的健康数据只会存储在本地,我们不上传任何相关数据。';
@override
String get onboardingNotificationTitle => '开启通知';
... ... @@ -202,8 +196,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get onboardingNotificationSubtitle => '及时了解身体每一次异动';
@override
String get onboardingNotificationBody =>
'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态';
String get onboardingNotificationBody => 'AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态';
@override
String get onboardingMemberTitle => '获得年度会员优惠';
... ... @@ -227,9 +220,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get healthCompanionIsNowAvailable => '健康陪伴已开启';
@override
String
get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired =>
'你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。';
String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired => '你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。';
@override
String get bindPartnerTitle => '添加亲密联系人\n多一个人关注你的健康';
... ...
... ... @@ -133,10 +133,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
... ... @@ -149,10 +149,10 @@ packages:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.1"
code_builder:
dependency: transitive
description:
... ... @@ -165,10 +165,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.19.1"
version: "1.19.0"
convert:
dependency: transitive
description:
... ... @@ -237,10 +237,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
version: "1.3.1"
ffi:
dependency: transitive
description:
... ... @@ -418,7 +418,7 @@ packages:
dependency: transitive
description:
path: image_cropper_for_web
ref: "65c2c99891882ea59732959a672f3d5993a837bb"
ref: "br_v9.1.0_ohos"
resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb"
url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git"
source: git
... ... @@ -427,7 +427,7 @@ packages:
dependency: transitive
description:
path: image_cropper_platform_interface
ref: "65c2c99891882ea59732959a672f3d5993a837bb"
ref: "br_v9.1.0_ohos"
resolved-ref: "65c2c99891882ea59732959a672f3d5993a837bb"
url: "https://gitcode.com/openharmony-sig/fluttertpc_image_cropper.git"
source: git
... ... @@ -509,10 +509,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.20.2"
version: "0.19.0"
io:
dependency: transitive
description:
... ... @@ -549,26 +549,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.1"
lints:
dependency: transitive
description:
... ... @@ -597,10 +597,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
... ... @@ -613,10 +613,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.15.0"
mime:
dependency: transitive
description:
... ... @@ -645,10 +645,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.9.0"
path_provider:
dependency: transitive
description:
... ... @@ -982,18 +982,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.2"
stream_transform:
dependency: transitive
description:
... ... @@ -1022,10 +1022,10 @@ packages:
dependency: "direct main"
description:
name: table_calendar
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.1.3"
term_glyph:
dependency: transitive
description:
... ... @@ -1038,10 +1038,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.3"
timing:
dependency: transitive
description:
... ... @@ -1070,10 +1070,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.1.4"
vm_service:
dependency: transitive
description:
... ... @@ -1127,8 +1127,8 @@ packages:
dependency: transitive
description:
path: "packages/webview_flutter/webview_flutter_android"
ref: de942e79c9057b32ad31106508bd87c0d60aef83
resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83
ref: "br_webview_flutter-v4.13.0_ohos"
resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba"
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
source: git
version: "4.7.0"
... ... @@ -1136,8 +1136,8 @@ packages:
dependency: transitive
description:
path: "packages/webview_flutter/webview_flutter_ohos"
ref: de942e79c9057b32ad31106508bd87c0d60aef83
resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83
ref: "br_webview_flutter-v4.13.0_ohos"
resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba"
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
source: git
version: "4.7.0"
... ... @@ -1145,8 +1145,8 @@ packages:
dependency: transitive
description:
path: "packages/webview_flutter/webview_flutter_platform_interface"
ref: de942e79c9057b32ad31106508bd87c0d60aef83
resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83
ref: "br_webview_flutter-v4.13.0_ohos"
resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba"
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
source: git
version: "2.13.1"
... ... @@ -1154,8 +1154,8 @@ packages:
dependency: transitive
description:
path: "packages/webview_flutter/webview_flutter_wkwebview"
ref: de942e79c9057b32ad31106508bd87c0d60aef83
resolved-ref: de942e79c9057b32ad31106508bd87c0d60aef83
ref: "br_webview_flutter-v4.13.0_ohos"
resolved-ref: "8d70938aa190eb7d7cd4d66f0d16fddb3c2d03ba"
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
source: git
version: "3.22.0"
... ... @@ -1176,5 +1176,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.0-0 <4.0.0"
dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.0"
... ...