更新
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,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(context),
|
_buildHeader(context),
|
||||||
|
SizedBox(
|
||||||
|
height: 33.rpx,
|
||||||
|
),
|
||||||
_buildSleepDateWidgets(),
|
_buildSleepDateWidgets(),
|
||||||
SizedBox(height: 20.rpx),
|
SizedBox(height: 20.rpx),
|
||||||
_buildSleepDataModuleWidgets(),
|
_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:flutterflow_ui/flutterflow_ui.dart';
|
||||||
import 'package:vbvs_app/common/color/appConstants.dart';
|
import 'package:vbvs_app/common/color/appConstants.dart';
|
||||||
import 'package:vbvs_app/common/util/FitTool.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/ClickableContainer.dart';
|
||||||
import 'package:vbvs_app/component/tool/CustomCard.dart';
|
import 'package:vbvs_app/component/tool/CustomCard.dart';
|
||||||
import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart';
|
import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart';
|
||||||
|
|||||||
@@ -304,15 +304,63 @@ class _HomePageState extends State<HomePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return Text(
|
return Row(
|
||||||
"嘉兴 " +
|
children: [
|
||||||
"${weatherModelController.model.weather_info ?? '未知数据'.tr}",
|
Text(
|
||||||
style: TextStyle(
|
"${weatherModelController.model.cityName??'未知数据'.tr}",
|
||||||
color: themeController
|
style: TextStyle(
|
||||||
.currentColor.sc4,
|
color: themeController
|
||||||
fontSize: AppConstants()
|
.currentColor.sc4,
|
||||||
.normal_text_fontSize,
|
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(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 40.rpx),
|
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(
|
Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.bottomLeft,
|
alignment: Alignment.bottomLeft,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
|
// 第一个容器,宽度占屏幕一半
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return ClickableContainer(
|
return Expanded(
|
||||||
padding: EdgeInsets.all(0),
|
child: ClickableContainer(
|
||||||
backgroundColor: Colors.transparent,
|
padding: EdgeInsets.all(0),
|
||||||
highlightColor:
|
backgroundColor: Colors.transparent,
|
||||||
themeController.currentColor.sc21,
|
highlightColor:
|
||||||
borderRadius: 8.rpx,
|
themeController.currentColor.sc21,
|
||||||
onTap: () => _onTabChanged(0),
|
borderRadius: 8.rpx,
|
||||||
child: Container(
|
onTap: () => _onTabChanged(0),
|
||||||
width: 160.rpx,
|
child: Container(
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
clipBehavior: Clip.none,
|
child: Stack(
|
||||||
children: [
|
alignment: Alignment.center,
|
||||||
Text(
|
clipBehavior: Clip.none,
|
||||||
'体征消息'.tr,
|
children: [
|
||||||
style: FlutterFlowTheme.of(context)
|
Text(
|
||||||
.bodyMedium
|
'体征消息'.tr,
|
||||||
.override(
|
style:
|
||||||
fontFamily: 'Inter',
|
FlutterFlowTheme.of(context)
|
||||||
fontSize: AppConstants()
|
.bodyMedium
|
||||||
.title_text_fontSize,
|
.override(
|
||||||
color:
|
fontFamily: 'Inter',
|
||||||
messageController
|
fontSize: AppConstants()
|
||||||
.model.type ==
|
.title_text_fontSize,
|
||||||
2
|
color: messageController
|
||||||
? themeController
|
.model.type ==
|
||||||
.currentColor.sc3
|
2
|
||||||
: themeController
|
? themeController
|
||||||
.currentColor.sc2,
|
.currentColor
|
||||||
),
|
.sc3
|
||||||
),
|
: themeController
|
||||||
Obx(() {
|
.currentColor
|
||||||
return messageController.model
|
.sc2,
|
||||||
.body_message_read ==
|
|
||||||
1
|
|
||||||
? Positioned(
|
|
||||||
top: -4,
|
|
||||||
right: -14,
|
|
||||||
child: Container(
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
decoration:
|
|
||||||
const BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
Obx(() {
|
||||||
: const SizedBox.shrink();
|
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(() {
|
Obx(() {
|
||||||
return ClickableContainer(
|
return Expanded(
|
||||||
padding: EdgeInsets.all(0),
|
child: ClickableContainer(
|
||||||
backgroundColor: Colors.transparent,
|
padding: EdgeInsets.all(0),
|
||||||
highlightColor:
|
backgroundColor: Colors.transparent,
|
||||||
themeController.currentColor.sc21,
|
highlightColor:
|
||||||
borderRadius: 8.rpx,
|
themeController.currentColor.sc21,
|
||||||
onTap: () => _onTabChanged(1),
|
borderRadius: 8.rpx,
|
||||||
child: Container(
|
onTap: () => _onTabChanged(1),
|
||||||
width: 160.rpx,
|
child: Container(
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
clipBehavior: Clip.none,
|
child: Stack(
|
||||||
children: [
|
alignment: Alignment.center,
|
||||||
Text(
|
clipBehavior: Clip.none,
|
||||||
'系统消息'.tr,
|
children: [
|
||||||
style: FlutterFlowTheme.of(context)
|
Text(
|
||||||
.bodyMedium
|
'系统消息'.tr,
|
||||||
.override(
|
style:
|
||||||
fontFamily: 'Inter',
|
FlutterFlowTheme.of(context)
|
||||||
fontSize: AppConstants()
|
.bodyMedium
|
||||||
.title_text_fontSize,
|
.override(
|
||||||
color:
|
fontFamily: 'Inter',
|
||||||
messageController
|
fontSize: AppConstants()
|
||||||
.model.type ==
|
.title_text_fontSize,
|
||||||
1
|
color: messageController
|
||||||
? themeController
|
.model.type ==
|
||||||
.currentColor.sc3
|
1
|
||||||
: themeController
|
? themeController
|
||||||
.currentColor.sc2,
|
.currentColor
|
||||||
),
|
.sc3
|
||||||
),
|
: themeController
|
||||||
Obx(() {
|
.currentColor
|
||||||
return messageController.model
|
.sc2,
|
||||||
.system_message_read ==
|
|
||||||
1
|
|
||||||
? Positioned(
|
|
||||||
top: -4,
|
|
||||||
right: -14,
|
|
||||||
child: Container(
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
decoration:
|
|
||||||
const BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
Obx(() {
|
||||||
: const SizedBox.shrink();
|
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(() {
|
Obx(() {
|
||||||
double lineWidth = 170.rpx;
|
double lineWidth =
|
||||||
|
MediaQuery.sizeOf(context).width * 0.5 -
|
||||||
|
20.rpx; // 每个容器占宽度的一半
|
||||||
return AnimatedPositioned(
|
return AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: messageController.model.type == 1
|
left: messageController.model.type == 1
|
||||||
? 0
|
? 0
|
||||||
: 170.rpx,
|
: MediaQuery.sizeOf(context).width * 0.5 +
|
||||||
|
10.rpx, // 动态设置左侧位置
|
||||||
child: Container(
|
child: Container(
|
||||||
width: lineWidth,
|
width: lineWidth,
|
||||||
height: 4.rpx,
|
height: 4.rpx,
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ class _ApplyRepairPageState extends State<ApplyRepairPage> {
|
|||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
letterSpacing: 0.0,
|
letterSpacing: 0.0,
|
||||||
fontSize: AppConstants().normal_text_fontSize,
|
fontSize: AppConstants().normal_text_fontSize,
|
||||||
|
color: themeController.currentColor.sc4,
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
@@ -477,6 +478,7 @@ class _ApplyRepairPageState extends State<ApplyRepairPage> {
|
|||||||
cursorColor: themeController.currentColor.sc3,
|
cursorColor: themeController.currentColor.sc3,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
),
|
),
|
||||||
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
].divide(SizedBox(width: 24.rpx)),
|
].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);
|
TextStyle(color: themeController.currentColor.sc3, fontSize: 26.rpx);
|
||||||
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||||
textPainter.text =
|
textPainter.text =
|
||||||
TextSpan(text: '${value.toStringAsFixed(0)}%', style: textStyle);
|
TextSpan(text: '${value.toStringAsFixed(0)}', style: textStyle);
|
||||||
textPainter.layout();
|
textPainter.layout();
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.clipRect(Rect.fromLTWH(
|
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 totalHours = maxTime.difference(minTime).inHours;
|
||||||
int startHour = minTime.hour;
|
int startHour = minTime.hour;
|
||||||
|
|
||||||
for (int i = 1; i < totalHours; i++) {
|
// for (int i = 1; i < totalHours; i++) {
|
||||||
double x = xStart + chartWidth * i / totalHours;
|
// double x = xStart + chartWidth * i / totalHours;
|
||||||
|
|
||||||
// 垂直虚线
|
// // 垂直虚线
|
||||||
drawDashedLine(
|
// drawDashedLine(
|
||||||
canvas,
|
// canvas,
|
||||||
Offset(x, 0),
|
// Offset(x, 0),
|
||||||
Offset(x, chartHeight),
|
// Offset(x, chartHeight),
|
||||||
axisPaint,
|
// axisPaint,
|
||||||
dashWidth: 4.rpx,
|
// dashWidth: 4.rpx,
|
||||||
dashSpace: 4.rpx,
|
// dashSpace: 4.rpx,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 5. 画左侧完整时分 (HH:mm),往内缩 labelInset
|
// 5. 画左侧完整时分 (HH:mm),往内缩 labelInset
|
||||||
String leftLabel = DateFormat('HH:mm').format(minTime);
|
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(
|
Positioned(
|
||||||
right: 60.rpx, // 放置在右侧
|
right: 60.rpx, // 放置在右侧
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
'assets/img/icon/add.svg',
|
'assets/img/icon/score_down.svg',
|
||||||
width: 14.rpx,
|
width: 14.rpx,
|
||||||
height: 22.rpx,
|
height: 22.rpx,
|
||||||
color: themeController.currentColor.sc9,
|
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:ef/ef.dart';
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:vbvs_app/common/color/appConstants.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/common/util/MyUtils.dart';
|
||||||
import 'package:vbvs_app/component/tool/ClickableContainer.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/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 {
|
class HeartPointWidget extends StatefulWidget {
|
||||||
HeartPointWidget({super.key});
|
HeartPointWidget({super.key});
|
||||||
@@ -33,6 +36,18 @@ class _HeartPointWidgetState extends State<HeartPointWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -91,24 +106,30 @@ class _HeartPointWidgetState extends State<HeartPointWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 83.rpx,
|
height: 31.rpx,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding:
|
||||||
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
|
EdgeInsetsDirectional.fromSTEB(30.rpx, 0.rpx, 30.rpx, 0.rpx),
|
||||||
child: StatusBarWithIndicator(
|
child: Container(
|
||||||
selectKey: 2,
|
width: MediaQuery.of(context).size.width * 0.7,
|
||||||
showLabel: [
|
height: MediaQuery.of(context).size.width * 0.7,
|
||||||
{"key": 1, "name": "正常", "color": Color(0xFF4CAF50)},
|
constraints: BoxConstraints(
|
||||||
{"key": 2, "name": "一般", "color": Color(0xFF8BC34A)},
|
minWidth: 430.rpx,
|
||||||
{"key": 3, "name": "注意", "color": Color(0xFFFFC107)},
|
minHeight: 430.rpx,
|
||||||
{"key": 4, "name": "警告", "color": Color(0xFFF44336)},
|
),
|
||||||
],
|
child: ScatterPlotChart(
|
||||||
|
points: data,
|
||||||
|
xMax: 1400, // x轴最大值
|
||||||
|
yMax: 1400, // y轴最大值
|
||||||
|
pointColor: stringToColor("#00C1AA"), // 点的颜色
|
||||||
|
divisions: 7, // 刻度分割数量
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
// SizedBox(
|
||||||
height: 56.rpx,
|
// 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,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"呼吸暂停监测".tr,
|
"打鼾监测".tr,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: themeController.currentColor.sc3,
|
color: themeController.currentColor.sc3,
|
||||||
fontSize: AppConstants().title_text_fontSize),
|
fontSize: AppConstants().title_text_fontSize),
|
||||||
@@ -77,7 +77,7 @@ class _SnoreViewWidgetWidgetState extends State<SnoreViewWidgetWidget> {
|
|||||||
context,
|
context,
|
||||||
Container(
|
Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
"呼吸暂停监测介绍。",
|
"打鼾监测介绍。",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 26.rpx,
|
fontSize: 26.rpx,
|
||||||
color: themeController.currentColor.sc3,
|
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/date/CalendarController.dart';
|
||||||
import 'package:vbvs_app/controller/sleep/sleep_report_controller.dart';
|
import 'package:vbvs_app/controller/sleep/sleep_report_controller.dart';
|
||||||
import 'package:vbvs_app/pages/common/selectDialog.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/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/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/HeartHealthWidget.dart';
|
||||||
import 'package:vbvs_app/pages/sleep_report/component/HeartPointWidget.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/SkinPercentWidget.dart';
|
||||||
import 'package:vbvs_app/pages/sleep_report/component/SleepScoreWidget.dart';
|
import 'package:vbvs_app/pages/sleep_report/component/SleepScoreWidget.dart';
|
||||||
import 'package:vbvs_app/pages/sleep_report/component/SnoreViewWidget.dart';
|
import 'package:vbvs_app/pages/sleep_report/component/SnoreViewWidget.dart';
|
||||||
@@ -505,6 +510,14 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
|
|||||||
child: SleepScoreWidget(),
|
child: SleepScoreWidget(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional.fromSTEB(
|
||||||
|
30.rpx, 0.rpx, 30.rpx, 0),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
child: CompareSleepWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsetsDirectional.fromSTEB(
|
padding: EdgeInsetsDirectional.fromSTEB(
|
||||||
30.rpx, 0.rpx, 30.rpx, 0),
|
30.rpx, 0.rpx, 30.rpx, 0),
|
||||||
@@ -513,6 +526,39 @@ class _NewSleepReportPageState extends State<NewSleepReportPage> {
|
|||||||
child: HeartPointWidget(),
|
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(
|
||||||
padding: EdgeInsetsDirectional.fromSTEB(
|
padding: EdgeInsetsDirectional.fromSTEB(
|
||||||
30.rpx, 0.rpx, 30.rpx, 0),
|
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
|
flutter_pdfview: ^1.4.0+1
|
||||||
weather: ^3.1.1
|
weather: ^3.1.1
|
||||||
geocoding: ^2.1.0
|
geocoding: ^2.1.0
|
||||||
|
# fl_chart: ^1.0.0
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user