更新
This commit is contained in:
1
assets/img/icon/ai.svg
Normal file
1
assets/img/icon/ai.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747822249470" class="icon" viewBox="0 0 1219 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3508" xmlns:xlink="http://www.w3.org/1999/xlink" width="238.0859375" height="200"><path d="M326.582238 206.473198a59.46507 59.46507 0 0 1 47.63963 24.32662 43.585194 43.585194 0 0 1 17.231356 18.582835v3.378697L642.828294 963.301367a46.288151 46.288151 0 0 1-33.786972 57.099983 47.9775 47.9775 0 0 1-59.80294-22.299402v-3.378697L484.367396 811.259994H161.026076l-64.533116 183.463257a47.301761 47.301761 0 0 1-61.830159 25.678099 46.288151 46.288151 0 0 1-33.786971-53.721286v-3.716566l249.347852-709.526409a43.923063 43.923063 0 0 1 30.408274-26.691707 58.451461 58.451461 0 0 1 45.950282-20.272184z m459.164947 202.721831c26.015968 0 47.301761 13.176919 49.328979 29.394666v549.714031c0 17.907095-21.961532 33.786972-49.328979 33.786972s-47.301761-12.839049-49.328979-29.394665V439.603304c0-17.569225 21.961532-32.097623 49.328979-32.097623z m-462.881514-58.451461L195.150918 713.953515h255.091637L322.865671 350.743568zM1038.135864 105.788022a15.204137 15.204137 0 0 1 10.136092 9.460352l38.517148 117.916532 120.957359 42.571585a14.866268 14.866268 0 0 1 8.784613 19.258574 14.528398 14.528398 0 0 1-9.798222 9.460352l-118.254401 33.786972-36.8278 115.889313a14.866268 14.866268 0 0 1-28.381056 0l-38.855018-118.254401-117.240792-37.16567a14.866268 14.866268 0 0 1-9.460352-18.920704 14.866268 14.866268 0 0 1 9.122482-9.122482l115.889314-38.855018 36.827799-115.889313a14.866268 14.866268 0 0 1 18.582834-9.798222zM772.570266 0.37267a9.798222 9.798222 0 0 1 6.419525 6.757395l21.961531 70.276901 72.64199 24.32662a10.136092 10.136092 0 0 1 6.419524 13.176919 10.473961 10.473961 0 0 1-6.757394 6.757394l-72.30412 21.285792L777.976181 214.919941a10.136092 10.136092 0 0 1-13.176919 6.419525 9.798222 9.798222 0 0 1-6.419524-6.757394l-21.961532-70.276902-70.276901-21.285792a10.473961 10.473961 0 0 1-7.095264-12.839049 10.136092 10.136092 0 0 1 6.757394-7.095264l70.614771-24.66449 23.313011-70.952641a10.136092 10.136092 0 0 1 12.501179-7.095264z" fill="#00C1AA" p-id="3509"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
assets/img/icon/score_down.svg
Normal file
1
assets/img/icon/score_down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747827928589" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3806" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M532.733927 1013.26727l292.710814-421.991424a25.856122 25.856122 0 0 0-21.465459-40.491663H660.550982a25.856122 25.856122 0 0 1-27.319676-25.856122V25.856122A25.856122 25.856122 0 0 0 608.838738 0h-195.140543a25.856122 25.856122 0 0 0-24.392568 25.856122v499.071939a25.856122 25.856122 0 0 1-25.856122 25.856122H220.021206a25.856122 25.856122 0 0 0-21.465459 40.491663l292.710814 421.991424a26.343973 26.343973 0 0 0 41.467366 0z" fill="#F84B20" p-id="3807"></path></svg>
|
||||
|
After Width: | Height: | Size: 804 B |
1
assets/img/icon/score_equal.svg
Normal file
1
assets/img/icon/score_equal.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747827932650" class="icon" viewBox="0 0 3235 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3955" xmlns:xlink="http://www.w3.org/1999/xlink" width="631.8359375" height="200"><path d="M2468.93535421 234.66666663m0 40.508049l0 450.528542q0 40.508049-40.508049 40.508049l-1598.585923 0q-40.508049 0-40.508049-40.508049l0-450.528542q0-40.508049 40.508049-40.508049l1598.585923 0q40.508049 0 40.508049 40.508049Z" fill="#00C1AA" p-id="3956"></path></svg>
|
||||
|
After Width: | Height: | Size: 607 B |
1
assets/img/icon/score_up.svg
Normal file
1
assets/img/icon/score_up.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747827923553" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3657" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M491.356539 11.433663l-292.50833 421.211996a25.838236 25.838236 0 0 0 21.450611 40.463652h143.329082a25.838236 25.838236 0 0 1 25.838235 25.838236v498.726703a25.838236 25.838236 0 0 0 25.838236 26.32575h195.005554a25.838236 25.838236 0 0 0 24.375694-26.32575V498.947547a25.838236 25.838236 0 0 1 25.838236-25.838236h141.86654a25.838236 25.838236 0 0 0 21.450611-40.463652L534.257761 11.433663a25.838236 25.838236 0 0 0-42.901222 0z" fill="#00C1AA" p-id="3658"></path></svg>
|
||||
|
After Width: | Height: | Size: 806 B |
1078
assets/img/服务协议.pdf
Normal file
1078
assets/img/服务协议.pdf
Normal file
File diff suppressed because one or more lines are too long
1078
assets/img/隐私协议.pdf
Normal file
1078
assets/img/隐私协议.pdf
Normal file
File diff suppressed because one or more lines are too long
@@ -360,6 +360,8 @@
|
||||
"身高":"身高",
|
||||
"身高输入提示":"请输入身高",
|
||||
"用户协议":"用户协议",
|
||||
"隐私协议":"隐私协议"
|
||||
"隐私协议":"隐私协议",
|
||||
"呼吸基准":"呼吸基准",
|
||||
"呼吸基准介绍":"呼吸基准介绍。"
|
||||
|
||||
}
|
||||
@@ -41,6 +41,9 @@ class DynamicReportDetailWidget extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
SizedBox(
|
||||
height: 33.rpx,
|
||||
),
|
||||
_buildSleepDateWidgets(),
|
||||
SizedBox(height: 20.rpx),
|
||||
_buildSleepDataModuleWidgets(),
|
||||
|
||||
40
lib/controller/setting/pdf/PrivacyPdfController.dart
Normal file
40
lib/controller/setting/pdf/PrivacyPdfController.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class PrivacyPdfController extends GetxController {
|
||||
var localPdfPath = Rx<String?>(null);
|
||||
|
||||
// 加载 PDF 文件
|
||||
Future<void> loadPdf(int type, [String? url]) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final filename = type == 1 ? 'service.pdf' : 'privacy.pdf';
|
||||
final filePath = p.join(tempDir.path, filename);
|
||||
final file = File(filePath);
|
||||
|
||||
try {
|
||||
if (url == null || url.isEmpty) {
|
||||
final byteData = await rootBundle
|
||||
.load(type == 1 ? 'assets/img/服务协议.pdf' : 'assets/img/隐私协议.pdf');
|
||||
await file.writeAsBytes(byteData.buffer.asUint8List());
|
||||
} else {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
} else {
|
||||
throw Exception('【PDF加载】类型$type:无法下载 PDF,状态码 ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
final byteData = await rootBundle
|
||||
.load(type == 1 ? 'assets/img/服务协议.pdf' : 'assets/img/隐私协议.pdf');
|
||||
await file.writeAsBytes(byteData.buffer.asUint8List());
|
||||
}
|
||||
|
||||
localPdfPath.value = filePath;
|
||||
}
|
||||
}
|
||||
40
lib/controller/setting/pdf/UserPdfController.dart
Normal file
40
lib/controller/setting/pdf/UserPdfController.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class UserPdfController extends GetxController {
|
||||
var localPdfPath = Rx<String?>(null);
|
||||
|
||||
// 加载 PDF 文件
|
||||
Future<void> loadPdf(int type, [String? url]) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final filename = type == 1 ? 'service.pdf' : 'privacy.pdf';
|
||||
final filePath = p.join(tempDir.path, filename);
|
||||
final file = File(filePath);
|
||||
|
||||
try {
|
||||
if (url == null || url.isEmpty) {
|
||||
final byteData = await rootBundle
|
||||
.load(type == 1 ? 'assets/img/服务协议.pdf' : 'assets/img/隐私协议.pdf');
|
||||
await file.writeAsBytes(byteData.buffer.asUint8List());
|
||||
} else {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
} else {
|
||||
throw Exception('【PDF加载】类型$type:无法下载 PDF,状态码 ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
final byteData = await rootBundle
|
||||
.load(type == 1 ? 'assets/img/服务协议.pdf' : 'assets/img/隐私协议.pdf');
|
||||
await file.writeAsBytes(byteData.buffer.asUint8List());
|
||||
}
|
||||
|
||||
localPdfPath.value = filePath;
|
||||
}
|
||||
}
|
||||
367
lib/controller/weather/weather_controller.dart
Normal file
367
lib/controller/weather/weather_controller.dart
Normal file
@@ -0,0 +1,367 @@
|
||||
// import 'dart:async';
|
||||
// import 'package:EasyDartModule/EasyDartModule.dart';
|
||||
// import 'package:ef/ef.dart';
|
||||
// import 'package:geocoding/geocoding.dart';
|
||||
// import 'package:geolocator/geolocator.dart';
|
||||
// import 'package:json_annotation/json_annotation.dart';
|
||||
// import 'package:vbvs_app/common/util/CommonVariables.dart';
|
||||
// import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
// import 'package:vbvs_app/controller/setting/language/language_controller.dart';
|
||||
// import 'package:weather/weather.dart';
|
||||
|
||||
// part 'weather_controller.g.dart';
|
||||
|
||||
// @JsonSerializable()
|
||||
// class WeatherModel {
|
||||
// double? longitude; //经度
|
||||
// double? latitude; //纬度
|
||||
// String? weather_info = ''; //天气
|
||||
// int? current_temperature; //温度
|
||||
// int? min_temperature; //温度
|
||||
// int? max_temperature; //温度
|
||||
// String? wind_direction; //风向
|
||||
// int? wind_speed; //风速等级
|
||||
// String? cityName; // 新增城市名字段
|
||||
// String? weatherIcon; // 新增天气图标字段
|
||||
// String? weatherIconurl; // 新增天气图标字段
|
||||
|
||||
// WeatherModel();
|
||||
// static WeatherModel fromJson(Map<String, dynamic> json) =>
|
||||
// _$WeatherModelFromJson(json);
|
||||
// Map<String, dynamic> toJson() => _$WeatherModelToJson(this);
|
||||
// }
|
||||
|
||||
// class WeatherModelController extends GetControllerEx<WeatherModel> {
|
||||
// LanguageController languageController = Get.find();
|
||||
// WeatherModelController() {
|
||||
// attr = GetModel(WeatherModel()).obs;
|
||||
// weatherFactory = WeatherFactory(CommonVariables.weather_apiKey,
|
||||
// language: Language.CHINESE_SIMPLIFIED);
|
||||
// }
|
||||
|
||||
// Timer? _timer;
|
||||
|
||||
// late WeatherFactory weatherFactory;
|
||||
|
||||
// @override
|
||||
// Future<void> onInit() async {
|
||||
// super.onInit();
|
||||
// await _getCurrentLocationAndWeather();
|
||||
// // 启动定时器,每5秒执行一次 getCurrentWeather 方法
|
||||
// _timer = Timer.periodic(Duration(seconds: 5), (timer) {
|
||||
// _getCurrentLocationAndWeather();
|
||||
// });
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void onClose() {
|
||||
// // 取消定时器
|
||||
// _timer?.cancel();
|
||||
// super.onClose();
|
||||
// }
|
||||
|
||||
// Future<void> _getCurrentLocationAndWeather() async {
|
||||
// try {
|
||||
// Position position = await _determinePosition();
|
||||
|
||||
// String? language = "zh_CN";
|
||||
// if (languageController.selectLanguage != null) {
|
||||
// language = languageController.selectLanguage.value!.language_code;
|
||||
// }
|
||||
// List<Placemark> placemarks = await placemarkFromCoordinates(
|
||||
// position.latitude, position.longitude,
|
||||
// localeIdentifier: "${language}");
|
||||
// if (placemarks.isNotEmpty) {
|
||||
// model.cityName = placemarks[0].locality ?? "未知数据".tr;
|
||||
// }
|
||||
// getCurrentWeather(position.latitude, position.longitude);
|
||||
// } catch (e) {
|
||||
// print(e);
|
||||
// EasyDartModule.logger.error("获取位置和天气失败: $e");
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<Position> _determinePosition() async {
|
||||
// bool serviceEnabled;
|
||||
// LocationPermission permission;
|
||||
|
||||
// // 检查位置服务是否启用
|
||||
// serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
// if (!serviceEnabled) {
|
||||
// // 位置服务未启用,返回默认位置
|
||||
// return Future.error('Location services are disabled.');
|
||||
// }
|
||||
|
||||
// permission = await Geolocator.checkPermission();
|
||||
// if (permission == LocationPermission.denied) {
|
||||
// permission = await Geolocator.requestPermission();
|
||||
// if (permission == LocationPermission.denied) {
|
||||
// // 权限被拒绝,返回默认位置
|
||||
// return Future.error('Location permissions are denied');
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (permission == LocationPermission.deniedForever) {
|
||||
// // 权限被永久拒绝,返回默认位置
|
||||
// return Future.error(
|
||||
// 'Location permissions are permanently denied, we cannot request permissions.');
|
||||
// }
|
||||
|
||||
// // 获取当前位置
|
||||
// return await Geolocator.getCurrentPosition();
|
||||
// }
|
||||
|
||||
// Future<Weather> getCurrentWeather(
|
||||
// double latitude,
|
||||
// double longitude,
|
||||
// ) async {
|
||||
// try {
|
||||
// weatherFactory.language = Language.CHINESE_SIMPLIFIED;
|
||||
// String? language = "zh_CN";
|
||||
// if (languageController.selectLanguage != null) {
|
||||
// language = languageController.selectLanguage.value!.language_code;
|
||||
// }
|
||||
// if (language == "zh_CN") {
|
||||
// weatherFactory.language = Language.CHINESE_SIMPLIFIED;
|
||||
// } else {
|
||||
// weatherFactory.language = Language.ENGLISH;
|
||||
// }
|
||||
// Weather weather =
|
||||
// await weatherFactory.currentWeatherByLocation(latitude, longitude);
|
||||
// model.weather_info = weather.weatherDescription;
|
||||
// model.min_temperature = weather.tempMin?.celsius?.toInt();
|
||||
// model.max_temperature = weather.tempMax?.celsius?.toInt();
|
||||
// model.current_temperature = weather.temperature?.celsius?.toInt();
|
||||
// model.wind_speed = weather.windSpeed?.toInt();
|
||||
// model.weatherIcon = weather.weatherIcon;
|
||||
// if (model.weatherIcon != null) {
|
||||
// model.weatherIconurl =
|
||||
// "https://openweathermap.org/img/w/${model.weatherIcon}.png";
|
||||
// }
|
||||
|
||||
// // model.wind_direction = getDirectionByDegree(weather.windDegree);
|
||||
// updateAll();
|
||||
// return weather;
|
||||
// } catch (e) {
|
||||
// print('Error: $e');
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<List<Weather>> getWeatherForecast(
|
||||
// double latitude, double longitude) async {
|
||||
// try {
|
||||
// return await weatherFactory.fiveDayForecastByLocation(
|
||||
// latitude, longitude);
|
||||
// } catch (e) {
|
||||
// print('Error: $e');
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
|
||||
// String? getDirectionByDegree(double? windDegree) {
|
||||
// if (windDegree == null) return null;
|
||||
|
||||
// if (windDegree >= 337.5 || windDegree < 22.5) {
|
||||
// return '主页.天气.方向.北'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else if (windDegree >= 22.5 && windDegree < 67.5) {
|
||||
// return '主页.天气.方向.东北'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else if (windDegree >= 67.5 && windDegree < 112.5) {
|
||||
// return '主页.天气.方向.东'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else if (windDegree >= 112.5 && windDegree < 157.5) {
|
||||
// return '主页.天气.方向.东南'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else if (windDegree >= 157.5 && windDegree < 202.5) {
|
||||
// return '主页.天气.方向.南'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else if (windDegree >= 202.5 && windDegree < 247.5) {
|
||||
// return '主页.天气.方向.西南'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else if (windDegree >= 247.5 && windDegree < 292.5) {
|
||||
// return '主页.天气.方向.西'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else if (windDegree >= 292.5 && windDegree < 337.5) {
|
||||
// return '主页.天气.方向.西北'.tr + '主页.天气.方向.单位'.tr;
|
||||
// } else {
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:EasyDartModule/EasyDartModule.dart';
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:vbvs_app/common/util/CommonVariables.dart';
|
||||
import 'package:vbvs_app/controller/setting/language/language_controller.dart';
|
||||
import 'package:weather/weather.dart';
|
||||
|
||||
part 'weather_controller.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class WeatherModel {
|
||||
double? longitude; // 经度
|
||||
double? latitude; // 纬度
|
||||
String? weather_info = ''; // 天气
|
||||
int? current_temperature; // 温度
|
||||
int? min_temperature; // 最低温度
|
||||
int? max_temperature; // 最高温度
|
||||
String? wind_direction; // 风向
|
||||
int? wind_speed; // 风速等级
|
||||
String? cityName; // 城市名
|
||||
String? weatherIcon; // 天气图标
|
||||
String? weatherIconurl; // 天气图标url
|
||||
|
||||
WeatherModel();
|
||||
|
||||
static WeatherModel fromJson(Map<String, dynamic> json) =>
|
||||
_$WeatherModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$WeatherModelToJson(this);
|
||||
}
|
||||
|
||||
class WeatherModelController extends GetControllerEx<WeatherModel> {
|
||||
LanguageController languageController = Get.find();
|
||||
WeatherModelController() {
|
||||
attr = GetModel(WeatherModel()).obs;
|
||||
weatherFactory = WeatherFactory(CommonVariables.weather_apiKey,
|
||||
language: Language.CHINESE_SIMPLIFIED);
|
||||
}
|
||||
|
||||
Timer? _weatherTimer;
|
||||
Timer? _locationTimer;
|
||||
|
||||
late WeatherFactory weatherFactory;
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
await _getCurrentLocation();
|
||||
_weatherTimer = Timer.periodic(Duration(seconds: 5), (timer) {
|
||||
_getCurrentWeather(); // 每 5 秒更新一次天气
|
||||
});
|
||||
|
||||
_locationTimer = Timer.periodic(Duration(minutes: 10), (timer) {
|
||||
_getCurrentLocation(); // 每 10 分钟更新一次位置
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_weatherTimer?.cancel(); // 取消天气更新定时器
|
||||
_locationTimer?.cancel(); // 取消位置更新定时器
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// 获取当前位置并存储到 model
|
||||
Future<void> _getCurrentLocation() async {
|
||||
try {
|
||||
Position position = await _determinePosition();
|
||||
|
||||
String? language = "zh_CN";
|
||||
if (languageController.selectLanguage != null) {
|
||||
language = languageController.selectLanguage.value!.language_code;
|
||||
}
|
||||
List<Placemark> placemarks = await placemarkFromCoordinates(
|
||||
position.latitude, position.longitude,
|
||||
localeIdentifier: language);
|
||||
|
||||
if (placemarks.isNotEmpty) {
|
||||
model.cityName = placemarks[0].locality ?? "未知数据".tr;
|
||||
model.latitude = position.latitude;
|
||||
model.longitude = position.longitude;
|
||||
}
|
||||
|
||||
// 调用获取天气方法
|
||||
_getCurrentWeather();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
EasyDartModule.logger.error("获取位置失败: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前位置
|
||||
Future<Position> _determinePosition() async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
return Future.error('位置服务未启用');
|
||||
}
|
||||
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
return Future.error('位置权限被拒绝');
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
return Future.error('位置权限被永久拒绝');
|
||||
}
|
||||
|
||||
return await Geolocator.getCurrentPosition();
|
||||
}
|
||||
|
||||
// 获取天气信息
|
||||
Future<void> _getCurrentWeather() async {
|
||||
if (model.latitude == null || model.longitude == null) {
|
||||
return; // 如果位置数据没有获取到,则不更新天气
|
||||
}
|
||||
String? language = "zh_CN";
|
||||
if (languageController.selectLanguage != null) {
|
||||
language = languageController.selectLanguage.value!.language_code;
|
||||
}
|
||||
List<Placemark> placemarks = await placemarkFromCoordinates(
|
||||
model.latitude!, model.longitude!,
|
||||
localeIdentifier: language);
|
||||
|
||||
if (placemarks.isNotEmpty) {
|
||||
model.cityName = placemarks[0].locality ?? "未知数据".tr;
|
||||
}
|
||||
|
||||
try {
|
||||
weatherFactory.language = Language.CHINESE_SIMPLIFIED;
|
||||
String? language = "zh_CN";
|
||||
if (languageController.selectLanguage != null) {
|
||||
language = languageController.selectLanguage.value!.language_code;
|
||||
}
|
||||
if (language == "zh_CN") {
|
||||
weatherFactory.language = Language.CHINESE_SIMPLIFIED;
|
||||
} else {
|
||||
weatherFactory.language = Language.ENGLISH;
|
||||
}
|
||||
Weather weather = await weatherFactory.currentWeatherByLocation(
|
||||
model.latitude!, model.longitude!);
|
||||
|
||||
model.weather_info = weather.weatherDescription;
|
||||
model.min_temperature = weather.tempMin?.celsius?.toInt();
|
||||
model.max_temperature = weather.tempMax?.celsius?.toInt();
|
||||
model.current_temperature = weather.temperature?.celsius?.toInt();
|
||||
model.wind_speed = weather.windSpeed?.toInt();
|
||||
model.weatherIcon = weather.weatherIcon;
|
||||
|
||||
if (model.weatherIcon != null) {
|
||||
model.weatherIconurl =
|
||||
"https://openweathermap.org/img/w/${model.weatherIcon}.png";
|
||||
}
|
||||
|
||||
updateAll(); // 更新 UI
|
||||
} catch (e) {
|
||||
print('获取天气失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 5 天天气预报
|
||||
Future<List<Weather>> getWeatherForecast(
|
||||
double latitude, double longitude) async {
|
||||
try {
|
||||
return await weatherFactory.fiveDayForecastByLocation(
|
||||
latitude, longitude);
|
||||
} catch (e) {
|
||||
print('获取天气预报失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
lib/controller/weather/weather_controller.g.dart
Normal file
31
lib/controller/weather/weather_controller.g.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'weather_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
WeatherModel _$WeatherModelFromJson(Map<String, dynamic> json) => WeatherModel()
|
||||
..longitude = (json['longitude'] as num?)?.toDouble()
|
||||
..latitude = (json['latitude'] as num?)?.toDouble()
|
||||
..weather_info = json['weather_info'] as String?
|
||||
..current_temperature = (json['current_temperature'] as num?)?.toInt()
|
||||
..min_temperature = (json['min_temperature'] as num?)?.toInt()
|
||||
..max_temperature = (json['max_temperature'] as num?)?.toInt()
|
||||
..wind_direction = json['wind_direction'] as String?
|
||||
..wind_speed = (json['wind_speed'] as num?)?.toInt()
|
||||
..cityName = json['cityName'] as String?;
|
||||
|
||||
Map<String, dynamic> _$WeatherModelToJson(WeatherModel instance) =>
|
||||
<String, dynamic>{
|
||||
'longitude': instance.longitude,
|
||||
'latitude': instance.latitude,
|
||||
'weather_info': instance.weather_info,
|
||||
'current_temperature': instance.current_temperature,
|
||||
'min_temperature': instance.min_temperature,
|
||||
'max_temperature': instance.max_temperature,
|
||||
'wind_direction': instance.wind_direction,
|
||||
'wind_speed': instance.wind_speed,
|
||||
'cityName': instance.cityName,
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/component/tool/CustomCard.dart';
|
||||
import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart';
|
||||
|
||||
@@ -304,15 +304,63 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
return Text(
|
||||
"嘉兴 " +
|
||||
"${weatherModelController.model.weather_info ?? '未知数据'.tr}",
|
||||
style: TextStyle(
|
||||
color: themeController
|
||||
.currentColor.sc4,
|
||||
fontSize: AppConstants()
|
||||
.normal_text_fontSize,
|
||||
),
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
"${weatherModelController.model.cityName??'未知数据'.tr}",
|
||||
style: TextStyle(
|
||||
color: themeController
|
||||
.currentColor.sc4,
|
||||
fontSize: AppConstants()
|
||||
.normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${(weatherModelController.model.current_temperature != null && weatherModelController.model.current_temperature! > 0) ? weatherModelController.model.current_temperature : '未知数据'.tr}" +
|
||||
"°C",
|
||||
style: TextStyle(
|
||||
color: themeController
|
||||
.currentColor.sc4,
|
||||
fontSize: AppConstants()
|
||||
.normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${(weatherModelController.model.weather_info?.isNotEmpty ?? false) ? weatherModelController.model.weather_info : '未知数据'.tr}",
|
||||
style: TextStyle(
|
||||
color: themeController
|
||||
.currentColor.sc4,
|
||||
fontSize: AppConstants()
|
||||
.normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
if (weatherModelController
|
||||
.model
|
||||
.weatherIconurl !=
|
||||
null &&
|
||||
weatherModelController
|
||||
.model
|
||||
.weatherIconurl!
|
||||
.isNotEmpty)
|
||||
Container(
|
||||
width: 35.rpx,
|
||||
height: 26.rpx,
|
||||
clipBehavior:
|
||||
Clip.antiAlias,
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
shape: BoxShape
|
||||
.circle),
|
||||
child: Image.network(
|
||||
weatherModelController
|
||||
.model
|
||||
.weatherIconurl!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 20.rpx,
|
||||
)),
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -102,138 +102,298 @@ class _MessagePageState extends State<MessagePage> {
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 40.rpx),
|
||||
// Expanded(
|
||||
// child: Stack(
|
||||
// alignment: Alignment.bottomLeft,
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.start,
|
||||
// children: [
|
||||
// Obx(() {
|
||||
// return ClickableContainer(
|
||||
// padding: EdgeInsets.all(0),
|
||||
// backgroundColor: Colors.transparent,
|
||||
// highlightColor:
|
||||
// themeController.currentColor.sc21,
|
||||
// borderRadius: 8.rpx,
|
||||
// onTap: () => _onTabChanged(0),
|
||||
// child: Container(
|
||||
// width: 160.rpx,
|
||||
// alignment: Alignment.center,
|
||||
// child: Stack(
|
||||
// alignment: Alignment.center,
|
||||
// clipBehavior: Clip.none,
|
||||
// children: [
|
||||
// Text(
|
||||
// '体征消息'.tr,
|
||||
// style: FlutterFlowTheme.of(context)
|
||||
// .bodyMedium
|
||||
// .override(
|
||||
// fontFamily: 'Inter',
|
||||
// fontSize: AppConstants()
|
||||
// .title_text_fontSize,
|
||||
// color:
|
||||
// messageController
|
||||
// .model.type ==
|
||||
// 2
|
||||
// ? themeController
|
||||
// .currentColor.sc3
|
||||
// : themeController
|
||||
// .currentColor.sc2,
|
||||
// ),
|
||||
// ),
|
||||
// Obx(() {
|
||||
// return messageController.model
|
||||
// .body_message_read ==
|
||||
// 1
|
||||
// ? Positioned(
|
||||
// top: -4,
|
||||
// right: -14,
|
||||
// child: Container(
|
||||
// width: 8,
|
||||
// height: 8,
|
||||
// decoration:
|
||||
// const BoxDecoration(
|
||||
// color: Colors.red,
|
||||
// shape: BoxShape.circle,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// : const SizedBox.shrink();
|
||||
// }),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// Obx(() {
|
||||
// return ClickableContainer(
|
||||
// padding: EdgeInsets.all(0),
|
||||
// backgroundColor: Colors.transparent,
|
||||
// highlightColor:
|
||||
// themeController.currentColor.sc21,
|
||||
// borderRadius: 8.rpx,
|
||||
// onTap: () => _onTabChanged(1),
|
||||
// child: Container(
|
||||
// width: 160.rpx,
|
||||
// alignment: Alignment.center,
|
||||
// child: Stack(
|
||||
// alignment: Alignment.center,
|
||||
// clipBehavior: Clip.none,
|
||||
// children: [
|
||||
// Text(
|
||||
// '系统消息'.tr,
|
||||
// style: FlutterFlowTheme.of(context)
|
||||
// .bodyMedium
|
||||
// .override(
|
||||
// fontFamily: 'Inter',
|
||||
// fontSize: AppConstants()
|
||||
// .title_text_fontSize,
|
||||
// color:
|
||||
// messageController
|
||||
// .model.type ==
|
||||
// 1
|
||||
// ? themeController
|
||||
// .currentColor.sc3
|
||||
// : themeController
|
||||
// .currentColor.sc2,
|
||||
// ),
|
||||
// ),
|
||||
// Obx(() {
|
||||
// return messageController.model
|
||||
// .system_message_read ==
|
||||
// 1
|
||||
// ? Positioned(
|
||||
// top: -4,
|
||||
// right: -14,
|
||||
// child: Container(
|
||||
// width: 8,
|
||||
// height: 8,
|
||||
// decoration:
|
||||
// const BoxDecoration(
|
||||
// color: Colors.red,
|
||||
// shape: BoxShape.circle,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// : const SizedBox.shrink();
|
||||
// }),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ].divide(SizedBox(width: 10.rpx)),
|
||||
// ),
|
||||
// Obx(() {
|
||||
// double lineWidth = 170.rpx;
|
||||
// return AnimatedPositioned(
|
||||
// duration: const Duration(milliseconds: 300),
|
||||
// curve: Curves.easeInOut,
|
||||
// bottom: 0,
|
||||
// left: messageController.model.type == 1
|
||||
// ? 0
|
||||
// : 170.rpx,
|
||||
// child: Container(
|
||||
// width: lineWidth,
|
||||
// height: 4.rpx,
|
||||
// decoration: BoxDecoration(
|
||||
// color: themeController.currentColor.sc2,
|
||||
// borderRadius: BorderRadius.circular(2.rpx),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
// 第一个容器,宽度占屏幕一半
|
||||
Obx(() {
|
||||
return ClickableContainer(
|
||||
padding: EdgeInsets.all(0),
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor:
|
||||
themeController.currentColor.sc21,
|
||||
borderRadius: 8.rpx,
|
||||
onTap: () => _onTabChanged(0),
|
||||
child: Container(
|
||||
width: 160.rpx,
|
||||
alignment: Alignment.center,
|
||||
child: Stack(
|
||||
return Expanded(
|
||||
child: ClickableContainer(
|
||||
padding: EdgeInsets.all(0),
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor:
|
||||
themeController.currentColor.sc21,
|
||||
borderRadius: 8.rpx,
|
||||
onTap: () => _onTabChanged(0),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Text(
|
||||
'体征消息'.tr,
|
||||
style: FlutterFlowTheme.of(context)
|
||||
.bodyMedium
|
||||
.override(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: AppConstants()
|
||||
.title_text_fontSize,
|
||||
color:
|
||||
messageController
|
||||
.model.type ==
|
||||
2
|
||||
? themeController
|
||||
.currentColor.sc3
|
||||
: themeController
|
||||
.currentColor.sc2,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
return messageController.model
|
||||
.body_message_read ==
|
||||
1
|
||||
? Positioned(
|
||||
top: -4,
|
||||
right: -14,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration:
|
||||
const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Text(
|
||||
'体征消息'.tr,
|
||||
style:
|
||||
FlutterFlowTheme.of(context)
|
||||
.bodyMedium
|
||||
.override(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: AppConstants()
|
||||
.title_text_fontSize,
|
||||
color: messageController
|
||||
.model.type ==
|
||||
2
|
||||
? themeController
|
||||
.currentColor
|
||||
.sc3
|
||||
: themeController
|
||||
.currentColor
|
||||
.sc2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
Obx(() {
|
||||
return messageController.model
|
||||
.body_message_read ==
|
||||
1
|
||||
? Positioned(
|
||||
top: -4,
|
||||
right: -14,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration:
|
||||
const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape:
|
||||
BoxShape.circle,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(width: 10.rpx),
|
||||
// 第二个容器,宽度占屏幕一半
|
||||
Obx(() {
|
||||
return ClickableContainer(
|
||||
padding: EdgeInsets.all(0),
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor:
|
||||
themeController.currentColor.sc21,
|
||||
borderRadius: 8.rpx,
|
||||
onTap: () => _onTabChanged(1),
|
||||
child: Container(
|
||||
width: 160.rpx,
|
||||
alignment: Alignment.center,
|
||||
child: Stack(
|
||||
return Expanded(
|
||||
child: ClickableContainer(
|
||||
padding: EdgeInsets.all(0),
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor:
|
||||
themeController.currentColor.sc21,
|
||||
borderRadius: 8.rpx,
|
||||
onTap: () => _onTabChanged(1),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Text(
|
||||
'系统消息'.tr,
|
||||
style: FlutterFlowTheme.of(context)
|
||||
.bodyMedium
|
||||
.override(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: AppConstants()
|
||||
.title_text_fontSize,
|
||||
color:
|
||||
messageController
|
||||
.model.type ==
|
||||
1
|
||||
? themeController
|
||||
.currentColor.sc3
|
||||
: themeController
|
||||
.currentColor.sc2,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
return messageController.model
|
||||
.system_message_read ==
|
||||
1
|
||||
? Positioned(
|
||||
top: -4,
|
||||
right: -14,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration:
|
||||
const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Text(
|
||||
'系统消息'.tr,
|
||||
style:
|
||||
FlutterFlowTheme.of(context)
|
||||
.bodyMedium
|
||||
.override(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: AppConstants()
|
||||
.title_text_fontSize,
|
||||
color: messageController
|
||||
.model.type ==
|
||||
1
|
||||
? themeController
|
||||
.currentColor
|
||||
.sc3
|
||||
: themeController
|
||||
.currentColor
|
||||
.sc2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
Obx(() {
|
||||
return messageController.model
|
||||
.system_message_read ==
|
||||
1
|
||||
? Positioned(
|
||||
top: -4,
|
||||
right: -14,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration:
|
||||
const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape:
|
||||
BoxShape.circle,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
].divide(SizedBox(width: 10.rpx)),
|
||||
],
|
||||
),
|
||||
// 动画线
|
||||
Obx(() {
|
||||
double lineWidth = 170.rpx;
|
||||
double lineWidth =
|
||||
MediaQuery.sizeOf(context).width * 0.5 -
|
||||
20.rpx; // 每个容器占宽度的一半
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
bottom: 0,
|
||||
left: messageController.model.type == 1
|
||||
? 0
|
||||
: 170.rpx,
|
||||
: MediaQuery.sizeOf(context).width * 0.5 +
|
||||
10.rpx, // 动态设置左侧位置
|
||||
child: Container(
|
||||
width: lineWidth,
|
||||
height: 4.rpx,
|
||||
|
||||
@@ -436,6 +436,7 @@ class _ApplyRepairPageState extends State<ApplyRepairPage> {
|
||||
hintStyle: TextStyle(
|
||||
letterSpacing: 0.0,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
@@ -477,6 +478,7 @@ class _ApplyRepairPageState extends State<ApplyRepairPage> {
|
||||
cursorColor: themeController.currentColor.sc3,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(width: 24.rpx)),
|
||||
|
||||
119
lib/pages/sleep_report/chart/AdviceComponnetWidget.dart
Normal file
119
lib/pages/sleep_report/chart/AdviceComponnetWidget.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class AdviceComponnetWidget extends StatefulWidget {
|
||||
final String title; // 建议标题
|
||||
final String description; // 建议说明
|
||||
|
||||
const AdviceComponnetWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdviceComponnetWidget> createState() => _AdviceComponnetWidgetState();
|
||||
}
|
||||
|
||||
class _AdviceComponnetWidgetState extends State<AdviceComponnetWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
// 显示标题
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: stringToColor("#313541"),
|
||||
borderRadius: BorderRadius.circular(20.rpx),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
28.rpx, 30.rpx, 28.rpx, 30.rpx),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Text(
|
||||
widget.title, // 使用传入的标题
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 显示描述
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
28.rpx, 30.rpx, 28.rpx, 30.rpx),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: 26.rpx, // 设置文字大小
|
||||
height: 1.3, // 设置行高,控制文字上下间距
|
||||
),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle, // 图标和文字垂直居中
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 4.rpx, // 调整底部间距
|
||||
right: 10.rpx, // 适当调整图标右边距
|
||||
top: 4.rpx, // 增加顶部间距
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/ai.svg', // 替换为你的 SVG 文件路径
|
||||
width: 37.rpx, // 设置适中的 SVG 图标大小
|
||||
height: 31.rpx, // 使图标和文字大小一致
|
||||
color:
|
||||
themeController.currentColor.sc2, // 设置 SVG 颜色
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: widget.description, // 使用传入的描述
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().normal_text_fontSize, // 文字大小
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
108
lib/pages/sleep_report/chart/DataShowWidget.dart
Normal file
108
lib/pages/sleep_report/chart/DataShowWidget.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
|
||||
class DataShowWidget extends StatefulWidget {
|
||||
final Widget widget1; // 第一个传入的 widget
|
||||
final Widget widget2; // 第二个传入的 widget
|
||||
final Widget widget3; // 第三个传入的 widget
|
||||
final Widget widget4; // 第四个传入的 widget
|
||||
final MainAxisAlignment alignment; // 控制 Row 的对齐方式
|
||||
|
||||
const DataShowWidget({
|
||||
super.key,
|
||||
required this.widget1,
|
||||
required this.widget2,
|
||||
required this.widget3,
|
||||
required this.widget4,
|
||||
this.alignment = MainAxisAlignment.start, // 默认左对齐
|
||||
});
|
||||
|
||||
@override
|
||||
State<DataShowWidget> createState() => _DataShowWidgetState();
|
||||
}
|
||||
|
||||
class _DataShowWidgetState extends State<DataShowWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 66.rpx,
|
||||
decoration: BoxDecoration(),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal, // 设置横向滚动
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.alignment, // 根据传入的 alignment 控制对齐方式
|
||||
children: [
|
||||
// 放入传入的 widget1
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.4, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 根据传入的 alignment 设置对齐
|
||||
child: widget.widget1, // 显示传入的 widget1
|
||||
),
|
||||
),
|
||||
// 放入传入的 widget2
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.15, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 同样设置对齐
|
||||
child: widget.widget2, // 显示传入的 widget2
|
||||
),
|
||||
),
|
||||
// 放入传入的 widget3
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.2, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 同样设置对齐
|
||||
child: widget.widget3, // 显示传入的 widget3
|
||||
),
|
||||
),
|
||||
// 放入传入的 widget4
|
||||
Container(
|
||||
width: MediaQuery.sizeOf(context).width * 0.15, // 固定宽度
|
||||
decoration: BoxDecoration(),
|
||||
child: Align(
|
||||
alignment: widget.alignment == MainAxisAlignment.start
|
||||
? Alignment.centerLeft
|
||||
: widget.alignment == MainAxisAlignment.center
|
||||
? Alignment.center
|
||||
: Alignment.centerRight, // 同样设置对齐
|
||||
child: widget.widget4, // 显示传入的 widget4
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ class SingleBarPainter extends CustomPainter {
|
||||
TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
|
||||
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
textPainter.text =
|
||||
TextSpan(text: '${value.toStringAsFixed(0)}%', style: textStyle);
|
||||
TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle);
|
||||
textPainter.layout();
|
||||
canvas.save();
|
||||
canvas.clipRect(Rect.fromLTWH(
|
||||
|
||||
158
lib/pages/sleep_report/chart/LineChart.dart
Normal file
158
lib/pages/sleep_report/chart/LineChart.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
class LineChart extends StatelessWidget {
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double minValue;
|
||||
final double maxValue;
|
||||
final List<Map<String, dynamic>> dataPoints;
|
||||
|
||||
LineChart({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.dataPoints,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(double.infinity, 300),
|
||||
painter: LineChartPainter(
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
dataPoints: dataPoints,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LineChartPainter extends CustomPainter {
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double minValue;
|
||||
final double maxValue;
|
||||
final List<Map<String, dynamic>> dataPoints;
|
||||
|
||||
LineChartPainter({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.dataPoints,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint paint = Paint()..style = PaintingStyle.stroke;
|
||||
double chartWidth = size.width;
|
||||
double chartHeight = size.height;
|
||||
|
||||
// 时间轴刻度设置
|
||||
DateFormat timeFormatStartEnd = DateFormat('HH:mm');
|
||||
DateFormat timeFormatMiddle = DateFormat('h');
|
||||
|
||||
// 绘制Y轴刻度
|
||||
double yAxisHeight = chartHeight - 40; // 留一些空间给X轴
|
||||
double yAxisStep = yAxisHeight / 4;
|
||||
paint.color = Colors.grey;
|
||||
paint.strokeWidth = 1;
|
||||
canvas.drawLine(Offset(30, 0), Offset(30, chartHeight), paint); // Y轴
|
||||
|
||||
// 绘制Y轴的刻度线
|
||||
paint.color = Colors.grey;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
double y = i * yAxisStep;
|
||||
if (i == 0) {
|
||||
paint.color = Colors.grey; // 0线
|
||||
canvas.drawLine(Offset(25, y), Offset(35, y), paint);
|
||||
} else if (i == 1) {
|
||||
paint.color = Colors.red; // 最小值线
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.strokeWidth = 1;
|
||||
canvas.drawLine(Offset(25, y), Offset(35, y), paint);
|
||||
} else if (i == 2) {
|
||||
paint.color = Colors.grey; // 最大值与最小值中间线
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.strokeWidth = 1;
|
||||
_drawDashedLine(canvas, paint, 25, y, 35, y); // Custom dashed line
|
||||
} else {
|
||||
paint.color = Colors.red; // 最大值线
|
||||
canvas.drawLine(Offset(25, y), Offset(35, y), paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制X轴时间刻度
|
||||
DateTime startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||
DateTime endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
double xAxisStep = (chartWidth - 60) /
|
||||
(endDate.millisecondsSinceEpoch - startDate.millisecondsSinceEpoch);
|
||||
|
||||
for (DateTime date = startDate;
|
||||
date.isBefore(endDate);
|
||||
date = date.add(Duration(hours: 1))) {
|
||||
String timeLabel = (date == startDate || date == endDate)
|
||||
? timeFormatStartEnd.format(date)
|
||||
: timeFormatMiddle.format(date);
|
||||
paint.color = Colors.black;
|
||||
|
||||
// Draw text using TextPainter
|
||||
_drawText(
|
||||
canvas, timeLabel, Offset(30, yAxisHeight)); // Position dynamically
|
||||
}
|
||||
|
||||
// 绘制折线图数据点
|
||||
Path path = Path();
|
||||
for (var i = 0; i < dataPoints.length; i++) {
|
||||
var point = dataPoints[i];
|
||||
DateTime pointTime = DateTime.fromMillisecondsSinceEpoch(point['time']);
|
||||
double x = (pointTime.millisecondsSinceEpoch -
|
||||
startDate.millisecondsSinceEpoch) *
|
||||
xAxisStep +
|
||||
30;
|
||||
double y = chartHeight -
|
||||
(point['value'] - minValue) * yAxisHeight / (maxValue - minValue);
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
|
||||
paint.color = Colors.green; // Line color based on range
|
||||
paint.strokeWidth = 2;
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
// Custom method to draw dashed line
|
||||
void _drawDashedLine(Canvas canvas, Paint paint, double startX, double startY,
|
||||
double endX, double endY) {
|
||||
double dashWidth = 5;
|
||||
double dashSpace = 3;
|
||||
double distance = (endX - startX).abs();
|
||||
double dashCount = (distance / (dashWidth + dashSpace)).floorToDouble();
|
||||
for (int i = 0; i < dashCount; i++) {
|
||||
double startXDash = startX + (i * (dashWidth + dashSpace));
|
||||
double endXDash = startXDash + dashWidth;
|
||||
canvas.drawLine(
|
||||
Offset(startXDash, startY), Offset(endXDash, endY), paint);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom method to draw text
|
||||
void _drawText(Canvas canvas, String text, Offset offset) {
|
||||
TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text, style: TextStyle(color: Colors.black, fontSize: 12)),
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
textPainter.paint(canvas, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -154,19 +154,19 @@ class _LineChartByRangePainter extends CustomPainter {
|
||||
int totalHours = maxTime.difference(minTime).inHours;
|
||||
int startHour = minTime.hour;
|
||||
|
||||
for (int i = 1; i < totalHours; i++) {
|
||||
double x = xStart + chartWidth * i / totalHours;
|
||||
// for (int i = 1; i < totalHours; i++) {
|
||||
// double x = xStart + chartWidth * i / totalHours;
|
||||
|
||||
// 垂直虚线
|
||||
drawDashedLine(
|
||||
canvas,
|
||||
Offset(x, 0),
|
||||
Offset(x, chartHeight),
|
||||
axisPaint,
|
||||
dashWidth: 4.rpx,
|
||||
dashSpace: 4.rpx,
|
||||
);
|
||||
}
|
||||
// // 垂直虚线
|
||||
// drawDashedLine(
|
||||
// canvas,
|
||||
// Offset(x, 0),
|
||||
// Offset(x, chartHeight),
|
||||
// axisPaint,
|
||||
// dashWidth: 4.rpx,
|
||||
// dashSpace: 4.rpx,
|
||||
// );
|
||||
// }
|
||||
|
||||
// 5. 画左侧完整时分 (HH:mm),往内缩 labelInset
|
||||
String leftLabel = DateFormat('HH:mm').format(minTime);
|
||||
|
||||
109
lib/pages/sleep_report/chart/RadarChart.dart
Normal file
109
lib/pages/sleep_report/chart/RadarChart.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math';
|
||||
|
||||
class RadarChart extends StatelessWidget {
|
||||
final List<List<double>> data; // 存储多个数据集
|
||||
final List<String> labels; // 每个角的标签
|
||||
final double maxValue; // 数据的最大值,用来统一尺度
|
||||
|
||||
const RadarChart({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required this.labels,
|
||||
this.maxValue = 100,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(300, 300), // 图表的大小
|
||||
painter: RadarChartPainter(
|
||||
data: data,
|
||||
labels: labels,
|
||||
maxValue: maxValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RadarChartPainter extends CustomPainter {
|
||||
final List<List<double>> data;
|
||||
final List<String> labels;
|
||||
final double maxValue;
|
||||
|
||||
RadarChartPainter({
|
||||
required this.data,
|
||||
required this.labels,
|
||||
required this.maxValue,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Paint paintLine = Paint()
|
||||
..color = Colors.blue
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
Paint axisPaint = Paint()
|
||||
..color = Colors.grey.withOpacity(0.5)
|
||||
..strokeWidth = 1;
|
||||
|
||||
double centerX = size.width / 2;
|
||||
double centerY = size.height / 2;
|
||||
double radius = size.width / 2;
|
||||
|
||||
int numOfPoints = labels.length;
|
||||
|
||||
// 绘制雷达图的轴线
|
||||
for (int i = 0; i < numOfPoints; i++) {
|
||||
double angle = (2 * pi / numOfPoints) * i;
|
||||
double x = centerX + radius * cos(angle);
|
||||
double y = centerY + radius * sin(angle);
|
||||
|
||||
// 画轴线
|
||||
canvas.drawLine(Offset(centerX, centerY), Offset(x, y), axisPaint);
|
||||
|
||||
// 绘制标签
|
||||
TextPainter tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: labels[i],
|
||||
style: TextStyle(color: Colors.black, fontSize: 12),
|
||||
),
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
);
|
||||
tp.layout();
|
||||
tp.paint(canvas, Offset(x + 8, y - 8)); // 设置标签位置
|
||||
}
|
||||
|
||||
// 绘制多个数据集
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
Paint fillPaint = Paint()
|
||||
..color = Colors.primaries[i % Colors.primaries.length].withOpacity(0.3)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
Paint linePaint = Paint()
|
||||
..color = Colors.primaries[i % Colors.primaries.length]
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
List<Offset> points = [];
|
||||
for (int j = 0; j < numOfPoints; j++) {
|
||||
double angle = (2 * pi / numOfPoints) * j;
|
||||
double pointRadius = (data[i][j] / maxValue) * radius;
|
||||
double x = centerX + pointRadius * cos(angle);
|
||||
double y = centerY + pointRadius * sin(angle);
|
||||
|
||||
points.add(Offset(x, y));
|
||||
}
|
||||
|
||||
// 画出数据连接线
|
||||
Path path = Path()..addPolygon(points, true);
|
||||
canvas.drawPath(path, linePaint);
|
||||
canvas.drawPath(path, fillPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
118
lib/pages/sleep_report/chart/ScatterPlotChart.dart
Normal file
118
lib/pages/sleep_report/chart/ScatterPlotChart.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class ScatterPlotChart extends StatelessWidget {
|
||||
final List<ScatterSpot> points;
|
||||
final int xMax;
|
||||
final int yMax;
|
||||
final Color pointColor;
|
||||
final int divisions;
|
||||
|
||||
ScatterPlotChart({
|
||||
required this.points,
|
||||
required this.xMax,
|
||||
required this.yMax,
|
||||
required this.pointColor,
|
||||
required this.divisions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 计算向上取整后的最大值
|
||||
double xMaxCeil = (xMax / 100).ceil() * 100.0;
|
||||
double yMaxCeil = (yMax / 100).ceil() * 100.0;
|
||||
|
||||
return SizedBox(
|
||||
child: ScatterChart(
|
||||
ScatterChartData(
|
||||
backgroundColor: Colors.transparent,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
horizontalInterval: yMaxCeil / divisions,
|
||||
verticalInterval: xMaxCeil / divisions,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4, // 设置网格线颜色
|
||||
strokeWidth: 0.5,
|
||||
);
|
||||
},
|
||||
getDrawingVerticalLine: (value) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4, // 设置网格线颜色
|
||||
strokeWidth: 0.5,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 60.rpx, // 给 y 轴标签更多空间
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx), // 右侧加间距
|
||||
child: Text(
|
||||
value.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right, // 右对齐
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
return Text(
|
||||
value.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border.all(
|
||||
color: themeController.currentColor.sc4,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
// 修改散点的大小和颜色
|
||||
scatterSpots: points.map((point) {
|
||||
return ScatterSpot(
|
||||
point.x, // x 坐标
|
||||
point.y, // y 坐标
|
||||
dotPainter: FlDotCirclePainter(
|
||||
radius: 3.rpx, // 自定义大小
|
||||
color: pointColor, // 自定义颜色
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
minX: 0,
|
||||
maxX: xMaxCeil,
|
||||
minY: 0,
|
||||
maxY: yMaxCeil,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class SegmentedCircleWithCenterWidget extends StatelessWidget {
|
||||
Positioned(
|
||||
right: 60.rpx, // 放置在右侧
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/add.svg',
|
||||
'assets/img/icon/score_down.svg',
|
||||
width: 14.rpx,
|
||||
height: 22.rpx,
|
||||
color: themeController.currentColor.sc9,
|
||||
|
||||
100
lib/pages/sleep_report/chart/SleepRadarChart.dart
Normal file
100
lib/pages/sleep_report/chart/SleepRadarChart.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class SleepRadarChart extends StatelessWidget {
|
||||
final Map<String, double> today;
|
||||
final Map<String, double> yesterday;
|
||||
|
||||
const SleepRadarChart({
|
||||
Key? key,
|
||||
required this.today,
|
||||
required this.yesterday,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 雷达图
|
||||
_buildRadarChart(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRadarChart() {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1.3,
|
||||
child: RadarChart(
|
||||
RadarChartData(
|
||||
dataSets: [
|
||||
// 今日数据
|
||||
RadarDataSet(
|
||||
dataEntries: [
|
||||
RadarEntry(value: today['type1']!), // 呼吸暂停
|
||||
RadarEntry(value: today['type2']!), // 入睡时间
|
||||
RadarEntry(value: today['type3']!), // 离床次数
|
||||
RadarEntry(value: today['type4']!), // 深睡比例
|
||||
RadarEntry(value: today['type5']!), // 睡眠时长
|
||||
],
|
||||
borderColor: stringToColor("#00C1AA"),
|
||||
borderWidth: 2,
|
||||
fillColor: Colors.transparent,
|
||||
entryRadius: 0,
|
||||
),
|
||||
// 昨日数据
|
||||
RadarDataSet(
|
||||
dataEntries: [
|
||||
RadarEntry(value: yesterday['type1']!), // 呼吸暂停
|
||||
RadarEntry(value: yesterday['type2']!), // 入睡时间
|
||||
RadarEntry(value: yesterday['type3']!), // 离床次数
|
||||
RadarEntry(value: yesterday['type4']!), // 深睡比例
|
||||
RadarEntry(value: yesterday['type5']!), // 睡眠时长
|
||||
],
|
||||
borderColor: stringToColor("#FFD251"),
|
||||
borderWidth: 2,
|
||||
fillColor: Colors.transparent,
|
||||
entryRadius: 0,
|
||||
),
|
||||
],
|
||||
radarBackgroundColor: stringToColor("#343844"),
|
||||
radarBorderData:
|
||||
BorderSide(color: themeController.currentColor.sc4, width: 1),
|
||||
radarShape: RadarShape.polygon,
|
||||
titlePositionPercentageOffset: 0.2,
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
color: themeController.currentColor.sc3),
|
||||
getTitle: (index, angle) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return RadarChartTitle(text: '呼吸暂停');
|
||||
case 1:
|
||||
return RadarChartTitle(text: '入睡时间');
|
||||
case 2:
|
||||
return RadarChartTitle(text: '离床次数');
|
||||
case 3:
|
||||
return RadarChartTitle(text: '深睡比例');
|
||||
case 4:
|
||||
return RadarChartTitle(text: '睡眠时长');
|
||||
default:
|
||||
return const RadarChartTitle(text: '');
|
||||
}
|
||||
},
|
||||
tickCount: 5,
|
||||
ticksTextStyle:
|
||||
const TextStyle(color: Colors.transparent, fontSize: 10),
|
||||
// ticksColor: Colors.grey.shade300,
|
||||
gridBorderData: BorderSide(color: Colors.transparent, width: 1),
|
||||
tickBorderData:
|
||||
BorderSide(color: themeController.currentColor.sc4, width: 1),
|
||||
),
|
||||
swapAnimationDuration: const Duration(milliseconds: 400),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
273
lib/pages/sleep_report/chart/TimeLineChart.dart
Normal file
273
lib/pages/sleep_report/chart/TimeLineChart.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class TimeLineChart extends StatelessWidget {
|
||||
final List<DataPoint> points;
|
||||
final double yMin;
|
||||
final double yMax;
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const TimeLineChart({
|
||||
super.key,
|
||||
required this.points,
|
||||
required this.yMin,
|
||||
required this.yMax,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.width = 400,
|
||||
this.height = 300,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size(width, height),
|
||||
painter: _TimeLineChartPainter(
|
||||
points: points,
|
||||
yMin: yMin,
|
||||
yMax: yMax,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DataPoint {
|
||||
final int timestamp;
|
||||
final double value;
|
||||
|
||||
DataPoint(this.timestamp, this.value);
|
||||
}
|
||||
|
||||
class _TimeLineChartPainter extends CustomPainter {
|
||||
final List<DataPoint> points;
|
||||
final double yMin;
|
||||
final double yMax;
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
|
||||
_TimeLineChartPainter({
|
||||
required this.points,
|
||||
required this.yMin,
|
||||
required this.yMax,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_drawYAxis(canvas, size);
|
||||
_drawXAxis(canvas, size);
|
||||
_drawLine(canvas, size);
|
||||
}
|
||||
|
||||
void _drawXAxis(Canvas canvas, Size size) {
|
||||
const margin = 40.0;
|
||||
final paint = Paint()..color = Colors.black;
|
||||
final textStyle = const TextStyle(color: Colors.black, fontSize: 12);
|
||||
|
||||
// Draw X axis line
|
||||
canvas.drawLine(
|
||||
Offset(margin, size.height - margin),
|
||||
Offset(size.width - margin, size.height - margin),
|
||||
paint,
|
||||
);
|
||||
|
||||
// Generate time ticks
|
||||
final timeFormatStartEnd = DateFormat('HH:mm');
|
||||
final timeFormatMiddle = DateFormat('h');
|
||||
final startDateTime = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||
final endDateTime = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
|
||||
List<DateTime> hourTicks = [];
|
||||
DateTime current = DateTime(
|
||||
startDateTime.year,
|
||||
startDateTime.month,
|
||||
startDateTime.day,
|
||||
startDateTime.hour,
|
||||
).add(const Duration(hours: 1));
|
||||
|
||||
while (current.isBefore(endDateTime)) {
|
||||
if (current.isAfter(startDateTime)) {
|
||||
hourTicks.add(current);
|
||||
}
|
||||
current = current.add(const Duration(hours: 1));
|
||||
}
|
||||
|
||||
void drawTick(DateTime time, bool isEdge) {
|
||||
final x = margin +
|
||||
((time.millisecondsSinceEpoch - startTime) / (endTime - startTime)) *
|
||||
(size.width - 2 * margin);
|
||||
|
||||
final text = isEdge
|
||||
? timeFormatStartEnd.format(time)
|
||||
: timeFormatMiddle.format(time);
|
||||
|
||||
_drawText(
|
||||
canvas,
|
||||
text,
|
||||
Offset(x, size.height - margin + 20),
|
||||
TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
drawTick(startDateTime, true);
|
||||
drawTick(endDateTime, true);
|
||||
for (var tick in hourTicks) {
|
||||
drawTick(tick, false);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawYAxis(Canvas canvas, Size size) {
|
||||
const margin = 40.0;
|
||||
final midValue = (yMax + yMin) / 2;
|
||||
|
||||
// 计算三条虚线之间的垂直间距
|
||||
final lineSpacing = (size.height - 2 * margin) / 3; // 让三条线之间的间距相等
|
||||
|
||||
// 新增的 y=0 实线的垂直位置
|
||||
final zeroLinePosition = margin + lineSpacing * 3; // 确保 y=0 位于三条虚线下方
|
||||
|
||||
void drawLine(double value, Color color,
|
||||
{bool isDashed = false, bool isSolid = false}) {
|
||||
final y =
|
||||
(value - yMax) / (yMin - yMax) * (size.height - 2 * margin) + margin;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(margin, y);
|
||||
path.lineTo(size.width - margin, y);
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = (color != Colors.grey) ? 2 : 1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
if (isDashed) {
|
||||
Path dashedPath = _createDashedPath(path, dashWidth: 5, dashSpace: 5);
|
||||
canvas.drawPath(dashedPath, paint);
|
||||
} else if (isSolid) {
|
||||
// 对于实线,直接绘制
|
||||
canvas.drawPath(path, paint);
|
||||
} else {
|
||||
// 默认使用虚线绘制
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制 y=0 的灰色实线,并将其放置在三条虚线的下方
|
||||
if (yMin < 0 && yMax > 0) {
|
||||
drawLine(0, Colors.grey, isSolid: true); // 灰色实线绘制 y=0 线
|
||||
}
|
||||
|
||||
// 绘制最小值、中间值、最大值的虚线
|
||||
drawLine(yMin, themeController.currentColor.sc9, isDashed: true);
|
||||
drawLine(midValue, themeController.currentColor.sc4, isDashed: true);
|
||||
drawLine(yMax, themeController.currentColor.sc9, isDashed: true);
|
||||
}
|
||||
|
||||
Path _createDashedPath(Path path,
|
||||
{required double dashWidth, required double dashSpace}) {
|
||||
final Path dashedPath = Path();
|
||||
final ui.PathMetrics metrics = path.computeMetrics();
|
||||
|
||||
for (ui.PathMetric metric in metrics) {
|
||||
double distance = 0;
|
||||
while (distance < metric.length) {
|
||||
dashedPath.addPath(
|
||||
metric.extractPath(distance, distance + dashWidth),
|
||||
Offset.zero,
|
||||
);
|
||||
distance += dashWidth + dashSpace;
|
||||
}
|
||||
}
|
||||
|
||||
return dashedPath;
|
||||
}
|
||||
|
||||
void _drawLine(Canvas canvas, Size size) {
|
||||
const margin = 40.0;
|
||||
final sortedPoints = points
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
Path? currentPath;
|
||||
Paint currentPaint = _createPaint(Colors.green);
|
||||
|
||||
for (int i = 0; i < sortedPoints.length - 1; i++) {
|
||||
final p1 = sortedPoints[i];
|
||||
final p2 = sortedPoints[i + 1];
|
||||
|
||||
final x1 = margin +
|
||||
((p1.timestamp - startTime) / (endTime - startTime)) *
|
||||
(size.width - 2 * margin);
|
||||
final y1 = margin +
|
||||
(1 - (p1.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin);
|
||||
|
||||
final x2 = margin +
|
||||
((p2.timestamp - startTime) / (endTime - startTime)) *
|
||||
(size.width - 2 * margin);
|
||||
final y2 = margin +
|
||||
(1 - (p2.value - yMin) / (yMax - yMin)) * (size.height - 2 * margin);
|
||||
|
||||
final shouldBeGreen = p1.value >= yMin &&
|
||||
p1.value <= yMax &&
|
||||
p2.value >= yMin &&
|
||||
p2.value <= yMax;
|
||||
|
||||
// 根据当前线段的状态来决定是否切换颜色和虚线状态
|
||||
if (shouldBeGreen != (currentPaint.color == Colors.green)) {
|
||||
if (currentPath != null) {
|
||||
canvas.drawPath(currentPath, currentPaint);
|
||||
}
|
||||
currentPath = Path();
|
||||
currentPaint = _createPaint(shouldBeGreen ? Colors.green : Colors.red);
|
||||
}
|
||||
|
||||
currentPath ??= Path();
|
||||
if (i == 0) currentPath.moveTo(x1, y1);
|
||||
currentPath.lineTo(x2, y2);
|
||||
}
|
||||
|
||||
// 绘制剩余路径
|
||||
if (currentPath != null) {
|
||||
if (currentPaint.color == Colors.red) {
|
||||
// 如果是红色线,绘制虚线
|
||||
final dashedPath =
|
||||
_createDashedPath(currentPath, dashWidth: 5, dashSpace: 5);
|
||||
canvas.drawPath(dashedPath, currentPaint);
|
||||
} else {
|
||||
canvas.drawPath(currentPath, currentPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Paint _createPaint(Color color) => Paint()
|
||||
..color = color
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
void _drawText(Canvas canvas, String text, Offset offset, TextAlign align) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 12),
|
||||
),
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
final centeredOffset = offset.translate(
|
||||
-textPainter.width / 2,
|
||||
-textPainter.height / 2,
|
||||
);
|
||||
textPainter.paint(canvas, centeredOffset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
281
lib/pages/sleep_report/chart/TimeSeriesChart.dart
Normal file
281
lib/pages/sleep_report/chart/TimeSeriesChart.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
|
||||
class TimeSeriesChart extends StatelessWidget {
|
||||
final int startTime;
|
||||
final int endTime;
|
||||
final double yMin;
|
||||
final double yMax;
|
||||
final List<TimeSeriesPoint> dataPoints;
|
||||
|
||||
TimeSeriesChart({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.yMin,
|
||||
required this.yMax,
|
||||
required this.dataPoints,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final midValue = (yMax + yMin) / 2;
|
||||
final xLabels = _generateXLabels();
|
||||
|
||||
// Prepare spots and segments
|
||||
List<FlSpot> spots = [];
|
||||
List<Color> lineColors = [];
|
||||
|
||||
for (int i = 0; i < dataPoints.length; i++) {
|
||||
final point = dataPoints[i];
|
||||
final xValue = _convertTimeToXValue(point.timestamp);
|
||||
final yValue = point.value;
|
||||
|
||||
spots.add(FlSpot(xValue, yValue));
|
||||
if (yValue >= yMin && yValue <= yMax) {
|
||||
lineColors.add(Colors.green); // Color for points within range
|
||||
} else {
|
||||
lineColors.add(Colors.red); // Color for points outside range
|
||||
}
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: 2,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (List<LineBarSpot> touchedSpots) {
|
||||
return touchedSpots.map((spot) {
|
||||
final time = DateTime.fromMillisecondsSinceEpoch(
|
||||
_convertXValueToTime(spot.x),
|
||||
);
|
||||
return LineTooltipItem(
|
||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}\n${spot.y.toStringAsFixed(0)}',
|
||||
const TextStyle(color: Colors.black),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
if (value == 0) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4,
|
||||
strokeWidth: 1,
|
||||
);
|
||||
} else if (value == yMin) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc9,
|
||||
strokeWidth: 1,
|
||||
dashArray: [5, 5],
|
||||
);
|
||||
} else if (value == yMax) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc9,
|
||||
strokeWidth: 1,
|
||||
dashArray: [5, 5],
|
||||
);
|
||||
} else if (value == midValue) {
|
||||
return FlLine(
|
||||
color: themeController.currentColor.sc4,
|
||||
strokeWidth: 1,
|
||||
dashArray: [5, 5],
|
||||
);
|
||||
}
|
||||
return FlLine(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index >= 0 && index < xLabels.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
xLabels[index].label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value == 0) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
value.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
} else if (value == yMin) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
yMin.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
} else if (value == midValue) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
midValue.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
} else if (value == yMax) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 14.rpx),
|
||||
child: Text(
|
||||
yMax.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
reservedSize: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: false,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
minX: 0,
|
||||
maxX: xLabels.length - 1,
|
||||
minY: min(0, yMin) - (yMax - yMin) * 0.2,
|
||||
maxY: yMax + (yMax - yMin) * 0.2,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: false,
|
||||
color: themeController.currentColor.sc2,
|
||||
barWidth: 2,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(show: false), // Disable dots
|
||||
belowBarData: BarAreaData(show: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XLabel> _generateXLabels() {
|
||||
final labels = <XLabel>[];
|
||||
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||
|
||||
labels.add(XLabel(
|
||||
time: startTime,
|
||||
label:
|
||||
'${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}',
|
||||
));
|
||||
|
||||
DateTime current = DateTime(
|
||||
startDate.year,
|
||||
startDate.month,
|
||||
startDate.day,
|
||||
startDate.hour + 1,
|
||||
);
|
||||
|
||||
while (current.isBefore(endDate)) {
|
||||
labels.add(XLabel(
|
||||
time: current.millisecondsSinceEpoch,
|
||||
label: current.hour.toString(),
|
||||
));
|
||||
current = current.add(Duration(hours: 1));
|
||||
}
|
||||
|
||||
labels.add(XLabel(
|
||||
time: endTime,
|
||||
label:
|
||||
'${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}',
|
||||
));
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
double _convertTimeToXValue(int timestamp) {
|
||||
final totalDuration = endTime - startTime;
|
||||
final pointDuration = timestamp - startTime;
|
||||
final xLabels = _generateXLabels();
|
||||
return (pointDuration / totalDuration) * (xLabels.length - 1);
|
||||
}
|
||||
|
||||
int _convertXValueToTime(double xValue) {
|
||||
final xLabels = _generateXLabels();
|
||||
final totalDuration = endTime - startTime;
|
||||
final ratio = xValue / (xLabels.length - 1);
|
||||
return startTime + (totalDuration * ratio).round();
|
||||
}
|
||||
}
|
||||
|
||||
class TimeSeriesPoint {
|
||||
final int timestamp;
|
||||
final double value;
|
||||
|
||||
TimeSeriesPoint(this.timestamp, this.value);
|
||||
}
|
||||
|
||||
class XLabel {
|
||||
final int time;
|
||||
final String label;
|
||||
|
||||
XLabel({required this.time, required this.label});
|
||||
}
|
||||
140
lib/pages/sleep_report/component/AIAdviceWidget.dart
Normal file
140
lib/pages/sleep_report/component/AIAdviceWidget.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/AdviceComponnetWidget.dart';
|
||||
|
||||
class AIAdviceWidget extends StatefulWidget {
|
||||
AIAdviceWidget({super.key});
|
||||
|
||||
@override
|
||||
State<AIAdviceWidget> createState() => _AIAdviceWidgetState();
|
||||
}
|
||||
|
||||
class _AIAdviceWidgetState extends State<AIAdviceWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List advices = [
|
||||
{
|
||||
"title": "调整作息时间",
|
||||
"description": "确保每天在相同的时间上床并醒来。保持规律的作息可以帮助调整你的生物钟,改善睡眠质量。",
|
||||
},
|
||||
{
|
||||
"title": "减少睡前刺激",
|
||||
"description": "避免在睡前使用电子设备,如手机、电脑等,减少屏幕时间,以防影响你的睡眠质量。",
|
||||
},
|
||||
{
|
||||
"title": "创造理想睡眠环境",
|
||||
"description": "确保卧室安静、黑暗且舒适。调节室温,并避免过于嘈杂或明亮的环境,帮助你快速入睡。",
|
||||
},
|
||||
{
|
||||
"title": "避免摄入咖啡因和酒精",
|
||||
"description": "避免在睡前几小时内摄入咖啡、茶、酒精等饮品,因为这些物质可能会干扰你的睡眠。",
|
||||
},
|
||||
{
|
||||
"title": "增加日间活动",
|
||||
"description": "适量的日间运动可以帮助提高睡眠质量,但要避免睡前剧烈运动,以免影响入睡。",
|
||||
},
|
||||
{
|
||||
"title": "放松身心",
|
||||
"description": "睡前可以进行一些放松活动,如深呼吸、冥想或听轻音乐,这有助于减轻压力并促进良好的睡眠。",
|
||||
},
|
||||
];
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc5,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"AI分析".tr,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().title_text_fontSize),
|
||||
),
|
||||
ClickableContainer(
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
14.rpx, 0.rpx, 14.rpx, 0), //
|
||||
borderRadius: 0.rpx, // 圆形点击区域
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
"AI分析介绍".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
|
||||
width: 28.rpx,
|
||||
height: 28.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
fit: BoxFit.cover,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 31.rpx,
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx),
|
||||
child: Column(
|
||||
children: advices.map<Widget>((advice) {
|
||||
return AdviceComponnetWidget(
|
||||
title: advice["title"],
|
||||
description: advice["description"],
|
||||
).paddingOnly(bottom: 0.rpx); // 在每个组件下方添加间隔
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
315
lib/pages/sleep_report/component/BreatheStandardWidget.dart
Normal file
315
lib/pages/sleep_report/component/BreatheStandardWidget.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/TimeSeriesChart.dart';
|
||||
|
||||
class BreatheStandardWidget extends StatefulWidget {
|
||||
BreatheStandardWidget({super.key});
|
||||
|
||||
@override
|
||||
State<BreatheStandardWidget> createState() => _BreatheStandardWidgetState();
|
||||
}
|
||||
|
||||
class _BreatheStandardWidgetState extends State<BreatheStandardWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
final startTime = now.subtract(Duration(hours: 5)).millisecondsSinceEpoch;
|
||||
final endTime = now.millisecondsSinceEpoch;
|
||||
final dataPoints = [
|
||||
TimeSeriesPoint(startTime + Duration(minutes: 10).inMilliseconds, 50),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 1).inMilliseconds, 120),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 2).inMilliseconds, 80),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 3).inMilliseconds, 180),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 4).inMilliseconds, 30),
|
||||
TimeSeriesPoint(endTime - Duration(minutes: 10).inMilliseconds, 150),
|
||||
];
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc5,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"呼吸基准".tr,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().title_text_fontSize),
|
||||
),
|
||||
ClickableContainer(
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
14.rpx, 0.rpx, 14.rpx, 0), //
|
||||
borderRadius: 0.rpx, // 圆形点击区域
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
"呼吸基准介绍".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
|
||||
width: 28.rpx,
|
||||
height: 28.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
fit: BoxFit.cover,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 31.rpx,
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// 圆形小球容器
|
||||
Container(
|
||||
width: 14.rpx, // 圆球的直径
|
||||
height: 14.rpx,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc2, // 小球的颜色
|
||||
shape: BoxShape.circle, // 设置为圆形
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15.rpx), // 圆球和文字之间的间隔
|
||||
// 文字
|
||||
Text(
|
||||
'正常范围(8~20)',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
AppConstants().smaller_text_fontSize, // 文字的大小
|
||||
color: themeController.currentColor.sc3, // 文字颜色
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
// color: Colors.red,
|
||||
width: double.infinity,
|
||||
// height: 300.rpx,
|
||||
child: TimeSeriesChart(
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
yMin: 50,
|
||||
yMax: 150,
|
||||
dataPoints: dataPoints,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 0.rpx, 0.rpx),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"平均呼吸",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"12",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"基准呼吸",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"15",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"最低呼吸",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"11",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"最高呼吸",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"18",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(
|
||||
height: 18.rpx,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
197
lib/pages/sleep_report/component/CompareSleepWidget.dart
Normal file
197
lib/pages/sleep_report/component/CompareSleepWidget.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/AdviceComponnetWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/RadarChart.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/SleepRadarChart.dart';
|
||||
|
||||
class CompareSleepWidget extends StatefulWidget {
|
||||
CompareSleepWidget({super.key});
|
||||
|
||||
@override
|
||||
State<CompareSleepWidget> createState() => _CompareSleepWidgetState();
|
||||
}
|
||||
|
||||
class _CompareSleepWidgetState extends State<CompareSleepWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var today = {
|
||||
"type1": 40.0,
|
||||
"type2": 80.0,
|
||||
"type3": 60.0,
|
||||
"type4": 70.0,
|
||||
"type5": 100.0
|
||||
};
|
||||
var yesterday = {
|
||||
"type1": 40.0,
|
||||
"type2": 90.0,
|
||||
"type3": 50.0,
|
||||
"type4": 70.0,
|
||||
"type5": 30.0
|
||||
};
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc5,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"与昨日对比分析".tr,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().title_text_fontSize),
|
||||
),
|
||||
ClickableContainer(
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
14.rpx, 0.rpx, 14.rpx, 0), //
|
||||
borderRadius: 0.rpx, // 圆形点击区域
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
"与昨日对比分析介绍".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
|
||||
width: 28.rpx,
|
||||
height: 28.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
fit: BoxFit.cover,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 31.rpx,
|
||||
),
|
||||
// Padding(
|
||||
// padding:
|
||||
// EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
|
||||
// child: SleepRadarChart(
|
||||
// today: today,
|
||||
// yesterday: yesterday,
|
||||
// ),
|
||||
// ),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 雷达图
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 0.rpx, 0.rpx),
|
||||
child: SleepRadarChart(
|
||||
today: today,
|
||||
yesterday: yesterday,
|
||||
),
|
||||
),
|
||||
// 在左侧添加一个 Text
|
||||
Positioned(
|
||||
left: 0, // 这里可以修改文本左边的距离
|
||||
top: 0, // 这里可以修改文本顶部的距离
|
||||
child: Container(
|
||||
color: Colors.transparent, // 不需要背景色,可以去掉
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// 长条容器
|
||||
Container(
|
||||
width:
|
||||
34.rpx, // 你可以设置容器的宽度,或者使用 Expanded 填充剩余空间
|
||||
height: 2.rpx, // 容器的高度
|
||||
color: stringToColor("#00C1AA"), // 容器的颜色
|
||||
),
|
||||
SizedBox(width: 13.rpx), // 文字和容器之间的间距
|
||||
// 文字
|
||||
Text(
|
||||
'今日数据', // 文字内容
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx, // 文字大小
|
||||
color:
|
||||
themeController.currentColor.sc4, // 文字颜色
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
// 长条容器
|
||||
Container(
|
||||
width:
|
||||
34.rpx, // 你可以设置容器的宽度,或者使用 Expanded 填充剩余空间
|
||||
height: 2.rpx, // 容器的高度
|
||||
color: stringToColor("#FFD251"), // 容器的颜色
|
||||
),
|
||||
SizedBox(width: 13.rpx), // 文字和容器之间的间距
|
||||
// 文字
|
||||
Text(
|
||||
'昨日数据', // 文字内容
|
||||
style: TextStyle(
|
||||
fontSize: 18.rpx, // 文字大小
|
||||
color:
|
||||
themeController.currentColor.sc4, // 文字颜色
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
].divide(SizedBox(height: 25.rpx)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
316
lib/pages/sleep_report/component/HeartChangeWidget.dart
Normal file
316
lib/pages/sleep_report/component/HeartChangeWidget.dart
Normal file
@@ -0,0 +1,316 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/DataShowWidget.dart';
|
||||
|
||||
class HeartChangeWidget extends StatefulWidget {
|
||||
HeartChangeWidget({super.key});
|
||||
|
||||
@override
|
||||
State<HeartChangeWidget> createState() => _HeartChangeWidgetState();
|
||||
}
|
||||
|
||||
class _HeartChangeWidgetState extends State<HeartChangeWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//0上升 1下降 2持平
|
||||
List data = [
|
||||
{
|
||||
"name": "心脏总能量",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 0,
|
||||
"desc": "心脏总能量介绍"
|
||||
},
|
||||
{
|
||||
"name": "心率减速力",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 1,
|
||||
"desc": "心率减速力介绍"
|
||||
},
|
||||
{
|
||||
"name": "迷走神经张力指数",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 2,
|
||||
"desc": "迷走神经张力指数介绍"
|
||||
},
|
||||
{
|
||||
"name": "交感神经张力指数",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 0,
|
||||
"desc": "交感神经张力指数介绍"
|
||||
},
|
||||
{
|
||||
"name": "自主神经张力指数",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 2,
|
||||
"desc": "自主神经张力指数介绍"
|
||||
},
|
||||
{
|
||||
"name": "血管舒张指数",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 1,
|
||||
"desc": "血管舒张指数介绍"
|
||||
},
|
||||
{
|
||||
"name": "SDNN",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 0,
|
||||
"desc": "SDNN介绍"
|
||||
},
|
||||
{
|
||||
"name": "PNN50",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 1,
|
||||
"desc": "PNN50介绍"
|
||||
},
|
||||
{
|
||||
"name": "RMSSD",
|
||||
"value": 5262,
|
||||
"range": "2055-6000",
|
||||
"change": 2,
|
||||
"desc": "RMSSD介绍"
|
||||
},
|
||||
];
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc5,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"心率变异性(HRV)".tr,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().title_text_fontSize),
|
||||
),
|
||||
ClickableContainer(
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
14.rpx, 0.rpx, 14.rpx, 0), //
|
||||
borderRadius: 0.rpx, // 圆形点击区域
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
"心率变异性(HRV)介绍".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
|
||||
width: 28.rpx,
|
||||
height: 28.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
fit: BoxFit.cover,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 31.rpx,
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 0.rpx, 0.rpx),
|
||||
child: Column(
|
||||
children: [
|
||||
DataShowWidget(
|
||||
alignment: MainAxisAlignment.center,
|
||||
widget1: Text(
|
||||
"名称",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc4,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
widget2: Text(
|
||||
"测量值",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc4,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
widget3: Text(
|
||||
"参考范围",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc4,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
widget4: Text(
|
||||
"趋势",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc4,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: data.map<Widget>((data) {
|
||||
return DataShowWidget(
|
||||
alignment: MainAxisAlignment.center,
|
||||
widget1: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${data['name']}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
ClickableContainer(
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor: Colors.white,
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
14.rpx, 14.rpx, 14.rpx, 14.rpx),
|
||||
borderRadius: 0.rpx,
|
||||
onTap: () {
|
||||
// Get.toNamed("/deviceShareListPage", arguments: explain);
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
'${data['desc']}',
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 17.rpx,
|
||||
height: 17.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
fit: BoxFit.cover,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
widget2: Text(
|
||||
'${data['value']}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
widget3: Text(
|
||||
'${data['range']}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().normal_text_fontSize,
|
||||
),
|
||||
),
|
||||
widget4: data['change'] == 0
|
||||
? Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0, 0),
|
||||
child: Container(
|
||||
width: 22.rpx,
|
||||
height: 22.rpx,
|
||||
decoration: BoxDecoration(),
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/score_up.svg',
|
||||
// fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
)
|
||||
: data['change'] == 1
|
||||
? Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0, 0),
|
||||
child: Container(
|
||||
width: 22.rpx,
|
||||
height: 22.rpx,
|
||||
decoration: BoxDecoration(),
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/score_down.svg',
|
||||
// fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0, 0),
|
||||
child: Container(
|
||||
width: 22.rpx,
|
||||
height: 22.rpx,
|
||||
decoration: BoxDecoration(),
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/score_equal.svg',
|
||||
// fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
).paddingOnly(bottom: 0.rpx); // 在每个组件下方添加间隔
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
@@ -6,7 +9,7 @@ import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/StatusBarWithIndicator.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/ScatterPlotChart.dart';
|
||||
|
||||
class HeartPointWidget extends StatefulWidget {
|
||||
HeartPointWidget({super.key});
|
||||
@@ -33,6 +36,18 @@ class _HeartPointWidgetState extends State<HeartPointWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<ScatterSpot> data = List.generate(200, (index) {
|
||||
// 随机生成 x 和 y 值,范围都在 0-1400 之间
|
||||
double x = Random().nextDouble() * 1400; // x 值在 0-1400 范围
|
||||
double y = Random().nextDouble() * 1400; // y 值也在 0-1400 范围
|
||||
|
||||
// 返回 ScatterSpot,使用圆点绘制器自定义大小和颜色
|
||||
return ScatterSpot(
|
||||
x,
|
||||
y,
|
||||
);
|
||||
});
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
@@ -91,24 +106,30 @@ class _HeartPointWidgetState extends State<HeartPointWidget> {
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 83.rpx,
|
||||
height: 31.rpx,
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
|
||||
child: StatusBarWithIndicator(
|
||||
selectKey: 2,
|
||||
showLabel: [
|
||||
{"key": 1, "name": "正常", "color": Color(0xFF4CAF50)},
|
||||
{"key": 2, "name": "一般", "color": Color(0xFF8BC34A)},
|
||||
{"key": 3, "name": "注意", "color": Color(0xFFFFC107)},
|
||||
{"key": 4, "name": "警告", "color": Color(0xFFF44336)},
|
||||
],
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
height: MediaQuery.of(context).size.width * 0.7,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 430.rpx,
|
||||
minHeight: 430.rpx,
|
||||
),
|
||||
child: ScatterPlotChart(
|
||||
points: data,
|
||||
xMax: 1400, // x轴最大值
|
||||
yMax: 1400, // y轴最大值
|
||||
pointColor: stringToColor("#00C1AA"), // 点的颜色
|
||||
divisions: 7, // 刻度分割数量
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 56.rpx,
|
||||
),
|
||||
// SizedBox(
|
||||
// height: 31.rpx,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
316
lib/pages/sleep_report/component/HeartRateStandardWidget.dart
Normal file
316
lib/pages/sleep_report/component/HeartRateStandardWidget.dart
Normal file
@@ -0,0 +1,316 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/pages/device_bind/componnet/bind_dialog.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/chart/TimeSeriesChart.dart';
|
||||
|
||||
class HeartRateStandardWidget extends StatefulWidget {
|
||||
HeartRateStandardWidget({super.key});
|
||||
|
||||
@override
|
||||
State<HeartRateStandardWidget> createState() =>
|
||||
_HeartRateStandardWidgetState();
|
||||
}
|
||||
|
||||
class _HeartRateStandardWidgetState extends State<HeartRateStandardWidget> {
|
||||
@override
|
||||
void setState(VoidCallback callback) {
|
||||
super.setState(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
final startTime = now.subtract(Duration(hours: 5)).millisecondsSinceEpoch;
|
||||
final endTime = now.millisecondsSinceEpoch;
|
||||
final dataPoints = [
|
||||
TimeSeriesPoint(startTime + Duration(minutes: 10).inMilliseconds, 50),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 1).inMilliseconds, 120),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 2).inMilliseconds, 80),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 3).inMilliseconds, 180),
|
||||
TimeSeriesPoint(startTime + Duration(hours: 4).inMilliseconds, 30),
|
||||
TimeSeriesPoint(endTime - Duration(minutes: 10).inMilliseconds, 150),
|
||||
];
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc5,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConstants().normal_container_radius), // 你可以按需调整圆角半径
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 29.rpx, 26.rpx, 45.rpx),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"心率基准".tr,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().title_text_fontSize),
|
||||
),
|
||||
ClickableContainer(
|
||||
backgroundColor: Colors.transparent,
|
||||
highlightColor: Colors.white, // 或设置为你需要的水波纹颜色
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
14.rpx, 0.rpx, 14.rpx, 0), //
|
||||
borderRadius: 0.rpx, // 圆形点击区域
|
||||
onTap: () {
|
||||
showTipDialog(
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
"心率基准介绍".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
0, 0.rpx, 0.rpx, 0), // 外部 padding 移到内部
|
||||
width: 28.rpx,
|
||||
height: 28.rpx,
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/explain.svg',
|
||||
fit: BoxFit.cover,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 31.rpx,
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsetsDirectional.fromSTEB(0.rpx, 0.rpx, 30.rpx, 0.rpx),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// 圆形小球容器
|
||||
Container(
|
||||
width: 14.rpx, // 圆球的直径
|
||||
height: 14.rpx,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc2, // 小球的颜色
|
||||
shape: BoxShape.circle, // 设置为圆形
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15.rpx), // 圆球和文字之间的间隔
|
||||
// 文字
|
||||
Text(
|
||||
'正常范围(50~80)',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
AppConstants().smaller_text_fontSize, // 文字的大小
|
||||
color: themeController.currentColor.sc3, // 文字颜色
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
// color: Colors.red,
|
||||
width: double.infinity,
|
||||
// height: 300.rpx,
|
||||
child: TimeSeriesChart(
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
yMin: 50,
|
||||
yMax: 150,
|
||||
dataPoints: dataPoints,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 0.rpx, 0.rpx),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"平均心率",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"89",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"基准心率",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"80",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"最低心率",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"68",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"最高心率",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"98",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc2,
|
||||
fontSize:
|
||||
AppConstants().normal_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
"次/分钟",
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize:
|
||||
AppConstants().small_text_fontSize),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
].divide(SizedBox(
|
||||
width: 6.rpx,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(
|
||||
height: 18.rpx,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class _SnoreViewWidgetWidgetState extends State<SnoreViewWidgetWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"呼吸暂停监测".tr,
|
||||
"打鼾监测".tr,
|
||||
style: TextStyle(
|
||||
color: themeController.currentColor.sc3,
|
||||
fontSize: AppConstants().title_text_fontSize),
|
||||
@@ -77,7 +77,7 @@ class _SnoreViewWidgetWidgetState extends State<SnoreViewWidgetWidget> {
|
||||
context,
|
||||
Container(
|
||||
child: Text(
|
||||
"呼吸暂停监测介绍。",
|
||||
"打鼾监测介绍。",
|
||||
style: TextStyle(
|
||||
fontSize: 26.rpx,
|
||||
color: themeController.currentColor.sc3,
|
||||
|
||||
@@ -9,10 +9,15 @@ import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||
import 'package:vbvs_app/controller/date/CalendarController.dart';
|
||||
import 'package:vbvs_app/controller/sleep/sleep_report_controller.dart';
|
||||
import 'package:vbvs_app/pages/common/selectDialog.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/AIAdviceWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/BreathPauseWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/BreatheStandardWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/CompareSleepWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/DiseasePercentsWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartChangeWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartHealthWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartPointWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/HeartRateStandardWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SkinPercentWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SleepScoreWidget.dart';
|
||||
import 'package:vbvs_app/pages/sleep_report/component/SnoreViewWidget.dart';
|
||||
@@ -505,6 +510,14 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
|
||||
child: SleepScoreWidget(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 30.rpx, 0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: CompareSleepWidget(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 30.rpx, 0),
|
||||
@@ -513,6 +526,39 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
|
||||
child: HeartPointWidget(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 30.rpx, 0),
|
||||
child: Container(
|
||||
// color: stringToColor("#242835"),
|
||||
width: double.infinity,
|
||||
child: AIAdviceWidget(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 30.rpx, 0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: HeartRateStandardWidget(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 30.rpx, 0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: HeartChangeWidget(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 30.rpx, 0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: BreatheStandardWidget(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
30.rpx, 0.rpx, 30.rpx, 0),
|
||||
|
||||
113
lib/pages/user/about_us_page copy.dart
Normal file
113
lib/pages/user/about_us_page copy.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
// import 'package:ef/ef.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:vbvs_app/common/color/appConstants.dart';
|
||||
// import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
// import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
// import 'package:vbvs_app/component/tool/CustomCard.dart';
|
||||
// import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart';
|
||||
// import 'package:vbvs_app/controller/main_bottom/global_controller.dart';
|
||||
// import 'package:vbvs_app/controller/theme_controller/ThemeController.dart';
|
||||
// import 'package:vbvs_app/controller/user_info_controller.dart';
|
||||
// // import 'package:easydevice/easydevice.dart';
|
||||
|
||||
|
||||
// class AboutUsPage extends StatefulWidget {
|
||||
// const AboutUsPage({super.key});
|
||||
|
||||
// @override
|
||||
// State<AboutUsPage> createState() => _AboutUsPageState();
|
||||
// }
|
||||
|
||||
// class _AboutUsPageState extends State<AboutUsPage> {
|
||||
// GlobalController globalController = Get.find();
|
||||
// UserInfoController userInfoController = Get.find();
|
||||
// BlueteethBindController blueteethBindController = Get.find();
|
||||
// ThemeController themeController = Get.find();
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return LayoutBuilder(
|
||||
// builder: (context, bodySize) => GestureDetector(
|
||||
// onTap: () => FocusScope.of(context).unfocus(),
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// image: DecorationImage(
|
||||
// image: AssetImage('assets/img/bgNoImg.png'), // 本地图片
|
||||
// fit: BoxFit.fill, // 填满整个 Container
|
||||
// ),
|
||||
// ),
|
||||
// child: Scaffold(
|
||||
// backgroundColor: Colors.transparent, // 加上这一行
|
||||
// appBar: AppBar(
|
||||
// backgroundColor: themeController.currentColor.sc17,
|
||||
// automaticallyImplyLeading: false,
|
||||
// iconTheme: IconThemeData(
|
||||
// color: themeController.currentColor.sc3,
|
||||
// ),
|
||||
// titleSpacing: 0,
|
||||
// // leading: returnIconButtom,
|
||||
// title: Container(
|
||||
// width: double.infinity,
|
||||
// height: 180.rpx,
|
||||
// child: Stack(
|
||||
// alignment: Alignment.center,
|
||||
// children: [
|
||||
// /// 居中标题
|
||||
// Text(
|
||||
// '关于我们.标题'.tr,
|
||||
// style: FlutterFlowTheme.of(context).bodyMedium.override(
|
||||
// fontFamily: 'Readex Pro',
|
||||
// color: themeController.currentColor.sc3,
|
||||
// letterSpacing: 0,
|
||||
// fontSize: 30.rpx,
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// /// 左边返回按钮
|
||||
// Positioned(
|
||||
// left: 0,
|
||||
// child: returnIconButtom,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// actions: [],
|
||||
// centerTitle: false,
|
||||
// ),
|
||||
// body: SafeArea(
|
||||
// top: true,
|
||||
// child: Padding(
|
||||
// padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 0),
|
||||
// child: SingleChildScrollView(
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.max,
|
||||
// children: [
|
||||
|
||||
// SizedBox(
|
||||
// height: 30.rpx,
|
||||
// ),
|
||||
// Text(
|
||||
// "企业简介\n\n\n嘉兴太和信息技术有限公司成立于2013年,是一家以传感技术、室内定位技术和人工智能技术为基础的国家高新技术企业,AI非接触生命体征传感器、高精度室内外一体定位平台、AI视频分析系统、射频消融等技术成果,目前已经拥有30多类知识产权证书,多项专利技术处于行业领先水平。\n\n\n我司研发的“非接触式生命体征传感器”是一款基于BCG信号原理,通过检测人体心脏搏动引起的微小振动的传感器系统。传感器系统通过将人体微弱的心跳、呼吸信号转换未电信号,进行相关生命体征分析。该传感器可为用户提供高灵敏度和精确度检测结构,适用于需要非接触式、高分辨率的监测场景。该系统的硬件、软件及生产维护均由我司自主开发和管理,拥有完全自主知识产权,并已申请多项国家专利,可依据用户需求定制个性化方案。\n\n\n该产品置于床垫下方使用,全程完全无感。采集的体征数据通过睡眠健康管理平台实时显示用户的健康状态,并对每次的睡眠报告进行系统化归档管理,支持长期查询。一旦用户在使用过程中出现异常情况,系统可及时做出判断并反馈预警信息和建议。目前,心率监测的准确度可达97%以上,呼吸监测的准确度可达95%以上,其他生理指标的监测精度也显著优于同类产品。该产品主体材质均采用符合国家标准的环保材料,部分硅胶配件达到食品级安全标准。产品尺寸可根据需求进行定制,适用于单人床、双人床、婴儿床、椅子及枕头等多种场景。\n\n\n睡眠健康管理平台通过实时预警与远程管理,提升睡眠质量及慢病干预效率,助力养老院、月子中心、康复中心、智能寝具等行业降本增效,实现精准健康的科学管理。",
|
||||
// style: TextStyle(
|
||||
// fontSize: AppConstants().normal_text_fontSize,
|
||||
// color: themeController.currentColor.sc3),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
118
lib/pages/user/privacy_scheme_page.dart
Normal file
118
lib/pages/user/privacy_scheme_page.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/controller/setting/pdf/PrivacyPdfController.dart';
|
||||
|
||||
class PrivacySchemePage extends StatefulWidget {
|
||||
PrivacySchemePage({super.key});
|
||||
|
||||
@override
|
||||
State<PrivacySchemePage> createState() => _PrivacySchemePageState();
|
||||
}
|
||||
|
||||
class _PrivacySchemePageState extends State<PrivacySchemePage> {
|
||||
PrivacyPdfController pdfController = Get.find();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
String language = "zh_CN"; // 默认语言
|
||||
|
||||
if (languageController.selectLanguage?.value?.language_code != null) {
|
||||
language = languageController.selectLanguage!.value!.language_code!;
|
||||
}
|
||||
|
||||
pdfController.loadPdf(2,
|
||||
"https://vsbst-api.he-info.cn/vsbs_sotrage/privacy-scheme/$language.pdf",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, bodySize) => GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/img/bgNoImg.png'), // 本地图片
|
||||
fit: BoxFit.fill, // 填满整个 Container
|
||||
),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent, // 加上这一行
|
||||
appBar: AppBar(
|
||||
backgroundColor: themeController.currentColor.sc17,
|
||||
automaticallyImplyLeading: false,
|
||||
iconTheme: IconThemeData(
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
titleSpacing: 0,
|
||||
// leading: returnIconButtom,
|
||||
title: Container(
|
||||
width: double.infinity,
|
||||
height: 180.rpx,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
/// 居中标题
|
||||
Text(
|
||||
'隐私协议'.tr,
|
||||
style: FlutterFlowTheme.of(context).bodyMedium.override(
|
||||
fontFamily: 'Readex Pro',
|
||||
color: themeController.currentColor.sc3,
|
||||
letterSpacing: 0,
|
||||
fontSize: 30.rpx,
|
||||
),
|
||||
),
|
||||
|
||||
/// 左边返回按钮
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: returnIconButtom,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
actions: [],
|
||||
centerTitle: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30.rpx),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (pdfController.localPdfPath.value == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return PDFView(
|
||||
filePath: pdfController.localPdfPath.value!,
|
||||
autoSpacing: false,
|
||||
enableSwipe: true,
|
||||
swipeHorizontal: false,
|
||||
pageSnap: true,
|
||||
fitEachPage: true,
|
||||
defaultPage: 0,
|
||||
onRender: (pages) => print('PDF 渲染完成,共 $pages 页'),
|
||||
onError: (error) => print('PDF 加载错误: $error'),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/pages/user/user_scheme_page.dart
Normal file
119
lib/pages/user/user_scheme_page.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import 'package:vbvs_app/common/util/MyUtils.dart';
|
||||
import 'package:vbvs_app/controller/setting/pdf/UserPdfController.dart';
|
||||
|
||||
class UserSchemePage extends StatefulWidget {
|
||||
UserSchemePage({super.key});
|
||||
|
||||
@override
|
||||
State<UserSchemePage> createState() => _UserSchemePageState();
|
||||
}
|
||||
|
||||
class _UserSchemePageState extends State<UserSchemePage> {
|
||||
UserPdfController pdfController = Get.find();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
String language = "zh_CN"; // 默认语言
|
||||
|
||||
if (languageController.selectLanguage?.value?.language_code != null) {
|
||||
language = languageController.selectLanguage!.value!.language_code!;
|
||||
}
|
||||
|
||||
pdfController.loadPdf(
|
||||
1,
|
||||
"https://vsbst-api.he-info.cn/vsbs_sotrage/user-scheme/$language.pdf",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, bodySize) => GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/img/bgNoImg.png'), // 本地图片
|
||||
fit: BoxFit.fill, // 填满整个 Container
|
||||
),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent, // 加上这一行
|
||||
appBar: AppBar(
|
||||
backgroundColor: themeController.currentColor.sc17,
|
||||
automaticallyImplyLeading: false,
|
||||
iconTheme: IconThemeData(
|
||||
color: themeController.currentColor.sc3,
|
||||
),
|
||||
titleSpacing: 0,
|
||||
// leading: returnIconButtom,
|
||||
title: Container(
|
||||
width: double.infinity,
|
||||
height: 180.rpx,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
/// 居中标题
|
||||
Text(
|
||||
'用户协议'.tr,
|
||||
style: FlutterFlowTheme.of(context).bodyMedium.override(
|
||||
fontFamily: 'Readex Pro',
|
||||
color: themeController.currentColor.sc3,
|
||||
letterSpacing: 0,
|
||||
fontSize: 30.rpx,
|
||||
),
|
||||
),
|
||||
|
||||
/// 左边返回按钮
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: returnIconButtom,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
actions: [],
|
||||
centerTitle: false,
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
top: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30.rpx),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (pdfController.localPdfPath.value == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return PDFView(
|
||||
filePath: pdfController.localPdfPath.value!,
|
||||
autoSpacing: false,
|
||||
enableSwipe: true,
|
||||
swipeHorizontal: false,
|
||||
pageSnap: true,
|
||||
fitEachPage: true,
|
||||
defaultPage: 0,
|
||||
onRender: (pages) => print('PDF 渲染完成,共 $pages 页'),
|
||||
onError: (error) => print('PDF 加载错误: $error'),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ dependencies:
|
||||
flutter_pdfview: ^1.4.0+1
|
||||
weather: ^3.1.1
|
||||
geocoding: ^2.1.0
|
||||
# fl_chart: ^1.0.0
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user