更新城市选择

This commit is contained in:
wyf
2025-11-21 10:34:18 +08:00
parent b2a621c2d3
commit 991bf97fd1
27 changed files with 79242 additions and 467 deletions

View File

@@ -8,8 +8,11 @@ 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/color/app_uri_status.dart';
import 'package:vbvs_app/common/pojo/city.dart';
import 'package:vbvs_app/common/util/EventBus.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/common/util/eventType.dart';
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
import 'package:vbvs_app/component/tool/CustomCard.dart';
import 'package:vbvs_app/component/tool/NewTopSlideNotification.dart';
@@ -50,6 +53,16 @@ class _DeviceDataComponentWidgetState extends State<DeviceDataComponentWidget> {
@override
void initState() {
super.initState();
late StreamSubscription<SwitchLanguageEvent> subscription;
// 监听切换语言
subscription = EventBus().on<SwitchLanguageEvent>().listen((event) async {
final CityModelController cityController =
Get.find<CityModelController>();
ef.log("切换语言事件通知:${event.language}");
cityController.cityList = [];
await initializeCityData();
});
}
void _showPopup() {
@@ -454,7 +467,8 @@ class _DeviceDataComponentWidgetState extends State<DeviceDataComponentWidget> {
color: Colors.black,
),
cursorColor: Colors.white,
initialValue: widget.device['person']['name'] == null ||
initialValue: widget.device['person'] == null ||
widget.device['person']['name'] == null ||
widget.device['person']['name'] == ""
? "体征监测设备".tr
: widget.device['person']['name'],
@@ -1093,6 +1107,22 @@ class _DeviceDataComponentWidgetState extends State<DeviceDataComponentWidget> {
personController.dateTime =
MyUtils.formatBirthdayTime(
widget.device['person']['birthday']);
if (widget.device['person']['city_id'] != null) {
// 根据city_id查找完整的城市数据
final int cityId =
widget.device['person']['city_id'];
await initializeCityData();
final CityModel? completeCityData =
_findCityDataById(cityId);
if (completeCityData != null) {
personController.cityModel = completeCityData;
} else {
personController.cityModel = null;
}
} else {
personController.cityModel = null;
}
} else {
personController.update_person_mac.value =
widget.device['mac'];
@@ -1104,6 +1134,7 @@ class _DeviceDataComponentWidgetState extends State<DeviceDataComponentWidget> {
personController.height.value = "";
personController.weight.value = "";
personController.diseaseList.value = [];
personController.cityModel = null;
}
await Get.toNamed("/updatePersonPage",
arguments: widget.device['bind_type']);
@@ -1497,4 +1528,96 @@ class _DeviceDataComponentWidgetState extends State<DeviceDataComponentWidget> {
return 4;
}
}
// 根据ID查找完整的城市数据
CityModel? _findCityDataById(int cityId) {
try {
if (cityId == null) {
return null;
}
// 从加载的城市数据中查找
final cityData = Get.find<CityModelController>().cityList;
// 遍历三级数据结构查找匹配的ID
for (var country in cityData) {
// 检查国家节点
if (country.id == cityId) {
return CityModel(
id: country.id,
value: country.value,
label: country.label,
country: country.value ?? country.country,
province: null,
city: null,
UTC: country.UTC,
children: country.children,
);
}
// 检查省份节点
for (var province in country.children ?? []) {
if (province.id == cityId) {
return CityModel(
id: province.id,
value: province.value,
label: province.label,
country: country.value ?? country.country,
province: province.value ?? province.province,
city: null,
UTC: province.UTC,
children: province.children,
);
}
// 检查城市节点
for (var city in province.children ?? []) {
if (city.id == cityId) {
return CityModel(
id: city.id,
value: city.value,
label: city.label,
country: country.value ?? country.country,
province: province.value ?? province.province,
city: city.value ?? city.city,
UTC: city.UTC,
children: city.children,
);
}
}
}
}
return null;
} catch (e) {
ef.log("根据ID查找城市数据失败$e");
return null;
}
}
Future initializeCityData() async {
final CityModelController cityController = Get.find<CityModelController>();
// 确保城市数据已加载
if (!cityController.isDataLoaded) {
await cityController.loadAndSetCityData();
}
// 如果device中有city_id查找并设置城市数据
if (widget.device['person']['city_id'] != null) {
final String cityId = widget.device['person']['city_id'].toString();
ef.log("开始查找city_id: $cityId");
final CityModel? completeCityData = cityController.findCityById(cityId);
if (completeCityData != null) {
personController.cityModel = completeCityData;
ef.log("成功设置城市数据: ${completeCityData.displayName}");
} else {
personController.cityModel = CityModel(id: int.tryParse(cityId));
ef.log("未找到对应城市数据创建默认CityModel");
}
personController.updateAll();
}
}
}

View File

@@ -283,6 +283,7 @@ import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
import 'package:vbvs_app/controller/main_bottom/global_controller.dart';
import 'package:vbvs_app/controller/main_bottom/main_page_controller.dart';
import 'package:vbvs_app/controller/message/message_controller.dart';
import 'package:vbvs_app/controller/setting/language/language_controller.dart';
import 'package:vbvs_app/controller/theme_controller/ThemeController.dart';
import 'package:vbvs_app/controller/user_info_controller.dart';
import 'package:vbvs_app/enum/APPPackageType.dart';
@@ -302,6 +303,7 @@ class MainPageBottomChange extends GetView<MainPageController> {
late final List<Widget> arr;
late final List<BottomNavigationBarItem> bottomItems;
DateTime? _lastBackPressedTime;
LanguageController languageController = Get.find();
MainPageBottomChange({super.key}) {
// ✅ 根据是否测试账号动态生成页面
@@ -408,122 +410,133 @@ class MainPageBottomChange extends GetView<MainPageController> {
});
}
return PopScope(
canPop: false,
onPopInvokedWithResult: (disposition, result) async {
UserInfoController userInfoController = Get.find();
if (userInfoController.model.isProgrammaticPop) {
userInfoController.model.isProgrammaticPop = false;
return;
}
return Obx(() {
final currentLanguage =
languageController.selectLanguage.value; // 监听此变量变化
return PopScope(
canPop: false,
onPopInvokedWithResult: (disposition, result) async {
UserInfoController userInfoController = Get.find();
if (userInfoController.model.isProgrammaticPop) {
userInfoController.model.isProgrammaticPop = false;
return;
}
if (Platform.isAndroid) {
Get.back();
}
},
child: Obx(() {
int currentIndex = controller.model.currentIndex;
// if (Platform.isAndroid) {
// Get.back();
// }
if (Platform.isAndroid) {
var flag = await _handleBackPressed(context);
if (flag) {
SystemNavigator.pop();
}
}
},
child: Obx(() {
int currentIndex = controller.model.currentIndex;
// ✅ 防止 index 超出范围(例如测试账号少一个 tab
if (currentIndex >= arr.length) {
currentIndex = 0;
controller.model.currentIndex = 0;
}
// ✅ 防止 index 超出范围(例如测试账号少一个 tab
if (currentIndex >= arr.length) {
currentIndex = 0;
controller.model.currentIndex = 0;
}
if (globalController.model.hideBottomNavigationBar == true) {
return Scaffold(
body: IndexedStack(
index: currentIndex,
children:
arr.map((page) => SizedBox.expand(child: page)).toList(),
),
);
} else {
return Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/img/bgImage.png'),
fit: BoxFit.fill,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
if (globalController.model.hideBottomNavigationBar == true) {
return Scaffold(
body: IndexedStack(
index: currentIndex,
children:
arr.map((page) => SizedBox.expand(child: page)).toList(),
),
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
);
} else {
return Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/img/bgImage.png'),
fit: BoxFit.fill,
),
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color:
themeController.currentColor.sc4.withOpacity(0.5),
width: AppConstants().border_width,
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: IndexedStack(
index: currentIndex,
children:
arr.map((page) => SizedBox.expand(child: page)).toList(),
),
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color:
themeController.currentColor.sc4.withOpacity(0.5),
width: AppConstants().border_width,
),
),
),
),
child: BottomNavigationBar(
unselectedItemColor: themeController.currentColor.sc4,
selectedItemColor: themeController.currentColor.sc1,
backgroundColor: themeController.currentColor.sc5,
selectedFontSize: 26.rpx,
unselectedFontSize: 26.rpx,
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex,
onTap: (index) {
Future.delayed(const Duration(milliseconds: 100), () {
UserInfoController userInfoController = Get.find();
bool isLoggedIn = userInfoController.model.login ==
LoginStatus.LOGIN.code;
child: BottomNavigationBar(
unselectedItemColor: themeController.currentColor.sc4,
selectedItemColor: themeController.currentColor.sc1,
backgroundColor: themeController.currentColor.sc5,
selectedFontSize: 26.rpx,
unselectedFontSize: 26.rpx,
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex,
onTap: (index) {
Future.delayed(const Duration(milliseconds: 100), () {
UserInfoController userInfoController = Get.find();
bool isLoggedIn = userInfoController.model.login ==
LoginStatus.LOGIN.code;
// ✅ 仅非测试账号检查登录状态
if (!AppConstants.is_test_account &&
(index == 2) &&
!isLoggedIn) {
TopSlideNotification.show(
context,
text: "必须登录提示".tr,
textColor: themeController.currentColor.sc9,
);
Future.delayed(const Duration(milliseconds: 50), () {
if (Get.currentRoute == '/ePage' ||
Get.currentRoute == '/messagePage') {
Get.back();
}
Future.delayed(const Duration(milliseconds: 100),
// ✅ 仅非测试账号检查登录状态
if (!AppConstants.is_test_account &&
(index == 2) &&
!isLoggedIn) {
TopSlideNotification.show(
context,
text: "必须登录提示".tr,
textColor: themeController.currentColor.sc9,
);
Future.delayed(const Duration(milliseconds: 50),
() {
Get.toNamed("/otherLoginPage");
if (Get.currentRoute == '/ePage' ||
Get.currentRoute == '/messagePage') {
Get.back();
}
Future.delayed(const Duration(milliseconds: 100),
() {
Get.toNamed("/otherLoginPage");
});
});
});
return;
}
return;
}
if (controller.model.currentIndex != index) {
globalController.model.hideBottomNavigationBar =
false;
globalController.updateAll();
}
if (controller.model.currentIndex != index) {
globalController.model.hideBottomNavigationBar =
false;
globalController.updateAll();
}
controller.model.currentIndex = index;
controller.updateAll();
});
},
items: bottomItems,
controller.model.currentIndex = index;
controller.updateAll();
});
},
items: bottomItems,
),
),
),
),
),
);
}
}),
);
);
}
}),
);
});
}
Future<bool> _handleBackPressed(BuildContext context) async {

View File

@@ -6,6 +6,7 @@ import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/ServiceConstant.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/color/app_uri_status.dart';
import 'package:vbvs_app/common/pojo/city.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/common/util/requestWithLog.dart';
@@ -18,6 +19,7 @@ import 'package:vbvs_app/controller/person/person_controller.dart';
import 'package:vbvs_app/controller/theme_controller/ThemeController.dart';
import 'package:vbvs_app/controller/user_info_controller.dart';
import 'package:vbvs_app/model/api_response.dart';
import 'package:vbvs_app/pages/person/select_city.dart';
import 'package:vbvs_app/pages/person/select_time.dart';
class PersonPage extends StatefulWidget {
@@ -34,6 +36,9 @@ class _EPageState extends State<PersonPage> {
PersonController personController = Get.find();
ThemeController themeController = Get.find();
final CityModelController cityController = Get.find<CityModelController>();
late Future<List<CityModel>> cityDataFuture;
@override
void initState() {
super.initState();
@@ -55,6 +60,10 @@ class _EPageState extends State<PersonPage> {
personController.weight.value = "";
personController.height.value = "";
personController.dateTime = null;
cityDataFuture = cityController.loadAndSetCityData().then((success) {
return cityController.cityList;
});
}
@override
@@ -533,6 +542,65 @@ class _EPageState extends State<PersonPage> {
),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
70.rpx, 50.rpx, 70.rpx, 0),
child: Container(
height: 100.rpx,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.rpx),
border: Border.all(
color: themeController.currentColor.sc4
.withOpacity(0.5),
width: AppConstants().border_width,
),
),
child: InkWell(
onTap: () {
FocusScope.of(context)
.requestFocus(FocusNode());
Future.delayed(Duration(milliseconds: 250),
() {
// 使用当前选中的城市数据,如果没有则创建默认
CityModel currentCity =
personController.cityModel ??
CityModel();
showCitySelectionDialog(
context,
selectedCity: currentCity,
onCityChanged: (CityModel newCity) {
// 处理城市选择变化
print(
'Selected city: ${newCity.toJson()}');
personController.cityModel = newCity;
personController.updateAll();
},
title: "选择城市".tr,
cityDataFuture:
cityDataFuture, // 传入预加载的数据
);
});
},
child: Center(
child: Text(
_getDetailedCityDisplayText(
personController.cityModel),
textAlign: TextAlign.right,
style: TextStyle(
fontFamily: 'Readex Pro',
color: personController.cityModel !=
null
? themeController.currentColor.sc3
: themeController.currentColor.sc4,
fontSize:
AppConstants().normal_text_fontSize,
letterSpacing: 0,
),
),
),
),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 117.rpx, 0, 0),
@@ -696,6 +764,29 @@ class _EPageState extends State<PersonPage> {
),
);
}
String _getDetailedCityDisplayText(CityModel? cityModel) {
if (cityModel == null) {
return '选择城市'.tr;
}
// 根据数据层级显示不同的格式
if (cityModel.city != null && cityModel.city!.isNotEmpty) {
// 三级数据:国家-省份-城市
return '${cityModel.country ?? ''}-${cityModel.province ?? ''}-${cityModel.city ?? cityModel.value ?? ''}';
} else if (cityModel.province != null && cityModel.province!.isNotEmpty) {
// 二级数据:国家-省份
return '${cityModel.country ?? ''}-${cityModel.province ?? cityModel.value ?? ''}';
} else if (cityModel.country != null && cityModel.country!.isNotEmpty) {
// 一级数据:国家
return cityModel.country!;
} else if (cityModel.value != null && cityModel.value!.isNotEmpty) {
// 只有 value 字段
return cityModel.value!;
} else {
return '选择城市'.tr;
}
}
}
void updateDeviceBindStatus(String mac) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,17 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:ef/ef.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/pojo/city.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/controller/theme_controller/ThemeController.dart';
import 'package:vbvs_app/language/AppLanguage.dart';
import 'package:vbvs_app/pages/common/selectDialog.dart';
// Future showDateSelectionDialog(BuildContext context,
@@ -814,3 +820,4 @@ Future<void> showWeightPickerDialog(
},
);
}

View File

@@ -6,6 +6,7 @@ import 'package:flutterflow_ui/flutterflow_ui.dart';
import 'package:vbvs_app/common/color/ServiceConstant.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/color/app_uri_status.dart';
import 'package:vbvs_app/common/pojo/city.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'package:vbvs_app/common/util/requestWithLog.dart';
@@ -20,6 +21,7 @@ import 'package:vbvs_app/controller/theme_controller/ThemeController.dart';
import 'package:vbvs_app/controller/user_info_controller.dart';
import 'package:vbvs_app/enum/BindType.dart';
import 'package:vbvs_app/model/api_response.dart';
import 'package:vbvs_app/pages/person/select_city.dart';
import 'package:vbvs_app/pages/person/select_time.dart';
class UpdatePersonPage extends StatefulWidget {
@@ -38,6 +40,9 @@ class _UpdatePageState extends State<UpdatePersonPage> {
BodyDeviceController bodyDeviceController = Get.find();
ThemeController themeController = Get.find();
final CityModelController cityController = Get.find<CityModelController>();
late Future<List<CityModel>> cityDataFuture;
@override
void initState() {
super.initState();
@@ -51,6 +56,9 @@ class _UpdatePageState extends State<UpdatePersonPage> {
);
}
});
cityDataFuture = cityController.loadAndSetCityData().then((success) {
return cityController.cityList;
});
}
@override
@@ -66,6 +74,7 @@ class _UpdatePageState extends State<UpdatePersonPage> {
),
),
child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent, // 加上这一行
appBar: AppBar(
backgroundColor: themeController.currentColor.sc17,
@@ -575,6 +584,72 @@ class _UpdatePageState extends State<UpdatePersonPage> {
),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
70.rpx, 50.rpx, 70.rpx, 0),
child: Container(
height: 100.rpx,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.rpx),
border: Border.all(
color: themeController.currentColor.sc4
.withOpacity(0.5),
width: AppConstants().border_width,
),
),
child: InkWell(
onTap: () {
if (widget.status == BindType.share.code) {
TopSlideNotification.show(context,
text: "被分享用户只能修改用户名称",
textColor:
themeController.currentColor.sc9);
return;
}
FocusScope.of(context)
.requestFocus(FocusNode());
Future.delayed(Duration(milliseconds: 250),
() {
// 使用当前选中的城市数据,如果没有则创建默认
CityModel currentCity =
personController.cityModel ??
CityModel();
showCitySelectionDialog(
context,
selectedCity: currentCity,
onCityChanged: (CityModel newCity) {
// 处理城市选择变化
print(
'Selected city: ${newCity.toJson()}');
personController.cityModel = newCity;
personController.updateAll();
},
title: "选择城市".tr,
cityDataFuture:
cityDataFuture, // 传入预加载的数据
);
});
},
child: Center(
child: Text(
_getDetailedCityDisplayText(
personController.cityModel),
textAlign: TextAlign.right,
style: TextStyle(
fontFamily: 'Readex Pro',
color: personController.cityModel !=
null
? themeController.currentColor.sc3
: themeController.currentColor.sc4,
fontSize:
AppConstants().normal_text_fontSize,
letterSpacing: 0,
),
),
),
),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
0, 117.rpx, 0, 0),
@@ -782,4 +857,29 @@ class _UpdatePageState extends State<UpdatePersonPage> {
onFailure: (res) {},
);
}
// 获取城市显示文本
// 更详细的显示逻辑(可选)
String _getDetailedCityDisplayText(CityModel? cityModel) {
if (cityModel == null) {
return '选择城市'.tr;
}
// 根据数据层级显示不同的格式
if (cityModel.city != null && cityModel.city!.isNotEmpty) {
// 三级数据:国家-省份-城市
return '${cityModel.country ?? ''}-${cityModel.province ?? ''}-${cityModel.city ?? cityModel.value ?? ''}';
} else if (cityModel.province != null && cityModel.province!.isNotEmpty) {
// 二级数据:国家-省份
return '${cityModel.country ?? ''}-${cityModel.province ?? cityModel.value ?? ''}';
} else if (cityModel.country != null && cityModel.country!.isNotEmpty) {
// 一级数据:国家
return cityModel.country!;
} else if (cityModel.value != null && cityModel.value!.isNotEmpty) {
// 只有 value 字段
return cityModel.value!;
} else {
return '选择城市'.tr;
}
}
}

View File

@@ -152,13 +152,14 @@ class _LanguageSettingState extends State<LanguageSetting> {
languageController
.updateAll();
}
await AppLanguage()
.loadLanguage(language
.language_code); // 加载语言
EventBus().emit(
SwitchLanguageEvent(
language
.language_code));
await AppLanguage()
.loadLanguage(language
.language_code); // 加载语言
languageController
.updateAll();
await ef.kvdb.write(

View File

@@ -1,366 +1,11 @@
import 'dart:math';
import 'dart:ui' as ui;
import 'package:ef/ef.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'dart:ui' as ui;
import 'dart:math';
//根据数据自定义
// class LineChartByRange extends StatefulWidget {
// final List<Map<String, dynamic>> showLabel;
// final int startTime;
// final int endTime;
// final int? threshold;
// const LineChartByRange({
// Key? key,
// required this.showLabel,
// required this.startTime,
// required this.endTime,
// this.threshold, // 新增
// }) : super(key: key);
// @override
// State<LineChartByRange> createState() => _LineChartByRangeState();
// }
// class _LineChartByRangeState extends State<LineChartByRange> {
// Offset? selectedOffset;
// Map<String, dynamic>? selectedData;
// @override
// Widget build(BuildContext context) {
// if (widget.showLabel.isEmpty) return const SizedBox();
// int maxTimes = widget.showLabel
// .map((e) => e['times'] ?? 0)
// .reduce((a, b) => a > b ? a : b);
// int yMax = (maxTimes / 10).ceil() * 10;
// if (yMax == 0) yMax = 10;
// DateTime minTime = DateTime.fromMillisecondsSinceEpoch(widget.startTime);
// DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime);
// return GestureDetector(
// onTapDown: (details) {
// RenderBox box = context.findRenderObject() as RenderBox;
// final localPosition = box.globalToLocal(details.globalPosition);
// // 查找是否点击到某个点
// for (var item in widget.showLabel) {
// int start = item['startTime'];
// int end = item['endTime'];
// int times = item['times'];
// double chartWidth = box.size.width - 40.rpx; // 与 painter 内一致处理
// double chartHeight = box.size.height - 30.rpx;
// double xStart = 20.rpx + 12.rpx;
// int totalDuration =
// maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
// double startX = xStart +
// chartWidth *
// (start - minTime.millisecondsSinceEpoch) /
// totalDuration;
// double y = chartHeight * (1 - times / yMax);
// // 判断点击范围圆点半径±6.rpx范围
// if ((localPosition - Offset(startX, y)).distance < 10.rpx) {
// setState(() {
// selectedOffset = Offset(startX, y);
// selectedData = item;
// });
// return;
// }
// double endX = xStart +
// chartWidth *
// (end - minTime.millisecondsSinceEpoch) /
// totalDuration;
// if ((localPosition - Offset(endX, y)).distance < 10.rpx) {
// setState(() {
// selectedOffset = Offset(endX, y);
// selectedData = item;
// });
// return;
// }
// }
// // 没点到,清除选中
// setState(() {
// selectedOffset = null;
// selectedData = null;
// });
// },
// child: Stack(
// children: [
// SizedBox(
// height: 500.rpx,
// child: CustomPaint(
// size: Size(double.infinity, 500.rpx),
// painter: _LineChartByRangePainter(
// data: widget.showLabel,
// yMax: yMax,
// minTime: minTime,
// maxTime: maxTime,
// threshold: widget.threshold, // 新增
// ),
// ),
// ),
// if (selectedOffset != null && selectedData != null)
// Positioned(
// left: selectedOffset!.dx - 60.rpx,
// top: selectedOffset!.dy - 50.rpx,
// child: Container(
// padding:
// EdgeInsets.symmetric(horizontal: 12.rpx, vertical: 8.rpx),
// decoration: BoxDecoration(
// color: Colors.black.withOpacity(0.3),
// borderRadius: BorderRadius.circular(10.rpx),
// ),
// child: Text(
// '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - '
// '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n'
// '次数: ${selectedData!['times']}',
// style: TextStyle(
// fontSize: 18.rpx,
// color: Colors.white,
// ),
// ),
// ),
// ),
// ],
// ),
// );
// }
// }
// class _LineChartByRangePainter extends CustomPainter {
// final List<Map<String, dynamic>> data;
// final int yMax;
// final DateTime minTime;
// final DateTime maxTime;
// final int? threshold;
// _LineChartByRangePainter({
// required this.data,
// required this.yMax,
// required this.minTime,
// required this.maxTime,
// this.threshold,
// });
// @override
// void paint(Canvas canvas, Size size) {
// double padding = 20.rpx;
// double labelInset = 12.rpx;
// final double xStart = padding + labelInset;
// final double xEnd = size.width - padding - labelInset;
// final double chartWidth = xEnd - xStart;
// double chartHeight = size.height - 30.rpx;
// int totalDuration =
// maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
// if (totalDuration <= 0) return;
// Paint linePaint = Paint()
// ..style = PaintingStyle.stroke
// ..strokeWidth = 3.rpx
// ..color = stringToColor("#00C1AA")
// ..strokeCap = StrokeCap.round;
// Paint axisPaint = Paint()
// ..color = Colors.grey.withOpacity(0.4)
// ..strokeWidth = 1.rpx;
// Paint thresholdPaint = Paint()
// ..color = themeController.currentColor.sc9
// ..strokeWidth = 1.rpx;
// // 1. 阈值虚线(红色)
// if (threshold != null && threshold! >= 0 && threshold! <= yMax) {
// double yThreshold = chartHeight * (1 - threshold! / yMax);
// drawDashedLine(
// canvas,
// Offset(xStart, yThreshold),
// Offset(xEnd, yThreshold),
// thresholdPaint,
// dashWidth: 8.rpx,
// dashSpace: 6.rpx,
// );
// }
// // 2. 绘制数据线段和圆点
// for (var item in data) {
// int start = item['startTime'];
// int end = item['endTime'];
// int times = item['times'];
// double startX = xStart +
// chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
// double endX = xStart +
// chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
// double y = chartHeight * (1 - times / yMax);
// // 设置颜色(根据 threshold 判断)
// Color pointColor;
// if (threshold != null && times >= threshold!) {
// pointColor = themeController.currentColor.sc9;
// } else {
// pointColor = stringToColor("#00C1AA");
// }
// Paint dynamicLinePaint = Paint()
// ..style = PaintingStyle.stroke
// ..strokeWidth = 3.rpx
// ..color = pointColor
// ..strokeCap = StrokeCap.round;
// Paint dynamicCirclePaint = Paint()
// ..style = PaintingStyle.fill
// ..color = pointColor;
// // 画线段
// canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint);
// // 画起点和终点圆点
// canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint);
// canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint);
// }
// // 3. Y轴辅助线和文字
// for (int i = 0; i <= 6; i++) {
// double y = chartHeight * i / 6;
// if (i == 6) {
// canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint);
// } else {
// drawDashedLine(
// canvas,
// Offset(xStart, y),
// Offset(xEnd, y),
// axisPaint,
// dashWidth: 8.rpx,
// dashSpace: 6.rpx,
// );
// }
// TextPainter tp = TextPainter(
// text: TextSpan(
// text: '${yMax - (yMax * i / 6).round()}',
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// tp.layout();
// tp.paint(canvas, Offset(0, y - tp.height / 2));
// }
// // 4. X轴主线
// canvas.drawLine(
// Offset(xStart, chartHeight),
// Offset(xEnd, chartHeight),
// axisPaint,
// );
// // 5. X轴时间文字左右两侧
// String leftLabel = DateFormat('HH:mm').format(minTime);
// TextPainter leftTp = TextPainter(
// text: TextSpan(
// text: leftLabel,
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// leftTp.layout();
// leftTp.paint(canvas,
// Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx));
// String rightLabel = DateFormat('HH:mm').format(maxTime);
// TextPainter rightTp = TextPainter(
// text: TextSpan(
// text: rightLabel,
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// rightTp.layout();
// rightTp.paint(
// canvas,
// Offset(size.width - padding - labelInset - rightTp.width / 2,
// chartHeight + 8.rpx));
// // 6. 中间小时刻度
// int totalHours = maxTime.difference(minTime).inHours + 1;
// int startHour = minTime.hour;
// for (int i = 1; i < totalHours; i++) {
// double x = xStart + chartWidth * i / totalHours;
// int hourLabelNum = (startHour + i) % 24;
// String hourLabel = '$hourLabelNum';
// TextPainter tp = TextPainter(
// text: TextSpan(
// text: hourLabel,
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// tp.layout();
// tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// void drawDashedLine(
// Canvas canvas,
// Offset start,
// Offset end,
// Paint paint, {
// required double dashWidth,
// required double dashSpace,
// }) {
// final dx = end.dx - start.dx;
// final dy = end.dy - start.dy;
// final distance = sqrt(dx * dx + dy * dy);
// final direction = Offset(dx / distance, dy / distance);
// double drawn = 0;
// while (drawn < distance) {
// final from = start + direction * drawn;
// final to = start + direction * (drawn + dashWidth).clamp(0, distance);
// canvas.drawLine(from, to, paint);
// drawn += dashWidth + dashSpace;
// }
// }
// }
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'dart:ui' as ui;
import 'dart:math';
class LineChartByRange extends StatefulWidget {
final List<Map<String, dynamic>> showLabel;
@@ -480,8 +125,10 @@ class _LineChartByRangeState extends State<LineChartByRange> {
),
child: Text(
'${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - '
'${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n'
"时长".tr+ ' ${selectedData!['times']}',
'${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n'
"时长"
.tr +
' ${selectedData!['times']}',
style: TextStyle(
fontSize: 18.rpx,
color: Colors.white,
@@ -554,9 +201,9 @@ class _LineChartByRangePainter extends CustomPainter {
int end = item['endTime'];
int times = item['times'];
double startX = xStart +
double startX = xStart * 2 +
chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
double endX = xStart +
double endX = xStart * 2 +
chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
double y = chartHeight * (1 - times / maxY);

View File

@@ -42,7 +42,7 @@ class _BreathPauseWidgetState extends State<BreathPauseWidget> {
return Container();
}
List data = widget.sleepReport['asp'];
List data = widget.sleepReport['asp'];
var showLabel = convertAspData(data);
var threshold = 30;
var startTime = widget.sleepReport['startTime'];

View File

@@ -61,6 +61,13 @@ class _SnoreViewWidgetWidgetState extends State<BreathePauseNewWidget> {
List<Map<String, dynamic>> data =
(widget.sleepReport['asp'] as List).cast<Map<String, dynamic>>();
// data = [
// {"st": 1763494195669, "value": 11},
// {"st": 1763494278485, "value": 18},
// {"st": 1763494293453, "value": 18},
// {"st": 1763494352321, "value": 14},
// {"st": 1763494606757, "value": 12}
// ];
List<Map<String, dynamic>> showLabel = convertToShowLabel(data);
double maxTimes = 70;
try {
@@ -217,7 +224,15 @@ class _SnoreViewWidgetWidgetState extends State<BreathePauseNewWidget> {
final value = item['value'];
if (value == currentValue) {
// endTime = st;
result.add({
"startTime": startTime,
"endTime": endTime,
"times": currentValue,
});
startTime = st;
endTime = st;
currentValue = value;
} else {
result.add({
"startTime": startTime,