Commit 7ab533274ef2ae35f10d03d2e2bf15fb3d1011dc

Authored by 常守达
1 parent 31cbe47b

feat(today): 活动目标,分享

... ... @@ -17,6 +17,8 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
... ... @@ -36,6 +38,7 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
... ... @@ -58,6 +61,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
... ... @@ -73,6 +78,7 @@ SPEC CHECKSUMS:
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
... ...
... ... @@ -283,7 +283,6 @@
66FBE5882FDA4F0F00F515B4 /* Frameworks */,
66FBE5892FDA4F0F00F515B4 /* Resources */,
66FBE5BE2FDA518D00F515B4 /* Embed Foundation Extensions */,
1F69C09D185F42507815E3C2 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
... ... @@ -365,23 +364,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1F69C09D185F42507815E3C2 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
4348DC7B38CB739C4ABA7DAC /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
... ...
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:get/get.dart';
... ... @@ -7,7 +8,7 @@ class BindPartnerBinding extends Bindings {
@override
void dependencies() {
Get.put(BindPartnerController(
Get.find<UserApi>(),
Get.find<FriendApi>(),
));
}
}
... ...
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/user/user_models.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:share_plus/share_plus.dart';
class BindPartnerController extends GetxController {
BindPartnerController(this._userApi);
BindPartnerController(this._friendApi);
final UserApi _userApi;
final FriendApi _friendApi;
final TextEditingController partnerIdController = TextEditingController();
... ... @@ -40,27 +40,22 @@ class BindPartnerController extends GetxController {
bool get canSubmit => partnerIdInput.value.isNotEmpty;
void onShareMyCode() {}
void onShareMyCode() {
Share.share("""嘿 ❤️ 我发现了一个叫 DoubleFeel 的 App!
它可以帮我们互相关心 — 记录压力和睡眠、查看 HRV 与身体状态、及时了解彼此的健康状况。
快来一起使用吧 👉 https://Xxxxxxx
我的好友id:${myInviteCode.value}""");
}
Future<void> onSubmitPartnerId() async {
if (!canSubmit || isSubmitting.value) return;
isSubmitting.value = true;
try {
final result = await _userApi.bindPartner(partnerIdInput.value);
final result = await _friendApi.addFriend(partnerIdInput.value);
if (result is AppFailure) {
return;
} else {
final partnerResult = await _userApi.getPartnerUserInfo();
if (partnerResult is AppSuccess<BoundUserInfoResponse>) {
final partner = partnerResult.data.partnerUserInfo;
if (partner != null) {
await Get.find<UserPreferencesStorage>()
.updatePartnerUserInfo(partner);
}
}
}
final userStateService = Get.find<UserStateService>();
final partnerId = partnerIdInput.value;
if (!userStateService.isVip) {
Get.offAllNamed(AppRoutes.userOnboarding, arguments: {
'type': "MembershipOfferPage",
... ...
import 'package:doublefeel_flutter/app/widget/dash_divider.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
... ... @@ -182,6 +183,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
Clipboard.setData(
ClipboardData(text: controller.myInviteCode.value),
);
AppToast.show('复制成功');
},
child: Container(
width: 20,
... ...
... ... @@ -117,6 +117,7 @@ class TodayController extends GetxController {
final v2LatestHrv = Rxn<V2LatestHrvData>();
final v2HrvTrend = Rxn<V2HrvTrendData>();
final v2RealtimeStress = Rxn<V2RealtimeStressData>();
final v2ActivityTarget = Rxn<V2ActivityTarget>();
Future<void> requestHealthAuthorization() async {
if (GetPlatform.isIOS) {
... ... @@ -254,6 +255,14 @@ class TodayController extends GetxController {
case AppFailure():
v2RealtimeStress.value = null;
}
if (v2ActivityTarget.value == null) {
switch (await _healthApi.getV2ActivityTarget(friendUserId)) {
case AppSuccess(:final data):
v2ActivityTarget.value = data;
case AppFailure():
v2ActivityTarget.value = null;
}
}
}
void _clearRealTimeData() {
... ... @@ -269,6 +278,7 @@ class TodayController extends GetxController {
stressChartData.clear();
hrvAnnotations.clear();
avgHrv.value = '--';
v2ActivityTarget.value = null;
}
void _applyHrvStatistics(HrvStatisticsData data, DateTime date) {
... ...
... ... @@ -78,12 +78,7 @@ class MyTab extends GetView<MyController> {
_SettingsRow(
title: 'Route List',
onTap: () {
Get.toNamed(Routes.FRIEND_HOME,
arguments: FriendItem(
friendUserId: 61,
remarkName: '徐璐',
avatar:
'https://cdn3.didiapp.com/manager_upload/ab2abeecb14442b198bdaedc7c41f094.png'));
Get.toNamed(Routes.ROUTE_LIST);
},
),
],
... ...
... ... @@ -208,12 +208,11 @@ class TodayTabBody extends StatelessWidget {
TodayHrvChartCard(controller: controller),
const SizedBox(height: 12),
_LatestHrvCard(controller: controller),
const SizedBox(height: 12),
measureHRVButton(context),
if (!controller.isFriend) measureHRVButton(context),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
context.l10n.dailyActions,
controller.isFriend ? 'TA的今日行动' : context.l10n.dailyActions,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
... ... @@ -246,6 +245,7 @@ class TodayTabBody extends StatelessWidget {
return GestureDetector(
onTap: showHrvMeasurementBottomSheet,
child: Container(
margin: EdgeInsets.only(top: 12),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
color: Colors.white,
... ...
... ... @@ -112,7 +112,7 @@ class TodayHrvChartCard extends StatelessWidget {
return Row(
children: [
Text(
context.l10n.todaySHrvTrend,
controller.isFriend ? 'Ta的今日HRV趋势' : context.l10n.todaySHrvTrend,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
... ...
... ... @@ -24,7 +24,9 @@ class TodayHrvNumberCard extends StatelessWidget {
// HRV
Expanded(
child: _NumberItem(
label: context.l10n.averageHrvForTheDay,
label: controller.isFriend
? 'Ta今日的平均HRV'
: context.l10n.averageHrvForTheDay,
value:
controller.v2HealthData.value?.hrvAvg?.toString() ?? '--',
unit: 'ms',
... ... @@ -33,7 +35,9 @@ class TodayHrvNumberCard extends StatelessWidget {
Expanded(
child: _NumberItem(
label: context.l10n.restingHeartRate,
label: controller.isFriend
? 'Ta的静息心率'
: context.l10n.restingHeartRate,
value: controller.v2HealthData.value?.lastRestingHrValue
?.toString() ??
'--',
... ...
... ... @@ -21,9 +21,11 @@ class TodaySleepCard extends StatelessWidget {
int sleepDuration = healthData?.sleepDuration ?? 0;
int sleepHours = sleepDuration ~/ 3600;
int sleepMinutes = sleepDuration % 60;
Color sleepStateColor =
healthData?.sleepStateColor(context) ?? context.colors.chartGreen;
String sleepStateStr = healthData?.sleepStateStr() ?? '--';
Color sleepStateColor = sleepDuration == 0
? context.colors.textPrimary
: healthData?.sleepStateColor(context);
String sleepStateStr =
sleepDuration == 0 ? '--' : healthData?.sleepStateStr() ?? '--';
String sleepAverageHeartRate =
(healthData?.hrAvg == null || healthData?.hrAvg == 0)
? '--'
... ... @@ -104,31 +106,49 @@ class TodayActivityCard extends StatelessWidget {
return GestureDetector(
onTap: controller.toTrendActivityPage,
child: Obx(() {
var activityTarget = controller.v2ActivityTarget.value;
var healthData = controller.v2HealthData.value;
var activity = healthData?.move;
var activity = healthData?.move ?? 0;
var stand = healthData?.stand ?? 0;
var exercise = healthData?.exercise ?? 0;
var standStr = stand > 0 ? '${stand ~/ 3600}' : '--';
var exerciseStr = exercise > 0 ? '${exercise % 60}' : '--';
var activityStr = (activity > 0 && (activityTarget?.move ?? 0) > 0)
? '$activity'
: '--';
var exerciseStr = (exercise > 0 && (activityTarget?.exercise ?? 0) > 0)
? '${exercise % 60}'
: '--';
var standStr = (stand > 0 && (activityTarget?.stand ?? 0) > 0)
? '${stand ~/ 3600}'
: '--';
double activityProgress =
(activity > 0 && (activityTarget?.move ?? 0) > 0)
? (activity / (activityTarget?.move ?? 0)) * 360
: -1;
double exerciseProgress =
(exercise > 0 && (activityTarget?.exercise ?? 0) > 0)
? (exercise / (activityTarget?.exercise ?? 0)) * 360
: -1;
double standProgress = (stand > 0 && (activityTarget?.stand ?? 0) > 0)
? (stand / (activityTarget?.stand ?? 0)) * 360
: -1;
return _TodaySummaryCard(
iconAsset: 'assets/images/common/ic_exercise.png',
iconColor: const Color(0xFFFF5279),
iconColor: context.colors.warning,
title: context.l10n.fitness,
actionText: context.l10n.viewFitnessReport,
metrics: [
_MetricData(
label: context.l10n.event,
labelColor: const Color(0xFFFF5279),
labelColor: context.colors.warning,
valueSpans: [
_MetricValueSpan(
(activity == null || activity == 0) ? '--' : '$activity',
'千卡'),
_MetricValueSpan(activityStr, '千卡'),
],
),
_MetricData(
label: context.l10n.exercise,
labelColor: const Color(0xFF3BD49D),
labelColor: context.colors.chartGreen,
valueSpans: [
_MetricValueSpan(
exerciseStr,
... ... @@ -161,10 +181,10 @@ class TodayActivityCard extends StatelessWidget {
duration: const Duration(seconds: 1),
centerCircleSizeRatio: 0.2,
gapRatio: 0.06,
sweepAngles: const [
180,
230,
180,
sweepAngles: [
activityProgress,
exerciseProgress,
standProgress
],
backgroundColors: [
context.colors.chartPink.withOpacity(0.4),
... ...
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:get/get.dart';
import '../constants/network_const.dart';
... ... @@ -48,6 +49,7 @@ class AppErrorHandler {
if (Get.isRegistered<UserStateService>()) {
Get.find<UserStateService>().onLogout();
}
Get.offAllNamed(AppRoutes.login);
}
void _showMessage(String? message) {
... ...
... ... @@ -337,4 +337,20 @@ class HealthApi {
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
Future<AppResult<V2ActivityTarget>> getV2ActivityTarget(
int? friendUserId,
) {
return safeCall(
call: () async {
final response = await _dioClient.dio
.get(ApiPaths.v2ActivityTarget, queryParameters: {
if (friendUserId != null) 'query_user_id': friendUserId,
});
return V2ActivityTarget.fromJson(
response.data as Map<String, dynamic>);
},
errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
);
}
}
... ...
... ... @@ -36,6 +36,8 @@ abstract final class ApiPaths {
static const v2HrvTrend = '/client/doublefeel/health/v2/hrv_trend/';
static const v2RealtimeStress =
'/client/doublefeel/health/v2/realtime_stress/';
static const v2ActivityTarget =
'/client/doublefeel/health/v2/activity_target/';
// Pay
static const paymentProducts = '/client/doublefeel/payment/products/';
... ...
... ... @@ -151,7 +151,7 @@ class V2HealthData {
case 3:
return context.colors.chartPink;
}
return context.colors.chartGreen;
return null;
}
sleepStateStr() {
... ... @@ -318,3 +318,63 @@ class V2RealtimeStressData {
return val;
}
}
/// v2/activity_target/ response
/// {"id":0,"create_time":0,"update_time":0,"user_id":131,
/// "move":300,"step":8000,"stand":12,"exercise":3600}
class V2ActivityTarget {
const V2ActivityTarget({
this.id,
this.userId,
this.move,
this.step,
this.stand,
this.exercise,
this.createTime,
this.updateTime,
});
final int? id;
final int? userId;
/// 活动消耗目标(千卡)
final int? move;
/// 步数目标
final int? step;
/// 站立小时目标
final int? stand;
/// 锻炼时长目标(秒)
final int? exercise;
final int? createTime;
final int? updateTime;
factory V2ActivityTarget.fromJson(Map<String, dynamic> json) {
return V2ActivityTarget(
id: _parseInt(json['id']),
userId: _parseInt(json['user_id']),
move: _parseInt(json['move']),
step: _parseInt(json['step']),
stand: _parseInt(json['stand']),
exercise: _parseInt(json['exercise']),
createTime: _parseInt(json['create_time']),
updateTime: _parseInt(json['update_time']),
);
}
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
if (id != null) val['id'] = id;
if (userId != null) val['user_id'] = userId;
if (move != null) val['move'] = move;
if (step != null) val['step'] = step;
if (stand != null) val['stand'] = stand;
if (exercise != null) val['exercise'] = exercise;
if (createTime != null) val['create_time'] = createTime;
if (updateTime != null) val['update_time'] = updateTime;
return val;
}
}
... ...
... ... @@ -650,7 +650,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
dependency: "direct overridden"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
... ... @@ -819,6 +819,24 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
share_plus:
dependency: "direct main"
description:
path: "packages/share_plus/share_plus"
ref: "br_share_plus-v10.1.1_ohos"
resolved-ref: "55de300a8627c55cd45ac86e6a26bcae8e0ca4cf"
url: "https://gitcode.com/CPF-Flutter/flutter_plus_plugins.git"
source: git
version: "10.1.1"
share_plus_platform_interface:
dependency: transitive
description:
path: "packages/share_plus/share_plus_platform_interface"
ref: "55de300a8627c55cd45ac86e6a26bcae8e0ca4cf"
resolved-ref: "55de300a8627c55cd45ac86e6a26bcae8e0ca4cf"
url: "https://gitcode.com/CPF-Flutter/flutter_plus_plugins.git"
source: git
version: "5.0.1"
shared_preferences:
dependency: "direct main"
description:
... ... @@ -1042,6 +1060,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
description:
... ... @@ -1143,6 +1193,14 @@ packages:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
source: git
version: "3.22.0"
win32:
dependency: transitive
description:
name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.dev"
source: hosted
version: "5.13.0"
xdg_directories:
dependency: transitive
description:
... ... @@ -1160,5 +1218,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.27.0"
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"
... ...
... ... @@ -42,12 +42,23 @@ dependencies:
url: https://gitcode.com/openharmony-sig/flutter_permission_handler.git
path: permission_handler_ohos
ref: br_permission_handler_v11.3.1_ohos
share_plus:
git:
url: https://gitcode.com/CPF-Flutter/flutter_plus_plugins.git
path: packages/share_plus/share_plus
ref: br_share_plus-v10.1.1_ohos
table_calendar: ^3.1.3
fluttertoast: ^8.2.2
flutter_localizations:
sdk: flutter
intl: any
# share_plus (git) 间接依赖 path_provider 的 git 版本,
# 与 cached_network_image → flutter_cache_manager 依赖的 hosted 版本冲突。
# 强制使用 hosted 版本统一来源。
dependency_overrides:
path_provider: ^2.1.5
dev_dependencies:
flutter_test:
sdk: flutter
... ...