Commit 65e173994d961d1a3c4798c0cee64d5a7ba43239

Authored by 常守达
1 parent 68edda64

fix(today):看板问题修复

... ... @@ -84,7 +84,7 @@ class BindPartnerController extends GetxController {
void onShareMyCode() async {
try {
ta.track('enter_doublefeel_add_friend_page',
ta.track('click_doublefeel_add_friend_page',
properties: {'channel_type': channel, 'click_content': '分享我的码'});
final success = await PlatformHostApi()
.shareText(l10n.sharePartnerCodeTemplate(myInviteCode.value));
... ... @@ -94,7 +94,7 @@ class BindPartnerController extends GetxController {
}
Future<void> onSubmitPartnerId() async {
ta.track('enter_doublefeel_add_friend_page',
ta.track('click_doublefeel_add_friend_page',
properties: {'channel_type': channel, 'click_content': '继续'});
if (!canSubmit || isSubmitting.value) return;
isSubmitting.value = true;
... ... @@ -152,7 +152,7 @@ class BindPartnerController extends GetxController {
}
Future<void> onSkip() async {
ta.track('enter_doublefeel_add_friend_page',
ta.track('click_doublefeel_add_friend_page',
properties: {'channel_type': channel, 'click_content': '下次再说'});
final userStateService = Get.find<UserStateService>();
if (!userStateService.isVip) {
... ...
... ... @@ -44,33 +44,13 @@ class BindPartnerView extends GetView<BindPartnerController> {
physics: const ClampingScrollPhysics(),
children: [
_buildTitle(context),
SizedBox(height: 28),
// SizedBox(height: 28),
_buildCardArea(context),
SizedBox(height: 58),
// SizedBox(height: 58),
if (controller.isShowSkipButton) _buildSkipButton(context),
],
),
),
Positioned(
top: MediaQuery.paddingOf(context).top + 92,
left: 0,
child: Image.asset(
'assets/images/user_onboarding/ic_bind_decor_top.png',
width: 133,
height: 124,
fit: BoxFit.fitHeight,
),
),
Positioned(
top: MediaQuery.paddingOf(context).top + 532,
right: 0,
child: Image.asset(
'assets/images/user_onboarding/ic_bind_decor_bottom.png',
width: 133,
height: 124,
fit: BoxFit.fitHeight,
),
),
],
),
),
... ... @@ -116,48 +96,73 @@ class BindPartnerView extends GetView<BindPartnerController> {
}
Widget _buildCardArea(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
image: DecorationImage(
image:
AssetImage('assets/images/friends/bg_friend_add_subtract.webp'),
fit: BoxFit.cover,
return Stack(
children: [
Container(
margin: EdgeInsets.only(top: 28, bottom: 64, left: 24, right: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
image: DecorationImage(
image: AssetImage(
'assets/images/friends/bg_friend_add_subtract.webp'),
fit: BoxFit.cover,
),
boxShadow: [
BoxShadow(
color: context.colors.primary.withValues(alpha: 0.08),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
SizedBox(height: 28),
_buildMyInfoSection(context),
SizedBox(height: 40),
_buildOrDivider(context),
SizedBox(height: 28),
_buildInputSection(context),
SizedBox(height: 16),
// 继续按钮
Container(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Obx(() {
return _buildPrimaryButton(
label: context.l10n.continueButton,
onTap: controller.onSubmitPartnerId,
enabled:
controller.canSubmit && !controller.isSubmitting.value,
isLoading: controller.isSubmitting.value,
primaryColor: context.colors.chartOrange,
);
}),
),
SizedBox(height: 40),
],
),
),
boxShadow: [
BoxShadow(
color: context.colors.primary.withValues(alpha: 0.08),
blurRadius: 24,
offset: const Offset(0, 8),
Positioned(
top: 0,
left: 0,
child: Image.asset(
'assets/images/user_onboarding/ic_bind_decor_top.png',
width: 133,
height: 124,
fit: BoxFit.fitHeight,
),
],
),
child: Column(
children: [
SizedBox(height: 28),
_buildMyInfoSection(context),
SizedBox(height: 40),
_buildOrDivider(context),
SizedBox(height: 28),
_buildInputSection(context),
SizedBox(height: 16),
// 继续按钮
Container(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Obx(() {
return _buildPrimaryButton(
label: context.l10n.continueButton,
onTap: controller.onSubmitPartnerId,
enabled: controller.canSubmit && !controller.isSubmitting.value,
isLoading: controller.isSubmitting.value,
primaryColor: context.colors.chartOrange,
);
}),
),
Positioned(
bottom: 0,
right: 0,
child: Image.asset(
'assets/images/user_onboarding/ic_bind_decor_bottom.png',
width: 133,
height: 124,
fit: BoxFit.fitHeight,
),
SizedBox(height: 40),
],
),
),
],
);
}
... ... @@ -199,7 +204,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
ClipboardData(text: controller.myInviteCode.value),
);
AppToast.show('复制成功');
ta.track('enter_doublefeel_add_friend_page', properties: {
ta.track('click_doublefeel_add_friend_page', properties: {
'channel_type': controller.channel,
'click_content': '复制共享码'
});
... ... @@ -294,24 +299,45 @@ class BindPartnerView extends GetView<BindPartnerController> {
border: Border.all(color: const Color(0xFFF0F0F6), width: 1),
borderRadius: BorderRadius.circular(27),
),
child: TextField(
controller: controller.partnerIdController,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w400,
color: context.colors.textPrimary,
),
decoration: InputDecoration(
hintText: context.l10n.bindPartnerInputHint,
hintStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: const Color(0xFFD0D0D2),
child: Stack(
alignment: Alignment.center, // 确保 Stack 里的子项绝对居中
children: [
// 自定义 Hint 文字层
Obx(() => controller.partnerIdInput.value.isEmpty
? IgnorePointer(
child: Text(
context.l10n.bindPartnerInputHint,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Color(0xFFD0D0D2),
height: 1.1,
),
),
)
: const SizedBox()),
// 真正的输入框层(不设 hintText)
TextField(
controller: controller.partnerIdController,
textAlign: TextAlign.center,
textAlignVertical: TextAlignVertical.center,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w400,
color: context.colors.textPrimary,
height: 1.1,
),
cursorWidth: 2.0,
cursorHeight: 24.0,
cursorRadius: const Radius.circular(1.5),
decoration: const InputDecoration(
border: InputBorder.none,
isCollapsed: true,
contentPadding:
EdgeInsets.symmetric(horizontal: 20, vertical: 0),
),
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
),
],
),
),
],
... ...
... ... @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_cont
import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/constants/membership.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
... ... @@ -190,35 +191,42 @@ class TodayController extends GetMaterialController {
}
void checkAddFriendVisible() {
_refreshFriendList(onFriendListUpdated: () {
if (friendsList.isNotEmpty ||
friendsList.value.length >= friendsListLimit) {
showPartnerAdBanner.value = false;
return;
}
// 关闭后 3 天内不再显示
final userId =
Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
0;
final lastShowTime =
Get.find<UserAccountStorage>().getLastAddFriendBannerShowTime(userId);
if (lastShowTime != null) {
final diff = DateTime.now()
.difference(DateTime.fromMillisecondsSinceEpoch(lastShowTime));
if (diff.inDays < 3) {
if (isFriend) {
showPartnerAdBanner.value = false;
} else {
_refreshFriendList(onFriendListUpdated: () {
if (friendsList.isNotEmpty ||
friendsList.value.length >= friendsListLimit) {
showPartnerAdBanner.value = false;
return;
}
}
showPartnerAdBanner.value = true;
});
// 关闭后 3 天内不再显示
final userId = Get.find<UserPreferencesStorage>()
.preferences
.value
.meUserInfo
?.id ??
0;
final lastShowTime = Get.find<UserAccountStorage>()
.getLastAddFriendBannerShowTime(userId);
if (lastShowTime != null) {
final diff = DateTime.now()
.difference(DateTime.fromMillisecondsSinceEpoch(lastShowTime));
if (diff.inDays < 3) {
showPartnerAdBanner.value = false;
return;
}
}
showPartnerAdBanner.value = true;
});
}
}
void changeDate(DateTime date, {DateTime? focused}) {
final normalizedDate = _clampDate(date);
if (!DateUtils.isSameDay(date, lastSelectableDay) &&
!_userStateService.isVip) {
toPremiumPage();
toPremiumPage('今日-查看非当日数据');
return;
}
selectedDate.value = normalizedDate;
... ... @@ -334,20 +342,40 @@ class TodayController extends GetMaterialController {
Future<void> checkHealthDataAuthCardVisible(
[HealthAuthorization? preResult]) async {
try {
final result = preResult ?? await _hostApi.checkHealthAppAuthorization();
ta.setSuperProperties(
{'health_data_permission': result.status.clamp(0, 1)});
if (result.status == 1) {
await _performDataUpload();
}
final hasUploaded =
switch (await _healthApi.getHealthDataEverUploaded()) {
AppSuccess(:final data) => data,
AppFailure() => false,
};
showHealthDataAuthCardStatus.value = !hasUploaded ? -1 : result.status;
} catch (e) {}
final firstTime = DateUtils.dateOnly(
DateTime(lastSelectableDay.year, lastSelectableDay.month - 6,
lastSelectableDay.day),
);
if (isFriend) {
showHealthDataAuthCardStatus.value = 1;
firstSelectableDay.value = firstTime;
} else {
try {
final result =
preResult ?? await _hostApi.checkHealthAppAuthorization();
ta.setSuperProperties(
{'health_data_permission': result.status.clamp(0, 1)});
if (result.status == 1) {
await _performDataUpload();
}
bool hasUploaded = false;
switch (await _healthApi.getHealthDataEverUploaded()) {
case AppSuccess(:final data):
hasUploaded = data.flag ?? false;
if (hasUploaded && data.startTime != null && data.startTime! > 0) {
final firstDayTime = DateUtils.dateOnly(
DateTime.fromMillisecondsSinceEpoch(data.startTime! * 1000),
);
if (firstDayTime.isBefore(firstTime)) {
firstSelectableDay.value = firstDayTime;
}
}
case AppFailure():
hasUploaded = false;
}
showHealthDataAuthCardStatus.value = !hasUploaded ? -1 : result.status;
} catch (e) {}
}
}
Future<void> checkNotificationCardVisible() async {
... ... @@ -388,12 +416,6 @@ class TodayController extends GetMaterialController {
switch (await _healthApi.getV2StressScore(friendUserId, intDate)) {
case AppSuccess(:final data):
v2StressScore.value = data;
// var nextInt = Random().nextInt(4);
// v2StressScore.value = V2StressScore(
// state: nextInt,
// comprehensiveScore: 100 - (nextInt * 25),
// );
case AppFailure():
v2StressScore.value = null;
}
... ... @@ -408,25 +430,6 @@ class TodayController extends GetMaterialController {
switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) {
case AppSuccess(:final data):
hrvChartData.assignAll(data.list ?? []);
// hrvChartData.assignAll([
// // 每小时一条,00:00 ~ 15:00(CST 午夜 = 1782057600)
// V2HrvTrendItem(time: 1782057600, trendHrv: 45, state: 3), // 00:00
// V2HrvTrendItem(time: 1782061200, trendHrv: 42, state: 3), // 01:00
// V2HrvTrendItem(time: 1782064800, trendHrv: 38, state: 3), // 02:00
// V2HrvTrendItem(time: 1782068400, trendHrv: 35, state: 2), // 03:00
// V2HrvTrendItem(time: 1782072000, trendHrv: 40, state: 3), // 04:00
// V2HrvTrendItem(time: 1782075600, trendHrv: 55, state: 4), // 05:00
// V2HrvTrendItem(time: 1782079200, trendHrv: 65, state: 4), // 06:00
// V2HrvTrendItem(time: 1782082800, trendHrv: 72, state: 4), // 07:00
// V2HrvTrendItem(time: 1782086400, trendHrv: 68, state: 4), // 08:00
// V2HrvTrendItem(time: 1782090000, trendHrv: 58, state: 4), // 09:00
// V2HrvTrendItem(time: 1782093600, trendHrv: 52, state: 3), // 10:00
// V2HrvTrendItem(time: 1782097200, trendHrv: 48, state: 3), // 11:00
// V2HrvTrendItem(time: 1782100800, trendHrv: 44, state: 3), // 12:00
// V2HrvTrendItem(time: 1782104400, trendHrv: 38, state: 2), // 13:00
// V2HrvTrendItem(time: 1782108000, trendHrv: 42, state: 3), // 14:00
// V2HrvTrendItem(time: 1782111600, trendHrv: 45, state: 3), // 15:00
// ]);
case AppFailure():
hrvChartData.clear();
}
... ... @@ -434,40 +437,6 @@ class TodayController extends GetMaterialController {
switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) {
case AppSuccess(:final data):
stressChartData.assignAll(data.list ?? []);
// stressChartData.assignAll(
// // 每 6 分钟一条,00:00 ~ 15:00(共 150 条)
// // midnight CST = 1782057600,步长 360s
// List.generate(150, (i) {
// const midnight = 1782057600;
// final ts = midnight + i * 360;
// final minutesFromMidnight = i * 6;
// final hour = minutesFromMidnight ~/ 60;
// final minInHour = minutesFromMidnight % 60;
// // 用正弦波模拟真实波动,早高峰(7-9点)压力最高
// final base = hour < 6
// ? 25.0 // 深夜低压力
// : hour < 9
// ? 55.0 + (hour - 6) * 10.0 // 早高峰爬升
// : hour < 12
// ? 75.0 - (hour - 9) * 8.0 // 上午下降
// : 50.0 - (hour - 12) * 3.0; // 下午缓降
// // 叠加微小波动(用 i 模拟随机)
// final jitter = (i % 7 - 3) * 2.0 + (minInHour % 3) * 1.5;
// final value = (base + jitter).clamp(10.0, 100.0).round();
// final int state;
// if (value >= 75) {
// state = 1;
// } else if (value >= 55) {
// state = 2;
// } else if (value >= 35) {
// state = 3;
// } else {
// state = 4;
// }
// return V2RealtimeStressItem(time: ts, value: value, state: state);
// }),
// );
case AppFailure():
stressChartData.clear();
}
... ... @@ -479,12 +448,6 @@ class TodayController extends GetMaterialController {
v2ActivityTarget.value = null;
}
}
// Future.delayed(const Duration(milliseconds: 5000), () {
// firstSelectableDay.value = DateUtils.dateOnly(
// DateTime(
// DateTime.now().year, DateTime.now().month, DateTime.now().day - 1),
// );
// });
}
void _clearRealTimeData() {
... ... @@ -588,8 +551,11 @@ class TodayController extends GetMaterialController {
RxList<FriendItem> friendsList = RxList.empty();
int friendsListLimit = 10;
Future<void> toPremiumPage() async {
await Get.toNamed(Routes.PURCHASE);
Future<void> toPremiumPage(String channelType) async {
await Get.toNamed(
Routes.PURCHASE,
arguments: {IntentKeys.channelType: channelType},
);
final vipResult = await _vipApi.getVipInfo();
if (vipResult case AppSuccess(data: final vip)) {
try {
... ... @@ -688,4 +654,11 @@ class TodayController extends GetMaterialController {
unawaited(loadDataForDate(selectedDate.value));
}
}
Future<void> refreshTab() async {
checkAddFriendVisible();
checkHrvAdBannerVisible();
checkHealthDataAuthCardVisible();
await loadDataForDate(selectedDate.value);
}
}
... ...
... ... @@ -60,7 +60,11 @@ class TodayTabBody extends StatelessWidget {
}
return false;
},
child: _buildDayScrollView(context, pinnedHeaderHeight),
child: RefreshIndicator(
edgeOffset: pinnedHeaderHeight,
onRefresh: () async => controller.refreshTab(),
child: _buildDayScrollView(context, pinnedHeaderHeight),
),
),
Positioned(
top: 0,
... ... @@ -121,7 +125,9 @@ class TodayTabBody extends StatelessWidget {
) {
return CustomScrollView(
controller: controller.scrollController,
physics: const ClampingScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(
parent: ClampingScrollPhysics(),
),
slivers: [
SliverToBoxAdapter(
child: Container(
... ... @@ -322,6 +328,7 @@ class TodayTabBody extends StatelessWidget {
child: SizedBox(
height: _weekRowsHeight,
child: TableCalendar(
key: ValueKey(controller.firstSelectableDay.value),
calendarFormat: CalendarFormat.week,
headerVisible: false,
daysOfWeekHeight: _weekRowsHeight / 2,
... ... @@ -375,7 +382,7 @@ class TodayTabBody extends StatelessWidget {
GestureDetector(
onTap: () async {
if (!Get.find<UserStateService>().isVip) {
controller.toPremiumPage();
controller.toPremiumPage('今日-查看非当日数据');
return;
}
final picked = await showReportDatePickerSheet(
... ...
... ... @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -33,7 +34,7 @@ class FriendSelectBottomSheet extends StatelessWidget {
children: [
Center(
child: Text(
'选择好友',
l10n.friendsSelect,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
... ... @@ -79,6 +80,7 @@ class FriendSelectBottomSheet extends StatelessWidget {
friend: friend,
isSelected: isSelected,
onTap: () {
Get.back();
controller.selectFriend(friend);
},
);
... ...
... ... @@ -151,7 +151,7 @@ class PremiumCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => controller.toPremiumPage(),
onTap: () => controller.toPremiumPage('今日-会员广告'),
behavior: HitTestBehavior.opaque,
child: Text(
context.l10n.allPlans,
... ...
... ... @@ -256,6 +256,7 @@ class _ContactText extends StatelessWidget {
child: Text(
l10n.todayFaqContactAction,
style: TextStyle(
fontSize: 12,
color: context.colors.primary,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
... ...
... ... @@ -67,7 +67,7 @@ class TodayHealthDataAuthCard extends StatelessWidget {
style: TextStyle(
color: colors.primary,
fontSize: 12,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
... ...
... ... @@ -238,7 +238,7 @@ class TodayHrvChartCard extends StatelessWidget {
DateTime selectedDate,
bool isVip) {
void onTapPurchase() {
controller.toPremiumPage();
controller.toPremiumPage('今日-实时压力-立即解锁');
}
// 槽时间秒 → state 映射(与 _buildStressBarGroups 一致:取 value 最大那条 of state)
... ... @@ -384,8 +384,17 @@ class TodayHrvChartCard extends StatelessWidget {
List<TextSpan>? _getTooltip(BarChartRodData rod, int x, int? state) {
return [
TextSpan(
text: '○ ${_getStatusByState(state)} ${_formatNumber(rod.toY)}%',
text: '○ ',
style: TextStyle(
fontSize: 14,
color: _getColorByState(state),
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: '${_getStatusByState(state)} ${_formatNumber(rod.toY)}%',
style: TextStyle(
fontSize: 18,
color: _getColorByState(state),
fontWeight: FontWeight.bold,
),
... ... @@ -398,9 +407,18 @@ class TodayHrvChartCard extends StatelessWidget {
),
),
TextSpan(
text: '♥ ${l10n.pressure} ${_formatHour(x.toDouble())}',
text: '♥ ',
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: '${l10n.realTimePressure} · ${_formatHour(x.toDouble())}',
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
... ... @@ -410,7 +428,7 @@ class TodayHrvChartCard extends StatelessWidget {
List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot, int? state) {
return [
TextSpan(
text: '○ ${_getStatusByState(state)}',
text: '○ ',
style: TextStyle(
color: _getColorByState(state),
// fontWeight: FontWeight.bold,
... ... @@ -418,6 +436,14 @@ class TodayHrvChartCard extends StatelessWidget {
),
),
TextSpan(
text: _getStatusByState(state),
style: TextStyle(
color: _getColorByState(state),
// fontWeight: FontWeight.bold,
fontSize: 18,
),
),
TextSpan(
text: '\n',
style: TextStyle(
color: Colors.white,
... ... @@ -425,7 +451,15 @@ class TodayHrvChartCard extends StatelessWidget {
),
),
TextSpan(
text: '♥ HRV ${_formatNumber(flSpot.y)}ms · ${_formatHour(flSpot.x)}',
text: '♥ ',
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: 'HRV ${_formatNumber(flSpot.y)}ms · ${_formatHour(flSpot.x)}',
style: const TextStyle(
color: Color(0xFF78787D),
fontWeight: FontWeight.bold,
... ... @@ -574,6 +608,7 @@ class _HrvLineChartState extends State<_HrvLineChart> {
],
lineTouchData: LineTouchData(
handleBuiltInTouches: false,
touchSpotThreshold: 20.0,
touchCallback:
(FlTouchEvent event, LineTouchResponse? touchResponse) {
if (event is FlPanStartEvent ||
... ... @@ -588,6 +623,10 @@ class _HrvLineChartState extends State<_HrvLineChart> {
setState(() {
_hrvShowingSpots = touchResponse.lineBarSpots!;
});
} else {
setState(() {
_hrvShowingSpots = [];
});
}
} else if (event is FlPanEndEvent ||
event is FlTapUpEvent ||
... ... @@ -700,11 +739,11 @@ class _StressBarChartState extends State<_StressBarChart> {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// 右轴 reservedSize=22,剩余宽度平分给所有槽
const rightAxisWidth = 22.0;
// 右轴 reservedSize=22,不占用空间设为 0.0
const rightAxisWidth = 0.0;
const slotSec = 6 * 60;
final slotCount = (widget.maxSeconds / slotSec).ceil().clamp(1, 9999);
const double groupsSpace = 0; // 设置间隔为 1 像素
const double groupsSpace = 0.25; // 设置间隔为 1 像素
final barWidth = (constraints.maxWidth -
rightAxisWidth -
(groupsSpace * (slotCount - 1))) /
... ... @@ -822,11 +861,16 @@ class _StressBarChartState extends State<_StressBarChart> {
}
}
final chartHeight = constraints.maxHeight;
const gridTop = 18.0;
final gridBottom = chartHeight - 20.0;
final gridHeight = gridBottom - gridTop;
return Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.only(top: 6),
Container(
margin: const EdgeInsets.only(top: gridTop),
child: BarChart(
BarChartData(
minY: 0,
... ... @@ -868,28 +912,12 @@ class _StressBarChartState extends State<_StressBarChart> {
sideTitles: SideTitles(showTitles: false)),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 50,
reservedSize: 22,
getTitlesWidget: (val, _) {
if (val < 0 || val > 100) {
return const SizedBox.shrink();
}
return Text(
'${val.toInt()}',
style: TextStyle(
fontSize: 10,
color: context.colors.textTertiary),
);
},
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 22,
reservedSize: 20,
interval: 6 * 3600,
getTitlesWidget: (val, meta) {
final label = widget.timeLabel(val);
... ... @@ -914,6 +942,8 @@ class _StressBarChartState extends State<_StressBarChart> {
),
barTouchData: BarTouchData(
handleBuiltInTouches: false,
touchExtraThreshold: const EdgeInsets.symmetric(
horizontal: 16, vertical: 40),
touchCallback:
(FlTouchEvent event, BarTouchResponse? touchResponse) {
if (event is FlPanStartEvent ||
... ... @@ -928,6 +958,10 @@ class _StressBarChartState extends State<_StressBarChart> {
_stressTouchedGroupIndex =
touchResponse.spot!.touchedBarGroupIndex;
});
} else {
setState(() {
_stressTouchedGroupIndex = null;
});
}
} else if (event is FlPanEndEvent ||
event is FlTapUpEvent ||
... ... @@ -978,6 +1012,34 @@ class _StressBarChartState extends State<_StressBarChart> {
),
),
...sleepWidgets,
// 手绘 100、50、0 标签在对应的横线上方
Positioned(
right: 0,
top: gridTop - 12,
child: Text(
'100',
style:
TextStyle(fontSize: 10, color: context.colors.textTertiary),
),
),
Positioned(
right: 0,
top: gridTop + gridHeight * 0.5 - 12,
child: Text(
'50',
style:
TextStyle(fontSize: 10, color: context.colors.textTertiary),
),
),
Positioned(
right: 0,
top: gridBottom - 12,
child: Text(
'0',
style:
TextStyle(fontSize: 10, color: context.colors.textTertiary),
),
),
],
);
},
... ... @@ -1017,7 +1079,7 @@ class _StressBarChartState extends State<_StressBarChart> {
color: entry != null
? widget.getColorByState(entry.state)
: Colors.transparent,
borderRadius: BorderRadius.zero,
borderRadius: BorderRadius.circular(barWidth / 2),
),
],
));
... ...
... ... @@ -24,7 +24,7 @@ class TodayHrvNumberCard extends StatelessWidget {
controller.selectedDate.value, controller.lastSelectableDay);
var otherHrv = (sameDay
? context.l10n.todaySHrvTrend
? context.l10n.todaySAverageHrv
: context.l10n.averageHrvForTheDay);
return Row(
children: [
... ...
... ... @@ -53,7 +53,6 @@ class TodaySleepCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
children: [
if (sleepHours > 0) ...[
... ... @@ -87,7 +86,6 @@ class TodaySleepCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
)),
_MetricBlock(
... ... @@ -99,7 +97,6 @@ class TodaySleepCard extends StatelessWidget {
color: sleepStateColor,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
),
),
... ... @@ -113,7 +110,6 @@ class TodaySleepCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
children: [
TextSpan(text: sleepAverageHeartRate),
... ... @@ -134,7 +130,6 @@ class TodaySleepCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
),
),
... ... @@ -196,7 +191,6 @@ class TodayActivityCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
children: [
if (exHours > 0) ...[
... ... @@ -231,7 +225,6 @@ class TodayActivityCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
children: [
const TextSpan(text: '-'),
... ... @@ -249,7 +242,6 @@ class TodayActivityCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
children: [
TextSpan(text: '$stand'),
... ... @@ -271,7 +263,6 @@ class TodayActivityCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
children: [
const TextSpan(text: '-'),
... ... @@ -306,7 +297,6 @@ class TodayActivityCard extends StatelessWidget {
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 19 / 16,
),
children: [
TextSpan(text: activityStr),
... ... @@ -427,7 +417,6 @@ class _TodaySummaryCard extends StatelessWidget {
color: colors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14,
),
),
const Spacer(),
... ... @@ -437,7 +426,6 @@ class _TodaySummaryCard extends StatelessWidget {
color: colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 17 / 12,
),
),
Image.asset(
... ... @@ -495,7 +483,6 @@ class _MetricBlock extends StatelessWidget {
color: labelColor,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 17 / 12,
),
),
const SizedBox(height: 5),
... ...
... ... @@ -351,11 +351,12 @@ class HealthApi {
);
}
Future<AppResult<bool>> getHealthDataEverUploaded() {
Future<AppResult<V2HealthEverUploaded>> getHealthDataEverUploaded() {
return safeCall(
call: () async {
final response = await _dioClient.dio.get(ApiPaths.weatherUploadedData);
return response.data['flag'];
return V2HealthEverUploaded.fromJson(
response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
... ...
... ... @@ -55,9 +55,9 @@ class V2LatestHrvData {
case 2:
return context.colors.chartOrange;
case 3:
return context.colors.chartGreen;
case 4:
return context.colors.chartBlue;
case 4:
return context.colors.chartGreen;
}
return context.colors.chartGreen;
}
... ... @@ -437,3 +437,27 @@ class V2ActivityTarget {
return val;
}
}
class V2HealthEverUploaded {
const V2HealthEverUploaded({
this.flag,
this.startTime,
});
final bool? flag;
final int? startTime;
factory V2HealthEverUploaded.fromJson(Map<String, dynamic> json) {
return V2HealthEverUploaded(
flag: json['flag'] as bool?,
startTime: _parseInt(json['start_time']),
);
}
Map<String, dynamic> toJson() {
return {
'flag': flag,
'start_time': startTime,
};
}
}
... ...
... ... @@ -562,5 +562,6 @@
"freeRedemptionOffer": "Free Redemption Offer",
"cellPhoneNumber": "Cell phone number",
"todayOnWeeklyCalendar": "today",
"hrvTrendForThatDay": "HRV Trend for That Day"
"hrvTrendForThatDay": "HRV Trend for That Day",
"todaySAverageHrv": "Today's Average HRV"
}
\ No newline at end of file
... ...
... ... @@ -850,5 +850,6 @@
"freeRedemptionOffer": "免费兑换优惠",
"cellPhoneNumber": "手机号",
"todayOnWeeklyCalendar": "今",
"hrvTrendForThatDay": "该日HRV趋势"
"hrvTrendForThatDay": "该日HRV趋势",
"todaySAverageHrv": "今日平均HRV"
}
\ No newline at end of file
... ...
... ... @@ -3332,6 +3332,12 @@ abstract class AppLocalizations {
/// In zh, this message translates to:
/// **'该日HRV趋势'**
String get hrvTrendForThatDay;
/// No description provided for @todaySAverageHrv.
///
/// In zh, this message translates to:
/// **'今日平均HRV'**
String get todaySAverageHrv;
}
class _AppLocalizationsDelegate
... ...
... ... @@ -1856,4 +1856,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get hrvTrendForThatDay => 'HRV Trend for That Day';
@override
String get todaySAverageHrv => 'Today\'s Average HRV';
}
... ...
... ... @@ -1757,4 +1757,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get hrvTrendForThatDay => '该日HRV趋势';
@override
String get todaySAverageHrv => '今日平均HRV';
}
... ...