修复日报中图标时间轴错乱。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
903
lib/pages/device/deviceCopy.dart
Normal file
903
lib/pages/device/deviceCopy.dart
Normal file
@@ -0,0 +1,903 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
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/color/app_uri_status.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/NullDataComponentWidget.dart';
|
||||||
|
import 'package:vbvs_app/component/tool/ClickableContainer.dart';
|
||||||
|
import 'package:vbvs_app/component/tool/CustomCard.dart';
|
||||||
|
import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
|
||||||
|
import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart';
|
||||||
|
import 'package:vbvs_app/controller/device/body_device_controller.dart';
|
||||||
|
import 'package:vbvs_app/controller/home/home_controller.dart';
|
||||||
|
import 'package:vbvs_app/controller/theme_controller/ThemeController.dart';
|
||||||
|
import 'package:vbvs_app/pages/device/component/DeviceDataComponentWidget.dart';
|
||||||
|
|
||||||
|
// 滚动通知事件类
|
||||||
|
class ScrollNotificationEvent {}
|
||||||
|
|
||||||
|
class BodyDeviceWidgetCopy extends StatefulWidget {
|
||||||
|
var type;
|
||||||
|
BodyDeviceWidgetCopy({super.key, required this.type});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BodyDeviceWidgetCopy> createState() => _BodyDevicePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BodyDevicePageState extends State<BodyDeviceWidgetCopy>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
final ThemeController themeController = Get.find();
|
||||||
|
final BodyDeviceController bodyDeviceController = Get.find();
|
||||||
|
HomeController homeController = Get.find();
|
||||||
|
final GlobalKey addIconKey = GlobalKey();
|
||||||
|
final ScrollController _myDeviceScrollController = ScrollController();
|
||||||
|
final ScrollController _cloudDeviceScrollController = ScrollController();
|
||||||
|
OverlayEntry? _popupEntry;
|
||||||
|
Timer? _timer;
|
||||||
|
late PageController _pageController;
|
||||||
|
|
||||||
|
// 飘动文字的变量(简化版)
|
||||||
|
late AnimationController _floatController;
|
||||||
|
double _floatX = 0.0; // 水平位置(-1到1之间,表示屏幕宽度的百分比)
|
||||||
|
double _floatY = 0.0; // 垂直位置(-1到1之间,表示屏幕高度的百分比)
|
||||||
|
double _speedX = 0.005; // X方向速度(向右)
|
||||||
|
double _speedY = -0.005; // Y方向速度(负值表示向上,45度方向)
|
||||||
|
|
||||||
|
void _showPopup() {
|
||||||
|
final renderBox =
|
||||||
|
addIconKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox == null) return;
|
||||||
|
final position = renderBox.localToGlobal(Offset.zero);
|
||||||
|
final size = renderBox.size;
|
||||||
|
double popupWidth = 190.rpx;
|
||||||
|
|
||||||
|
_popupEntry?.remove();
|
||||||
|
|
||||||
|
_popupEntry = OverlayEntry(
|
||||||
|
builder: (context) => Stack(
|
||||||
|
children: [
|
||||||
|
ModalBarrier(
|
||||||
|
dismissible: true,
|
||||||
|
color: Colors.transparent,
|
||||||
|
onDismiss: _hidePopup,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: position.dy + size.height + 26.rpx,
|
||||||
|
left: position.dx + size.width - popupWidth - 40.rpx,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
width: popupWidth,
|
||||||
|
padding: EdgeInsets.all(20.rpx),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: themeController.currentColor.sc17,
|
||||||
|
borderRadius: BorderRadius.circular(12.rpx),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
blurRadius: 12.rpx,
|
||||||
|
spreadRadius: 2.rpx,
|
||||||
|
offset: Offset(0, 6.rpx),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 11.rpx),
|
||||||
|
ClickableContainer(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 10.rpx),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
highlightColor:
|
||||||
|
themeController.currentColor.sc16.withOpacity(0.1),
|
||||||
|
borderRadius: 0.rpx,
|
||||||
|
onTap: () {
|
||||||
|
_hidePopup();
|
||||||
|
BlueteethBindController blueteethBindController =
|
||||||
|
Get.find();
|
||||||
|
blueteethBindController.returnPage = 1;
|
||||||
|
Get.toNamed("/deviceType");
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'蓝牙绑定.标题'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppConstants().normal_text_fontSize,
|
||||||
|
color: themeController.currentColor.sc3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 13.rpx),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Overlay.of(context)!.insert(_popupEntry!);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hidePopup() {
|
||||||
|
_popupEntry?.remove();
|
||||||
|
_popupEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
bodyDeviceController.keyWord.value = "";
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// 初始化PageController,根据当前类型设置初始页面
|
||||||
|
_pageController = PageController(
|
||||||
|
initialPage: bodyDeviceController.model.type == 1 ? 0 : 1);
|
||||||
|
|
||||||
|
// 处理传入的type参数
|
||||||
|
if (widget.type != null && widget.type is Map) {
|
||||||
|
final bindType = widget.type['bind_type'];
|
||||||
|
final mac = widget.type['mac'];
|
||||||
|
|
||||||
|
if (bindType != null) {
|
||||||
|
bodyDeviceController.model.type = bindType;
|
||||||
|
homeController.model.type = bindType;
|
||||||
|
// 更新PageController到正确的位置
|
||||||
|
_pageController = PageController(
|
||||||
|
initialPage: bodyDeviceController.model.type == 1 ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fetchDeviceList().then((_) {
|
||||||
|
if (mac != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_scrollToDeviceWithMac(mac);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_fetchDeviceList();
|
||||||
|
}
|
||||||
|
|
||||||
|
_timer = Timer.periodic(Duration(seconds: 5), (timer) {
|
||||||
|
_fetchDeviceList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化飘动动画
|
||||||
|
_floatController = AnimationController(
|
||||||
|
duration: Duration(milliseconds: 16), // 约60帧/秒
|
||||||
|
vsync: this,
|
||||||
|
)..repeat();
|
||||||
|
|
||||||
|
// 监听动画更新
|
||||||
|
_floatController.addListener(_updateFloatPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新飘动文字位置(简化版)
|
||||||
|
void _updateFloatPosition() {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// 更新位置
|
||||||
|
setState(() {
|
||||||
|
_floatX += _speedX;
|
||||||
|
_floatY += _speedY;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 边界检测和反弹
|
||||||
|
final screenWidth = MediaQuery.of(Get.context!).size.width;
|
||||||
|
final screenHeight = MediaQuery.of(Get.context!).size.height;
|
||||||
|
|
||||||
|
// 文字尺寸
|
||||||
|
final textWidth = 100.rpx;
|
||||||
|
final textHeight = 30.rpx;
|
||||||
|
|
||||||
|
// 转换为实际像素位置
|
||||||
|
final actualX = (_floatX + 1) / 2 * (screenWidth - textWidth);
|
||||||
|
final actualY = (_floatY + 1) / 2 * (screenHeight - textHeight);
|
||||||
|
|
||||||
|
// 碰到右边界,向左反弹
|
||||||
|
if (actualX >= screenWidth - textWidth - 10) {
|
||||||
|
setState(() {
|
||||||
|
_speedX = -_speedX.abs(); // 向左
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 碰到左边界,向右反弹
|
||||||
|
else if (actualX <= 10) {
|
||||||
|
setState(() {
|
||||||
|
_speedX = _speedX.abs(); // 向右
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 碰到下边界,向上反弹
|
||||||
|
if (actualY >= screenHeight - textHeight - 80) {
|
||||||
|
// 留出底部空间
|
||||||
|
setState(() {
|
||||||
|
_speedY = -_speedY.abs(); // 向上
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 碰到上边界(避开AppBar),向下反弹
|
||||||
|
else if (actualY <= 180) {
|
||||||
|
// 留出AppBar和标签栏空间
|
||||||
|
setState(() {
|
||||||
|
_speedY = _speedY.abs(); // 向下
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建飘动文字组件(简化版)
|
||||||
|
Widget _buildFloatingText() {
|
||||||
|
return Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
// 忽略点击,避免干扰其他交互
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(
|
||||||
|
(_floatX + 1) / 2 * (MediaQuery.of(context).size.width - 100.rpx),
|
||||||
|
(_floatY + 1) / 2 * (MediaQuery.of(context).size.height - 30.rpx),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.rpx,
|
||||||
|
vertical: 6.rpx,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
// color: themeController.currentColor.sc2.withOpacity(0.15),
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(15.rpx),
|
||||||
|
// border: Border.all(
|
||||||
|
// color: themeController.currentColor.sc2.withOpacity(0.3),
|
||||||
|
// width: 1.rpx,
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'太和e护',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20.rpx,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: themeController.currentColor.sc2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchDeviceList() async {
|
||||||
|
await bodyDeviceController
|
||||||
|
.getDeviceList(key: bodyDeviceController.keyWord.value)
|
||||||
|
.then((apiResponse) {
|
||||||
|
if (apiResponse.code != HttpStatusCodes.ok) {
|
||||||
|
TopSlideNotification.show(
|
||||||
|
Get.context!,
|
||||||
|
text: apiResponse.msg!,
|
||||||
|
textColor: themeController.currentColor.sc9,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToDeviceWithMac(String mac) {
|
||||||
|
final deviceList = bodyDeviceController.deviceList.value;
|
||||||
|
final index = deviceList.indexWhere((device) => device['mac'] == mac);
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
final screenHeight = MediaQuery.of(Get.context!).size.height;
|
||||||
|
final dynamicItemHeight = (screenHeight * 0.266).rpx;
|
||||||
|
final itemHeight =
|
||||||
|
dynamicItemHeight < 501.rpx ? 501.rpx : dynamicItemHeight;
|
||||||
|
final spacing = 25.rpx;
|
||||||
|
final targetPosition = index * (itemHeight + spacing);
|
||||||
|
|
||||||
|
// 根据当前类型选择对应的ScrollController
|
||||||
|
final currentScrollController = bodyDeviceController.model.type == 1
|
||||||
|
? _myDeviceScrollController
|
||||||
|
: _cloudDeviceScrollController;
|
||||||
|
|
||||||
|
if (currentScrollController.hasClients) {
|
||||||
|
currentScrollController.animateTo(
|
||||||
|
targetPosition,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签切换回调
|
||||||
|
Future<void> _onTabChanged(int index) async {
|
||||||
|
_pageController.animateToPage(index,
|
||||||
|
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||||
|
BodyDeviceController deviceController = Get.find();
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
deviceController.model.type = 1;
|
||||||
|
homeController.model.type = 1;
|
||||||
|
} else if (index == 1) {
|
||||||
|
deviceController.model.type = 2;
|
||||||
|
homeController.model.type = 2;
|
||||||
|
}
|
||||||
|
deviceController.updateAll();
|
||||||
|
await deviceController.getDeviceList();
|
||||||
|
await deviceController.getSleepReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面切换回调
|
||||||
|
void _onPageChanged(int index) {
|
||||||
|
int newType = index == 0 ? 1 : 2;
|
||||||
|
if (bodyDeviceController.model.type != newType) {
|
||||||
|
bodyDeviceController.model.type = newType;
|
||||||
|
homeController.model.type = newType;
|
||||||
|
bodyDeviceController.updateAll();
|
||||||
|
homeController.updateAll();
|
||||||
|
_fetchDeviceList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_myDeviceScrollController.dispose();
|
||||||
|
_cloudDeviceScrollController.dispose();
|
||||||
|
_pageController.dispose();
|
||||||
|
_floatController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, bodysize) => GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_hidePopup();
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/img/bgNoImg.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: themeController.currentColor.sc17,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
iconTheme: IconThemeData(
|
||||||
|
color: themeController.currentColor.sc3,
|
||||||
|
),
|
||||||
|
titleSpacing: 0,
|
||||||
|
title: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 180.rpx,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'体征检测设备.标题'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'ReadexPro',
|
||||||
|
color: themeController.currentColor.sc3,
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontSize: 30.rpx,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
child: returnIconButtomAddCallback(() {
|
||||||
|
bodyDeviceController.getDeviceNum();
|
||||||
|
bodyDeviceController.getDeviceList();
|
||||||
|
bodyDeviceController.updateAll();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 20.rpx,
|
||||||
|
child: ClickableContainer(
|
||||||
|
key: addIconKey,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
highlightColor: themeController.currentColor.sc16,
|
||||||
|
padding: EdgeInsets.all(8.rpx),
|
||||||
|
onTap: () {
|
||||||
|
if (_popupEntry == null) {
|
||||||
|
_showPopup();
|
||||||
|
} else {
|
||||||
|
_hidePopup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/img/icon/add.svg',
|
||||||
|
width: 39.rpx,
|
||||||
|
height: 39.rpx,
|
||||||
|
color: themeController.currentColor.sc16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [],
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
body: GestureDetector(
|
||||||
|
onTap: _hidePopup,
|
||||||
|
child: SafeArea(
|
||||||
|
top: true,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsetsDirectional.fromSTEB(0.rpx, 0, 0.rpx, 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsetsDirectional.fromSTEB(0, 30.rpx, 0, 0),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: 90.rpx,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: themeController.currentColor.sc5),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsetsDirectional.fromSTEB(
|
||||||
|
30.rpx, 15.rpx, 30.rpx, 15.rpx),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
// 标签切换部分 - 保持原有样式
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Obx(() {
|
||||||
|
return ClickableContainer(
|
||||||
|
backgroundColor:
|
||||||
|
Colors.transparent,
|
||||||
|
highlightColor: themeController
|
||||||
|
.currentColor.sc3,
|
||||||
|
borderRadius: 8.rpx,
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
onTap: () => _onTabChanged(0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize:
|
||||||
|
MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 180.rpx,
|
||||||
|
alignment:
|
||||||
|
Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
'体征检测设备.我的e护'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontSize: AppConstants()
|
||||||
|
.title_text_fontSize,
|
||||||
|
letterSpacing: 0.0,
|
||||||
|
color: bodyDeviceController
|
||||||
|
.model
|
||||||
|
.type ==
|
||||||
|
2
|
||||||
|
? themeController
|
||||||
|
.currentColor
|
||||||
|
.sc3
|
||||||
|
: themeController
|
||||||
|
.currentColor
|
||||||
|
.sc2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.rpx),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Obx(() {
|
||||||
|
return ClickableContainer(
|
||||||
|
backgroundColor:
|
||||||
|
Colors.transparent,
|
||||||
|
highlightColor: themeController
|
||||||
|
.currentColor.sc3,
|
||||||
|
borderRadius: 8.rpx,
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
onTap: () => _onTabChanged(1),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize:
|
||||||
|
MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 180.rpx,
|
||||||
|
alignment:
|
||||||
|
Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
'体征检测设备.云关爱'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontSize: AppConstants()
|
||||||
|
.title_text_fontSize,
|
||||||
|
letterSpacing: 0.0,
|
||||||
|
color: bodyDeviceController
|
||||||
|
.model
|
||||||
|
.type ==
|
||||||
|
1
|
||||||
|
? themeController
|
||||||
|
.currentColor
|
||||||
|
.sc3
|
||||||
|
: themeController
|
||||||
|
.currentColor
|
||||||
|
.sc2,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow
|
||||||
|
.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.rpx),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Obx(() {
|
||||||
|
// 保持原有的横线宽度180.rpx
|
||||||
|
double lineWidth = 180.rpx;
|
||||||
|
return AnimatedPositioned(
|
||||||
|
duration:
|
||||||
|
Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
bottom: 0,
|
||||||
|
left: bodyDeviceController
|
||||||
|
.model.type ==
|
||||||
|
1
|
||||||
|
? 0
|
||||||
|
: 180.rpx,
|
||||||
|
child: Container(
|
||||||
|
width: lineWidth,
|
||||||
|
height: 4.rpx,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: themeController
|
||||||
|
.currentColor.sc2,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(
|
||||||
|
2.rpx),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 搜索框部分保持不变
|
||||||
|
Container(
|
||||||
|
width: MediaQuery.sizeOf(context).width *
|
||||||
|
0.38,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: 285.rpx,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(20.rpx),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsetsDirectional.fromSTEB(
|
||||||
|
0.rpx, 0, 20.rpx, 0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 80.rpx,
|
||||||
|
child: Align(
|
||||||
|
alignment:
|
||||||
|
AlignmentDirectional(
|
||||||
|
-1, 0),
|
||||||
|
child: TextFormField(
|
||||||
|
onChanged: (value) {
|
||||||
|
bodyDeviceController
|
||||||
|
.keyWord
|
||||||
|
.value = value;
|
||||||
|
},
|
||||||
|
autofocus: false,
|
||||||
|
obscureText: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.fromLTRB(
|
||||||
|
12.rpx,
|
||||||
|
0,
|
||||||
|
0.rpx,
|
||||||
|
0),
|
||||||
|
isDense: true,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontSize: 26.rpx,
|
||||||
|
letterSpacing: 0.0,
|
||||||
|
),
|
||||||
|
hintText:
|
||||||
|
'体征检测设备.输入关键词'.tr,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontSize: 26.rpx,
|
||||||
|
letterSpacing: 0.0,
|
||||||
|
color: themeController
|
||||||
|
.currentColor.sc4,
|
||||||
|
),
|
||||||
|
enabledBorder:
|
||||||
|
OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color:
|
||||||
|
Color(0x00000000),
|
||||||
|
width: 1.rpx,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius
|
||||||
|
.circular(
|
||||||
|
8.rpx),
|
||||||
|
),
|
||||||
|
focusedBorder:
|
||||||
|
OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color:
|
||||||
|
Color(0x00000000),
|
||||||
|
width: 1.rpx,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius
|
||||||
|
.circular(
|
||||||
|
8.rpx),
|
||||||
|
),
|
||||||
|
errorBorder:
|
||||||
|
OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.red,
|
||||||
|
width: 1.rpx,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius
|
||||||
|
.circular(
|
||||||
|
8.rpx),
|
||||||
|
),
|
||||||
|
focusedErrorBorder:
|
||||||
|
OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.red,
|
||||||
|
width: 1.rpx,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius
|
||||||
|
.circular(
|
||||||
|
8.rpx),
|
||||||
|
),
|
||||||
|
filled: false,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontSize: 26.rpx,
|
||||||
|
letterSpacing: 0.0,
|
||||||
|
color: themeController
|
||||||
|
.currentColor.sc3,
|
||||||
|
),
|
||||||
|
cursorColor: themeController
|
||||||
|
.currentColor.sc3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional
|
||||||
|
.fromSTEB(26.rpx, 0, 0, 0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 40.rpx,
|
||||||
|
child: VerticalDivider(
|
||||||
|
thickness: 2.rpx,
|
||||||
|
color: themeController
|
||||||
|
.currentColor.sc2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ClickableContainer(
|
||||||
|
backgroundColor:
|
||||||
|
Colors.transparent,
|
||||||
|
highlightColor:
|
||||||
|
themeController
|
||||||
|
.currentColor.sc5,
|
||||||
|
borderRadius: 6.rpx,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onTap: () async {
|
||||||
|
await bodyDeviceController
|
||||||
|
.getDeviceList(
|
||||||
|
key:
|
||||||
|
bodyDeviceController
|
||||||
|
.keyWord
|
||||||
|
.value);
|
||||||
|
bodyDeviceController
|
||||||
|
.updateAll();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'体征检测设备.搜索'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontSize: AppConstants()
|
||||||
|
.normal_text_fontSize,
|
||||||
|
letterSpacing: 0.0,
|
||||||
|
color: themeController
|
||||||
|
.currentColor.sc2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].divide(
|
||||||
|
SizedBox(width: 14.rpx)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 使用PageView替换原来的单一列表
|
||||||
|
Expanded(
|
||||||
|
child: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification:
|
||||||
|
(ScrollNotification notification) {
|
||||||
|
if (notification is ScrollStartNotification ||
|
||||||
|
notification is ScrollUpdateNotification) {
|
||||||
|
// 发送全局滚动事件
|
||||||
|
EventBus().emit(ScrollNotificationEvent());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: PageView(
|
||||||
|
controller: _pageController,
|
||||||
|
onPageChanged: _onPageChanged,
|
||||||
|
children: [
|
||||||
|
// 我的e护页面
|
||||||
|
Obx(() {
|
||||||
|
final myDeviceList = bodyDeviceController
|
||||||
|
.deviceList.value
|
||||||
|
.where((device) =>
|
||||||
|
device['type'] == 1 ||
|
||||||
|
device['bind_type'] == 1)
|
||||||
|
.toList();
|
||||||
|
return myDeviceList.isEmpty
|
||||||
|
? NullDataWidget()
|
||||||
|
: Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsetsDirectional.fromSTEB(
|
||||||
|
30.rpx, 26.rpx, 30.rpx, 0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller:
|
||||||
|
_myDeviceScrollController,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: myDeviceList
|
||||||
|
.map((device) =>
|
||||||
|
DeviceDataComponentWidget(
|
||||||
|
device: device))
|
||||||
|
.toList()
|
||||||
|
.divide(SizedBox(
|
||||||
|
height: 25.rpx)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
// 云关爱页面
|
||||||
|
Obx(() {
|
||||||
|
final cloudDeviceList = bodyDeviceController
|
||||||
|
.deviceList.value
|
||||||
|
.where((device) =>
|
||||||
|
device['type'] == 2 ||
|
||||||
|
device['bind_type'] == 2)
|
||||||
|
.toList();
|
||||||
|
return cloudDeviceList.isEmpty
|
||||||
|
? NullDataWidget()
|
||||||
|
: Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsetsDirectional.fromSTEB(
|
||||||
|
30.rpx, 26.rpx, 30.rpx, 0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller:
|
||||||
|
_cloudDeviceScrollController,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: cloudDeviceList
|
||||||
|
.map((device) =>
|
||||||
|
DeviceDataComponentWidget(
|
||||||
|
device: device))
|
||||||
|
.toList()
|
||||||
|
.divide(SizedBox(
|
||||||
|
height: 25.rpx)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 添加飘动文字(简化版)
|
||||||
|
_buildFloatingText(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDeviceCard(BuildContext context,
|
||||||
|
{required String title, required String imageUrl, required String type}) {
|
||||||
|
return CustomCard(
|
||||||
|
borderRadius: 20.rpx,
|
||||||
|
onTap: () {
|
||||||
|
if (type != null) {
|
||||||
|
if (type == '1') {
|
||||||
|
Get.toNamed("/blueteethDevice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors: [themeController.currentColor.sc17],
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: MediaQuery.sizeOf(context).height * 0.135,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: 220.rpx,
|
||||||
|
),
|
||||||
|
padding: EdgeInsetsDirectional.fromSTEB(77.rpx, 0, 21.rpx, 0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
color: const Color(0xFFC2CED7),
|
||||||
|
fontSize: 30.rpx,
|
||||||
|
letterSpacing: 0.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8.rpx),
|
||||||
|
child: Image.asset(
|
||||||
|
imageUrl,
|
||||||
|
width: 212.rpx,
|
||||||
|
height: 168.rpx,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -703,7 +703,7 @@ class _MinePageState extends State<MinePage> {
|
|||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'V1.0.2601.06',
|
'V1.0.2601.08',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
// color: Color(0xFFD9E3EB),
|
// color: Color(0xFFD9E3EB),
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class _SettingPageState extends State<SettingPage> {
|
|||||||
),
|
),
|
||||||
].divide(SizedBox(width: 22.rpx)),
|
].divide(SizedBox(width: 22.rpx)),
|
||||||
),
|
),
|
||||||
Text('SWES2026.1.5',
|
Text('SWES2026.1.8',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 26.rpx,
|
fontSize: 26.rpx,
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class _PrivacyPolicyNewPageState extends State<PrivacyPolicyNewPage> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(0, 0, 30.rpx, 0),
|
padding: EdgeInsets.fromLTRB(0, 0, 0.rpx, 0),
|
||||||
child: InAppWebView(
|
child: InAppWebView(
|
||||||
key: UniqueKey(),
|
key: UniqueKey(),
|
||||||
initialUrlRequest: URLRequest(url: WebUri(widget.sleepUri)),
|
initialUrlRequest: URLRequest(url: WebUri(widget.sleepUri)),
|
||||||
|
|||||||
@@ -1,3 +1,361 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
// class LineChartByRange extends StatefulWidget {
|
||||||
|
// final List<Map<String, dynamic>> showLabel;
|
||||||
|
// final int startTime;
|
||||||
|
// final int endTime;
|
||||||
|
// final int? threshold;
|
||||||
|
|
||||||
|
// /// 新增外部指定的 Y 轴最大值
|
||||||
|
// final int maxY;
|
||||||
|
|
||||||
|
// /// Y 轴分段数,默认6段
|
||||||
|
// final int ySegments;
|
||||||
|
|
||||||
|
// const LineChartByRange({
|
||||||
|
// Key? key,
|
||||||
|
// required this.showLabel,
|
||||||
|
// required this.startTime,
|
||||||
|
// required this.endTime,
|
||||||
|
// required this.maxY,
|
||||||
|
// this.threshold,
|
||||||
|
// this.ySegments = 6,
|
||||||
|
// }) : 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();
|
||||||
|
|
||||||
|
// 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 / widget.maxY);
|
||||||
|
|
||||||
|
// // 判断点击范围(圆点半径±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,
|
||||||
|
// maxY: widget.maxY,
|
||||||
|
// minTime: minTime,
|
||||||
|
// maxTime: maxTime,
|
||||||
|
// threshold: widget.threshold,
|
||||||
|
// ySegments: widget.ySegments,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// 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'
|
||||||
|
// "时长"
|
||||||
|
// .tr +
|
||||||
|
// ' ${selectedData!['times']}',
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 18.rpx,
|
||||||
|
// color: Colors.white,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class _LineChartByRangePainter extends CustomPainter {
|
||||||
|
// final List<Map<String, dynamic>> data;
|
||||||
|
// final int maxY;
|
||||||
|
// final DateTime minTime;
|
||||||
|
// final DateTime maxTime;
|
||||||
|
// final int? threshold;
|
||||||
|
// final int ySegments;
|
||||||
|
|
||||||
|
// _LineChartByRangePainter({
|
||||||
|
// required this.data,
|
||||||
|
// required this.maxY,
|
||||||
|
// required this.minTime,
|
||||||
|
// required this.maxTime,
|
||||||
|
// this.threshold,
|
||||||
|
// this.ySegments = 6,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// @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 axisPaint = Paint()
|
||||||
|
// ..color = Colors.grey.withOpacity(0.4)
|
||||||
|
// ..strokeWidth = 1.rpx;
|
||||||
|
|
||||||
|
// Paint thresholdPaint = Paint()
|
||||||
|
// ..color = themeController.currentColor.sc9
|
||||||
|
// ..strokeWidth = 1.rpx;
|
||||||
|
|
||||||
|
// // 阈值虚线(红色)
|
||||||
|
// if (threshold != null && threshold! >= 0 && threshold! <= maxY) {
|
||||||
|
// double yThreshold = chartHeight * (1 - threshold! / maxY);
|
||||||
|
// drawDashedLine(
|
||||||
|
// canvas,
|
||||||
|
// Offset(xStart, yThreshold),
|
||||||
|
// Offset(xEnd, yThreshold),
|
||||||
|
// thresholdPaint,
|
||||||
|
// dashWidth: 8.rpx,
|
||||||
|
// dashSpace: 6.rpx,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 绘制数据线段和圆点
|
||||||
|
// for (var item in data) {
|
||||||
|
// int start = item['startTime'];
|
||||||
|
// int end = item['endTime'];
|
||||||
|
// // int times = item['times'];
|
||||||
|
// int times = item['times'];
|
||||||
|
|
||||||
|
// double startX = xStart * 2 +
|
||||||
|
// chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
|
||||||
|
// double endX = xStart * 2 +
|
||||||
|
// chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
|
||||||
|
// double y = chartHeight * (1 - times / maxY);
|
||||||
|
|
||||||
|
// // 设置颜色(根据 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);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Y轴辅助线和文字
|
||||||
|
// for (int i = 0; i <= ySegments; i++) {
|
||||||
|
// double y = chartHeight * i / ySegments;
|
||||||
|
|
||||||
|
// if (i == ySegments) {
|
||||||
|
// 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: '${maxY - (maxY * i / ySegments).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));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // X轴主线
|
||||||
|
// canvas.drawLine(
|
||||||
|
// Offset(xStart, chartHeight),
|
||||||
|
// Offset(xEnd, chartHeight),
|
||||||
|
// axisPaint,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // 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));
|
||||||
|
|
||||||
|
// // 中间小时刻度
|
||||||
|
// 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 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
@@ -199,7 +557,6 @@ class _LineChartByRangePainter extends CustomPainter {
|
|||||||
for (var item in data) {
|
for (var item in data) {
|
||||||
int start = item['startTime'];
|
int start = item['startTime'];
|
||||||
int end = item['endTime'];
|
int end = item['endTime'];
|
||||||
// int times = item['times'];
|
|
||||||
int times = item['times'];
|
int times = item['times'];
|
||||||
|
|
||||||
double startX = xStart * 2 +
|
double startX = xStart * 2 +
|
||||||
@@ -305,15 +662,46 @@ class _LineChartByRangePainter extends CustomPainter {
|
|||||||
Offset(size.width - padding - labelInset - rightTp.width / 2,
|
Offset(size.width - padding - labelInset - rightTp.width / 2,
|
||||||
chartHeight + 8.rpx));
|
chartHeight + 8.rpx));
|
||||||
|
|
||||||
// 中间小时刻度
|
// 中间小时刻度 - 使用实际时间比例
|
||||||
int totalHours = maxTime.difference(minTime).inHours + 1;
|
final int hourMs = 60 * 60 * 1000;
|
||||||
int startHour = minTime.hour;
|
final int totalHours =
|
||||||
|
(maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch) ~/
|
||||||
|
hourMs;
|
||||||
|
|
||||||
for (int i = 1; i < totalHours; i++) {
|
if (totalHours <= 8) {
|
||||||
double x = xStart + chartWidth * i / totalHours;
|
// 显示每个整点
|
||||||
|
DateTime currentHour = DateTime(
|
||||||
|
minTime.year,
|
||||||
|
minTime.month,
|
||||||
|
minTime.day,
|
||||||
|
minTime.hour,
|
||||||
|
);
|
||||||
|
|
||||||
int hourLabelNum = (startHour + i) % 24;
|
// 如果起始时间不是整点,从下一个整点开始
|
||||||
String hourLabel = '$hourLabelNum';
|
if (minTime.minute > 0 || minTime.second > 0 || minTime.millisecond > 0) {
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
currentHour.millisecondsSinceEpoch < maxTime.millisecondsSinceEpoch) {
|
||||||
|
final int timeMs = currentHour.millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
if (timeMs > minTime.millisecondsSinceEpoch &&
|
||||||
|
timeMs < maxTime.millisecondsSinceEpoch) {
|
||||||
|
// 检查是否太靠近边界
|
||||||
|
if (timeMs - minTime.millisecondsSinceEpoch < 10 * 60 * 1000 ||
|
||||||
|
maxTime.millisecondsSinceEpoch - timeMs < 10 * 60 * 1000) {
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算从起始时间到当前整点的位置
|
||||||
|
final double x = xStart +
|
||||||
|
chartWidth *
|
||||||
|
(timeMs - minTime.millisecondsSinceEpoch) /
|
||||||
|
totalDuration;
|
||||||
|
|
||||||
|
final hourLabel = '${currentHour.hour}';
|
||||||
|
|
||||||
TextPainter tp = TextPainter(
|
TextPainter tp = TextPainter(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
@@ -326,7 +714,69 @@ class _LineChartByRangePainter extends CustomPainter {
|
|||||||
textDirection: ui.TextDirection.ltr,
|
textDirection: ui.TextDirection.ltr,
|
||||||
);
|
);
|
||||||
tp.layout();
|
tp.layout();
|
||||||
tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
|
tp.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(x - tp.width / 2, chartHeight + 8.rpx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 超过8小时,跳着显示
|
||||||
|
final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签
|
||||||
|
|
||||||
|
DateTime firstLabelHour = DateTime(
|
||||||
|
minTime.year,
|
||||||
|
minTime.month,
|
||||||
|
minTime.day,
|
||||||
|
minTime.hour + (labelInterval - (minTime.hour % labelInterval)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确保第一个标签在开始时间之后
|
||||||
|
if (firstLabelHour.millisecondsSinceEpoch <=
|
||||||
|
minTime.millisecondsSinceEpoch) {
|
||||||
|
firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime currentHour = firstLabelHour;
|
||||||
|
while (
|
||||||
|
currentHour.millisecondsSinceEpoch < maxTime.millisecondsSinceEpoch) {
|
||||||
|
final int timeMs = currentHour.millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
// 确保标签离边界足够远
|
||||||
|
if (timeMs - minTime.millisecondsSinceEpoch >= hourMs &&
|
||||||
|
maxTime.millisecondsSinceEpoch - timeMs >= hourMs) {
|
||||||
|
final double x = xStart +
|
||||||
|
chartWidth *
|
||||||
|
(timeMs - minTime.millisecondsSinceEpoch) /
|
||||||
|
totalDuration;
|
||||||
|
|
||||||
|
final hourLabel = '${currentHour.hour}';
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHour = currentHour.add(Duration(hours: labelInterval));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,364 @@
|
|||||||
|
// import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
// //柱形图显示
|
||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
// import 'package:intl/intl.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 BarData {
|
||||||
|
// final int st; // 起始时间(毫秒)
|
||||||
|
// final int et; // 结束时间(毫秒)
|
||||||
|
// final double value; // 柱子高度
|
||||||
|
// final int id;
|
||||||
|
// final String name;
|
||||||
|
// final Color color;
|
||||||
|
|
||||||
|
// BarData({
|
||||||
|
// required this.st,
|
||||||
|
// required this.et,
|
||||||
|
// required this.value,
|
||||||
|
// required this.id,
|
||||||
|
// required this.name,
|
||||||
|
// required this.color,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class BarChartWidget extends StatefulWidget {
|
||||||
|
// final List<BarData> data;
|
||||||
|
// final int startTime; // 毫秒时间戳
|
||||||
|
// final int endTime; // 毫秒时间戳
|
||||||
|
// final double maxYValue; // Y轴最大值
|
||||||
|
// final int yStepCount; // Y轴分段数
|
||||||
|
|
||||||
|
// const BarChartWidget({
|
||||||
|
// super.key,
|
||||||
|
// required this.data,
|
||||||
|
// required this.startTime,
|
||||||
|
// required this.endTime,
|
||||||
|
// required this.maxYValue,
|
||||||
|
// this.yStepCount = 5,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<BarChartWidget> createState() => _BarChartWidgetState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class _BarChartWidgetState extends State<BarChartWidget> {
|
||||||
|
// BarData? selectedBar;
|
||||||
|
|
||||||
|
// void _handleTapOrDrag(Offset localPosition, Size size) {
|
||||||
|
// final chartWidth = size.width - 30.rpx;
|
||||||
|
// final totalDuration = widget.endTime - widget.startTime;
|
||||||
|
|
||||||
|
// for (final d in widget.data) {
|
||||||
|
// final left =
|
||||||
|
// ((d.st - widget.startTime) / totalDuration) * chartWidth + 30.rpx;
|
||||||
|
// final right =
|
||||||
|
// ((d.et - widget.startTime) / totalDuration) * chartWidth + 30.rpx;
|
||||||
|
// if (localPosition.dx >= left && localPosition.dx <= right) {
|
||||||
|
// setState(() {
|
||||||
|
// selectedBar = d;
|
||||||
|
// });
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setState(() {
|
||||||
|
// selectedBar = null;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
// return GestureDetector(
|
||||||
|
// behavior: HitTestBehavior.opaque,
|
||||||
|
// onPanDown: (details) =>
|
||||||
|
// _handleTapOrDrag(details.localPosition, constraints.biggest),
|
||||||
|
// onPanUpdate: (details) =>
|
||||||
|
// _handleTapOrDrag(details.localPosition, constraints.biggest),
|
||||||
|
// onTapDown: (details) =>
|
||||||
|
// _handleTapOrDrag(details.localPosition, constraints.biggest),
|
||||||
|
// child: CustomPaint(
|
||||||
|
// size: Size(constraints.maxWidth, 250.rpx), // 使用约束的最大宽度
|
||||||
|
// painter: BarChartPainter(
|
||||||
|
// widget.data,
|
||||||
|
// widget.startTime,
|
||||||
|
// widget.endTime,
|
||||||
|
// maxYValue: widget.maxYValue,
|
||||||
|
// yStepCount: widget.yStepCount,
|
||||||
|
// selectedBar: selectedBar,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class BarChartPainter extends CustomPainter {
|
||||||
|
// final List<BarData> data;
|
||||||
|
// final int startTime;
|
||||||
|
// final int endTime;
|
||||||
|
// final double maxYValue;
|
||||||
|
// final int yStepCount;
|
||||||
|
// final BarData? selectedBar;
|
||||||
|
|
||||||
|
// final double topPadding = 0;
|
||||||
|
// final double bottomPadding = 0;
|
||||||
|
// final double leftPadding = 30.rpx;
|
||||||
|
|
||||||
|
// BarChartPainter(
|
||||||
|
// this.data,
|
||||||
|
// this.startTime,
|
||||||
|
// this.endTime, {
|
||||||
|
// required this.maxYValue,
|
||||||
|
// this.yStepCount = 5,
|
||||||
|
// this.selectedBar,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// void paint(Canvas canvas, Size size) {
|
||||||
|
// final chartWidth = size.width - leftPadding;
|
||||||
|
// final chartHeight = size.height - topPadding - bottomPadding;
|
||||||
|
// final totalDuration = endTime - startTime;
|
||||||
|
|
||||||
|
// final textPainter = TextPainter(textDirection: ui.TextDirection.ltr);
|
||||||
|
// final stepValue = maxYValue / yStepCount;
|
||||||
|
|
||||||
|
// // Y轴刻度
|
||||||
|
// // for (int i = 0; i <= yStepCount; i++) {
|
||||||
|
// // final value = stepValue * i;
|
||||||
|
// // final y = topPadding + chartHeight - (value / maxYValue) * chartHeight;
|
||||||
|
|
||||||
|
// // final dashPaint = Paint()
|
||||||
|
// // ..color = Colors.grey.withOpacity(0.4)
|
||||||
|
// // ..strokeWidth = 1.rpx;
|
||||||
|
|
||||||
|
// // drawDashedLine(
|
||||||
|
// // canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint);
|
||||||
|
|
||||||
|
// // textPainter.text = TextSpan(
|
||||||
|
// // text: value.toStringAsFixed(0),
|
||||||
|
// // style: TextStyle(
|
||||||
|
// // fontSize: 18.rpx,
|
||||||
|
// // color: themeController.currentColor.sc4,
|
||||||
|
// // ),
|
||||||
|
// // );
|
||||||
|
// // textPainter.layout();
|
||||||
|
// // textPainter.paint(
|
||||||
|
// // canvas,
|
||||||
|
// // Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2),
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // Y轴刻度
|
||||||
|
// for (int i = 0; i <= yStepCount; i++) {
|
||||||
|
// final value = stepValue * i;
|
||||||
|
// final y = topPadding + chartHeight - (value / maxYValue) * chartHeight;
|
||||||
|
|
||||||
|
// // 判断是否是基线(i == 0)
|
||||||
|
// final bool isBaseline = i == 0;
|
||||||
|
|
||||||
|
// if (isBaseline) {
|
||||||
|
// // 基线画实线
|
||||||
|
// final baselinePaint = Paint()
|
||||||
|
// ..color = Colors.grey.withOpacity(0.6)
|
||||||
|
// ..strokeWidth = 1.rpx
|
||||||
|
// ..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
// canvas.drawLine(
|
||||||
|
// Offset(leftPadding, y),
|
||||||
|
// Offset(size.width, y),
|
||||||
|
// baselinePaint,
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// // 其他刻度画虚线
|
||||||
|
// final dashPaint = Paint()
|
||||||
|
// ..color = Colors.grey.withOpacity(0.4)
|
||||||
|
// ..strokeWidth = 1.rpx;
|
||||||
|
|
||||||
|
// drawDashedLine(
|
||||||
|
// canvas,
|
||||||
|
// Offset(leftPadding, y),
|
||||||
|
// Offset(size.width, y),
|
||||||
|
// dashPaint,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 绘制刻度文字
|
||||||
|
// textPainter.text = TextSpan(
|
||||||
|
// text: value.toStringAsFixed(0),
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 18.rpx,
|
||||||
|
// color: themeController.currentColor.sc4,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// textPainter.layout();
|
||||||
|
// textPainter.paint(
|
||||||
|
// canvas,
|
||||||
|
// Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // X轴刻度 - 参考横线图的24小时制
|
||||||
|
// final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||||
|
// final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||||
|
// final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标
|
||||||
|
|
||||||
|
// // 计算总小时数
|
||||||
|
// final totalHours = endDate.difference(startDate).inHours + 1;
|
||||||
|
// final startHour = startDate.hour;
|
||||||
|
|
||||||
|
// // 绘制X轴主线(实线)
|
||||||
|
// // final xAxisPaint = Paint()
|
||||||
|
// // ..color = Colors.grey.withOpacity(0.4)
|
||||||
|
// // ..strokeWidth = 1.rpx;
|
||||||
|
// // canvas.drawLine(
|
||||||
|
// // Offset(leftPadding, xAxisY),
|
||||||
|
// // Offset(size.width, xAxisY),
|
||||||
|
// // xAxisPaint,
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // 绘制左右两侧时间标签(HH:mm格式)
|
||||||
|
// final leftLabel = DateFormat('HH:mm').format(startDate);
|
||||||
|
// textPainter.text = TextSpan(
|
||||||
|
// text: leftLabel,
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 18.rpx,
|
||||||
|
// color: themeController.currentColor.sc4,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// textPainter.layout();
|
||||||
|
// textPainter.paint(
|
||||||
|
// canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx));
|
||||||
|
|
||||||
|
// final rightLabel = DateFormat('HH:mm').format(endDate);
|
||||||
|
// textPainter.text = TextSpan(
|
||||||
|
// text: rightLabel,
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 18.rpx,
|
||||||
|
// color: themeController.currentColor.sc4,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// textPainter.layout();
|
||||||
|
// textPainter.paint(
|
||||||
|
// canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx));
|
||||||
|
|
||||||
|
// // 绘制中间小时刻度(只显示小时数字)
|
||||||
|
// for (int i = 1; i < totalHours; i++) {
|
||||||
|
// final double x = leftPadding + chartWidth * i / totalHours;
|
||||||
|
|
||||||
|
// int hourLabelNum = (startHour + i) % 24;
|
||||||
|
// final hourLabel = '$hourLabelNum';
|
||||||
|
|
||||||
|
// textPainter.text = TextSpan(
|
||||||
|
// text: hourLabel,
|
||||||
|
// style: TextStyle(
|
||||||
|
// fontSize: 18.rpx,
|
||||||
|
// color: themeController.currentColor.sc4,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// textPainter.layout();
|
||||||
|
// textPainter.paint(
|
||||||
|
// canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 柱子绘制 & 提示信息缓存
|
||||||
|
// Offset? tipOffset;
|
||||||
|
// Size? tipSize;
|
||||||
|
// String? tipText;
|
||||||
|
// Color tipColor = Colors.black;
|
||||||
|
|
||||||
|
// for (final d in data) {
|
||||||
|
// final left =
|
||||||
|
// ((d.st - startTime) / totalDuration) * chartWidth + leftPadding;
|
||||||
|
// final right =
|
||||||
|
// ((d.et - startTime) / totalDuration) * chartWidth + leftPadding;
|
||||||
|
// final barHeight = (d.value / maxYValue) * chartHeight;
|
||||||
|
// final top = topPadding + chartHeight - barHeight;
|
||||||
|
|
||||||
|
// final barPaint = Paint()..color = d.color;
|
||||||
|
// canvas.drawRect(
|
||||||
|
// Rect.fromLTRB(left, top, right, topPadding + chartHeight),
|
||||||
|
// barPaint,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // 缓存 tip 信息
|
||||||
|
// if (selectedBar == d) {
|
||||||
|
// tipText =
|
||||||
|
// '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}';
|
||||||
|
|
||||||
|
// final tp = TextPainter(
|
||||||
|
// text: TextSpan(
|
||||||
|
// text: tipText,
|
||||||
|
// style: TextStyle(fontSize: 16.rpx, color: Colors.white),
|
||||||
|
// ),
|
||||||
|
// textAlign: TextAlign.center,
|
||||||
|
// textDirection: ui.TextDirection.ltr,
|
||||||
|
// );
|
||||||
|
// tp.layout();
|
||||||
|
|
||||||
|
// final tipWidth = tp.width + 20.rpx;
|
||||||
|
// final tipHeight = tp.height + 10.rpx;
|
||||||
|
// final tipLeft = left + (right - left) / 2 - tipWidth / 2;
|
||||||
|
// final tipTop = top - tipHeight - 10.rpx;
|
||||||
|
|
||||||
|
// // 确保tip不会超出画布顶部
|
||||||
|
// final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop;
|
||||||
|
|
||||||
|
// tipOffset = Offset(tipLeft, adjustedTipTop);
|
||||||
|
// tipSize = Size(tipWidth, tipHeight);
|
||||||
|
// tipColor = Colors.black.withOpacity(0.8);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 绘制 tip(在柱子之上)
|
||||||
|
// if (tipText != null && tipOffset != null && tipSize != null) {
|
||||||
|
// final rect = RRect.fromRectAndRadius(
|
||||||
|
// Rect.fromLTWH(
|
||||||
|
// tipOffset.dx, tipOffset.dy, tipSize.width, tipSize.height),
|
||||||
|
// Radius.circular(8.rpx),
|
||||||
|
// );
|
||||||
|
// final tipBgPaint = Paint()..color = tipColor;
|
||||||
|
// canvas.drawRRect(rect, tipBgPaint);
|
||||||
|
|
||||||
|
// final tipTextPainter = TextPainter(
|
||||||
|
// text: TextSpan(
|
||||||
|
// text: tipText,
|
||||||
|
// style: TextStyle(fontSize: 16.rpx, color: Colors.white),
|
||||||
|
// ),
|
||||||
|
// textAlign: TextAlign.center,
|
||||||
|
// textDirection: ui.TextDirection.ltr,
|
||||||
|
// );
|
||||||
|
// tipTextPainter.layout();
|
||||||
|
// tipTextPainter.paint(
|
||||||
|
// canvas,
|
||||||
|
// Offset(tipOffset.dx + 10.rpx, tipOffset.dy + 5.rpx),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||||
|
|
||||||
|
// void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint,
|
||||||
|
// {double dashWidth = 5, double dashSpace = 3}) {
|
||||||
|
// double totalLength = (end.dx - start.dx).abs();
|
||||||
|
// double dashCount = (totalLength / (dashWidth + dashSpace)).floorToDouble();
|
||||||
|
|
||||||
|
// double dx = start.dx;
|
||||||
|
// final dy = start.dy;
|
||||||
|
|
||||||
|
// for (int i = 0; i < dashCount; i++) {
|
||||||
|
// final from = Offset(dx, dy);
|
||||||
|
// final to = Offset(dx + dashWidth, dy);
|
||||||
|
// canvas.drawLine(from, to, paint);
|
||||||
|
// dx += dashWidth + dashSpace;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
//柱形图显示
|
//柱形图显示
|
||||||
@@ -127,31 +488,6 @@ class BarChartPainter extends CustomPainter {
|
|||||||
final textPainter = TextPainter(textDirection: ui.TextDirection.ltr);
|
final textPainter = TextPainter(textDirection: ui.TextDirection.ltr);
|
||||||
final stepValue = maxYValue / yStepCount;
|
final stepValue = maxYValue / yStepCount;
|
||||||
|
|
||||||
// Y轴刻度
|
|
||||||
// for (int i = 0; i <= yStepCount; i++) {
|
|
||||||
// final value = stepValue * i;
|
|
||||||
// final y = topPadding + chartHeight - (value / maxYValue) * chartHeight;
|
|
||||||
|
|
||||||
// final dashPaint = Paint()
|
|
||||||
// ..color = Colors.grey.withOpacity(0.4)
|
|
||||||
// ..strokeWidth = 1.rpx;
|
|
||||||
|
|
||||||
// drawDashedLine(
|
|
||||||
// canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint);
|
|
||||||
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: value.toStringAsFixed(0),
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 18.rpx,
|
|
||||||
// color: themeController.currentColor.sc4,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas,
|
|
||||||
// Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// Y轴刻度
|
// Y轴刻度
|
||||||
for (int i = 0; i <= yStepCount; i++) {
|
for (int i = 0; i <= yStepCount; i++) {
|
||||||
final value = stepValue * i;
|
final value = stepValue * i;
|
||||||
@@ -201,24 +537,14 @@ class BarChartPainter extends CustomPainter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// X轴刻度 - 参考横线图的24小时制
|
// X轴刻度 - 基于实际时间而不是均匀分布
|
||||||
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||||
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||||
final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标
|
final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标
|
||||||
|
|
||||||
// 计算总小时数
|
// 计算总小时数
|
||||||
final totalHours = endDate.difference(startDate).inHours + 1;
|
final int hourMs = 60 * 60 * 1000;
|
||||||
final startHour = startDate.hour;
|
final int totalHours = (endTime - startTime) ~/ hourMs;
|
||||||
|
|
||||||
// 绘制X轴主线(实线)
|
|
||||||
// final xAxisPaint = Paint()
|
|
||||||
// ..color = Colors.grey.withOpacity(0.4)
|
|
||||||
// ..strokeWidth = 1.rpx;
|
|
||||||
// canvas.drawLine(
|
|
||||||
// Offset(leftPadding, xAxisY),
|
|
||||||
// Offset(size.width, xAxisY),
|
|
||||||
// xAxisPaint,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 绘制左右两侧时间标签(HH:mm格式)
|
// 绘制左右两侧时间标签(HH:mm格式)
|
||||||
final leftLabel = DateFormat('HH:mm').format(startDate);
|
final leftLabel = DateFormat('HH:mm').format(startDate);
|
||||||
@@ -245,12 +571,38 @@ class BarChartPainter extends CustomPainter {
|
|||||||
textPainter.paint(
|
textPainter.paint(
|
||||||
canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx));
|
canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx));
|
||||||
|
|
||||||
// 绘制中间小时刻度(只显示小时数字)
|
// 绘制中间小时刻度 - 参考SnoreChartContainer的逻辑
|
||||||
for (int i = 1; i < totalHours; i++) {
|
if (totalHours <= 8) {
|
||||||
final double x = leftPadding + chartWidth * i / totalHours;
|
// 显示每个整点
|
||||||
|
DateTime currentHour = DateTime(
|
||||||
|
startDate.year,
|
||||||
|
startDate.month,
|
||||||
|
startDate.day,
|
||||||
|
startDate.hour,
|
||||||
|
);
|
||||||
|
|
||||||
int hourLabelNum = (startHour + i) % 24;
|
// 如果起始时间不是整点,从下一个整点开始
|
||||||
final hourLabel = '$hourLabelNum';
|
if (startDate.minute > 0 ||
|
||||||
|
startDate.second > 0 ||
|
||||||
|
startDate.millisecond > 0) {
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
while (currentHour.millisecondsSinceEpoch < endTime) {
|
||||||
|
final int timeMs = currentHour.millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
if (timeMs > startTime && timeMs < endTime) {
|
||||||
|
// 检查是否太靠近边界
|
||||||
|
if (timeMs - startTime < 10 * 60 * 1000 ||
|
||||||
|
endTime - timeMs < 10 * 60 * 1000) {
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算从起始时间到当前整点的分钟数
|
||||||
|
final double x =
|
||||||
|
leftPadding + ((timeMs - startTime) / totalDuration) * chartWidth;
|
||||||
|
final hourLabel = '${currentHour.hour}';
|
||||||
|
|
||||||
textPainter.text = TextSpan(
|
textPainter.text = TextSpan(
|
||||||
text: hourLabel,
|
text: hourLabel,
|
||||||
@@ -261,7 +613,59 @@ class BarChartPainter extends CustomPainter {
|
|||||||
);
|
);
|
||||||
textPainter.layout();
|
textPainter.layout();
|
||||||
textPainter.paint(
|
textPainter.paint(
|
||||||
canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx));
|
canvas,
|
||||||
|
Offset(x - textPainter.width / 2, xAxisY + 8.rpx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 超过8小时,跳着显示
|
||||||
|
final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签
|
||||||
|
|
||||||
|
DateTime firstLabelHour = DateTime(
|
||||||
|
startDate.year,
|
||||||
|
startDate.month,
|
||||||
|
startDate.day,
|
||||||
|
startDate.hour + (labelInterval - (startDate.hour % labelInterval)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确保第一个标签在开始时间之后
|
||||||
|
if (firstLabelHour.millisecondsSinceEpoch <= startTime) {
|
||||||
|
firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime currentHour = firstLabelHour;
|
||||||
|
while (currentHour.millisecondsSinceEpoch < endTime) {
|
||||||
|
final int timeMs = currentHour.millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
// 确保标签离边界足够远
|
||||||
|
if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) {
|
||||||
|
final double x =
|
||||||
|
leftPadding + ((timeMs - startTime) / totalDuration) * chartWidth;
|
||||||
|
final hourLabel = '${currentHour.hour}';
|
||||||
|
|
||||||
|
textPainter.text = TextSpan(
|
||||||
|
text: hourLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.rpx,
|
||||||
|
color: themeController.currentColor.sc4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
textPainter.layout();
|
||||||
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(x - textPainter.width / 2, xAxisY + 8.rpx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHour = currentHour.add(Duration(hours: labelInterval));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 柱子绘制 & 提示信息缓存
|
// 柱子绘制 & 提示信息缓存
|
||||||
@@ -358,330 +762,3 @@ class BarChartPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//横线显示
|
|
||||||
// import 'dart:math';
|
|
||||||
// import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
// class BarData {
|
|
||||||
// final int st; // 起始时间(毫秒)
|
|
||||||
// final int et; // 结束时间(毫秒)
|
|
||||||
// final double value; // 横线的高度值
|
|
||||||
// final int id;
|
|
||||||
// final String name;
|
|
||||||
// final Color color;
|
|
||||||
|
|
||||||
// BarData({
|
|
||||||
// required this.st,
|
|
||||||
// required this.et,
|
|
||||||
// required this.value,
|
|
||||||
// required this.id,
|
|
||||||
// required this.name,
|
|
||||||
// required this.color,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class BarChartWidget extends StatefulWidget {
|
|
||||||
// final List<BarData> data;
|
|
||||||
// final int startTime; // 毫秒时间戳
|
|
||||||
// final int endTime; // 毫秒时间戳
|
|
||||||
// final double maxYValue; // Y轴最大值
|
|
||||||
// final int yStepCount; // Y轴分段数
|
|
||||||
|
|
||||||
// const BarChartWidget({
|
|
||||||
// super.key,
|
|
||||||
// required this.data,
|
|
||||||
// required this.startTime,
|
|
||||||
// required this.endTime,
|
|
||||||
// required this.maxYValue,
|
|
||||||
// this.yStepCount = 5,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// State<BarChartWidget> createState() => _BarChartWidgetState();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class _BarChartWidgetState extends State<BarChartWidget> {
|
|
||||||
// BarData? selectedBar;
|
|
||||||
|
|
||||||
// void _handleTapOrDrag(Offset localPosition, Size size) {
|
|
||||||
// print('点击位置: $localPosition, 画布大小: $size');
|
|
||||||
// final chartWidth = size.width - 30.rpx;
|
|
||||||
// final totalDuration = widget.endTime - widget.startTime;
|
|
||||||
|
|
||||||
// // 使用与绘制相同的chartHeight计算方式
|
|
||||||
// final double topPadding = 0;
|
|
||||||
// final double bottomPadding = 0;
|
|
||||||
// final double leftPadding = 30.rpx;
|
|
||||||
// final double chartHeight = size.height - topPadding - bottomPadding;
|
|
||||||
|
|
||||||
// for (final d in widget.data) {
|
|
||||||
// final left = ((d.st - widget.startTime) / totalDuration) * chartWidth +
|
|
||||||
// leftPadding;
|
|
||||||
// final right = ((d.et - widget.startTime) / totalDuration) * chartWidth +
|
|
||||||
// leftPadding;
|
|
||||||
|
|
||||||
// // 使用与绘制相同的Y坐标计算方式
|
|
||||||
// final y = topPadding + chartHeight * (1 - d.value / widget.maxYValue);
|
|
||||||
|
|
||||||
// // 判断点击是否在横线附近(增加容差范围)
|
|
||||||
// if (localPosition.dx >= left - 5.rpx &&
|
|
||||||
// localPosition.dx <= right + 5.rpx &&
|
|
||||||
// (localPosition.dy - y).abs() < 15.rpx) {
|
|
||||||
// setState(() {
|
|
||||||
// selectedBar = d;
|
|
||||||
// });
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setState(() {
|
|
||||||
// selectedBar = null;
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return LayoutBuilder(builder: (context, constraints) {
|
|
||||||
// return GestureDetector(
|
|
||||||
// behavior: HitTestBehavior.opaque,
|
|
||||||
// onPanDown: (details) =>
|
|
||||||
// _handleTapOrDrag(details.localPosition, constraints.biggest),
|
|
||||||
// onPanUpdate: (details) =>
|
|
||||||
// _handleTapOrDrag(details.localPosition, constraints.biggest),
|
|
||||||
// onTapDown: (details) =>
|
|
||||||
// _handleTapOrDrag(details.localPosition, constraints.biggest),
|
|
||||||
// child: CustomPaint(
|
|
||||||
// // size: Size(double.infinity, 500.rpx),
|
|
||||||
// size: Size(constraints.maxWidth, 500.rpx), // 使用约束的最大宽度
|
|
||||||
// painter: BarChartPainter(
|
|
||||||
// widget.data,
|
|
||||||
// widget.startTime,
|
|
||||||
// widget.endTime,
|
|
||||||
// maxYValue: widget.maxYValue,
|
|
||||||
// yStepCount: widget.yStepCount,
|
|
||||||
// selectedBar: selectedBar,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class BarChartPainter extends CustomPainter {
|
|
||||||
// final List<BarData> data;
|
|
||||||
// final int startTime;
|
|
||||||
// final int endTime;
|
|
||||||
// final double maxYValue;
|
|
||||||
// final int yStepCount;
|
|
||||||
// final BarData? selectedBar;
|
|
||||||
|
|
||||||
// final double topPadding = 0;
|
|
||||||
// final double bottomPadding = 0;
|
|
||||||
// final double leftPadding = 30.rpx;
|
|
||||||
|
|
||||||
// BarChartPainter(
|
|
||||||
// this.data,
|
|
||||||
// this.startTime,
|
|
||||||
// this.endTime, {
|
|
||||||
// required this.maxYValue,
|
|
||||||
// this.yStepCount = 5,
|
|
||||||
// this.selectedBar,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// void paint(Canvas canvas, Size size) {
|
|
||||||
// final chartWidth = size.width - leftPadding;
|
|
||||||
// final chartHeight = size.height - topPadding - bottomPadding;
|
|
||||||
// final totalDuration = endTime - startTime;
|
|
||||||
|
|
||||||
// final textPainter = TextPainter(textDirection: ui.TextDirection.ltr);
|
|
||||||
// final stepValue = maxYValue / yStepCount;
|
|
||||||
|
|
||||||
// // Y轴刻度 - 从大到小显示(与参考代码一致)
|
|
||||||
// for (int i = 0; i <= yStepCount; i++) {
|
|
||||||
// final value = maxYValue - (stepValue * i); // 从最大值开始递减
|
|
||||||
// final y = topPadding + chartHeight * i / yStepCount;
|
|
||||||
|
|
||||||
// final dashPaint = Paint()
|
|
||||||
// ..color = Colors.grey.withOpacity(0.4)
|
|
||||||
// ..strokeWidth = 1.rpx;
|
|
||||||
|
|
||||||
// // 最上面的线(i == ySegments)是实线
|
|
||||||
// if (i == yStepCount) {
|
|
||||||
// canvas.drawLine(
|
|
||||||
// Offset(leftPadding, y), Offset(size.width, y), dashPaint);
|
|
||||||
// } else {
|
|
||||||
// // 其他线都是虚线
|
|
||||||
// drawDashedLine(
|
|
||||||
// canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: value.toStringAsFixed(0),
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 18.rpx,
|
|
||||||
// color: themeController.currentColor.sc4,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas,
|
|
||||||
// Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // X轴刻度 - 24小时制
|
|
||||||
// final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
|
||||||
// final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
|
||||||
// final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标
|
|
||||||
|
|
||||||
// // 计算总小时数
|
|
||||||
// final totalHours = endDate.difference(startDate).inHours + 1;
|
|
||||||
// final startHour = startDate.hour;
|
|
||||||
|
|
||||||
// // 绘制X轴主线(实线)
|
|
||||||
// final xAxisPaint = Paint()
|
|
||||||
// ..color = Colors.grey.withOpacity(0.4)
|
|
||||||
// ..strokeWidth = 1.rpx;
|
|
||||||
// canvas.drawLine(
|
|
||||||
// Offset(leftPadding, xAxisY),
|
|
||||||
// Offset(size.width, xAxisY),
|
|
||||||
// xAxisPaint,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // 绘制左右两侧时间标签
|
|
||||||
// final leftLabel = DateFormat('HH:mm').format(startDate);
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: leftLabel,
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 18.rpx,
|
|
||||||
// color: themeController.currentColor.sc4,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx));
|
|
||||||
|
|
||||||
// final rightLabel = DateFormat('HH:mm').format(endDate);
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: rightLabel,
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 18.rpx,
|
|
||||||
// color: themeController.currentColor.sc4,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx));
|
|
||||||
|
|
||||||
// // 绘制中间小时刻度
|
|
||||||
// for (int i = 1; i < totalHours; i++) {
|
|
||||||
// final double x = leftPadding + chartWidth * i / totalHours;
|
|
||||||
|
|
||||||
// int hourLabelNum = (startHour + i) % 24;
|
|
||||||
// final hourLabel = '$hourLabelNum';
|
|
||||||
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: hourLabel,
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 18.rpx,
|
|
||||||
// color: themeController.currentColor.sc4,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 绘制数据横线(根据数据值绘制水平线段)
|
|
||||||
// for (final d in data) {
|
|
||||||
// final left =
|
|
||||||
// ((d.st - startTime) / totalDuration) * chartWidth + leftPadding;
|
|
||||||
// final right =
|
|
||||||
// ((d.et - startTime) / totalDuration) * chartWidth + leftPadding;
|
|
||||||
|
|
||||||
// // 根据value计算Y位置(0在底部,maxYValue在顶部)
|
|
||||||
// final y = topPadding + chartHeight * (1 - d.value / maxYValue);
|
|
||||||
|
|
||||||
// final linePaint = Paint()
|
|
||||||
// ..style = PaintingStyle.stroke
|
|
||||||
// ..strokeWidth = 3.rpx
|
|
||||||
// ..color = d.color
|
|
||||||
// ..strokeCap = StrokeCap.round;
|
|
||||||
|
|
||||||
// // 画水平线段
|
|
||||||
// canvas.drawLine(Offset(left, y), Offset(right, y), linePaint);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 如果选中了某条横线,显示提示信息
|
|
||||||
// // 如果选中了某条横线,显示提示信息
|
|
||||||
// if (selectedBar != null) {
|
|
||||||
// final d = selectedBar!;
|
|
||||||
// final left =
|
|
||||||
// ((d.st - startTime) / totalDuration) * chartWidth + leftPadding;
|
|
||||||
// final right =
|
|
||||||
// ((d.et - startTime) / totalDuration) * chartWidth + leftPadding;
|
|
||||||
// final y = topPadding + chartHeight * (1 - d.value / maxYValue);
|
|
||||||
|
|
||||||
// final tipText =
|
|
||||||
// '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}';
|
|
||||||
|
|
||||||
// final tp = TextPainter(
|
|
||||||
// text: TextSpan(
|
|
||||||
// text: tipText,
|
|
||||||
// style: TextStyle(fontSize: 16.rpx, color: Colors.white),
|
|
||||||
// ),
|
|
||||||
// textAlign: TextAlign.center,
|
|
||||||
// textDirection: ui.TextDirection.ltr,
|
|
||||||
// );
|
|
||||||
// tp.layout();
|
|
||||||
|
|
||||||
// final tipWidth = tp.width + 20.rpx;
|
|
||||||
// final tipHeight = tp.height + 10.rpx;
|
|
||||||
// final tipLeft = left + (right - left) / 2 - tipWidth / 2;
|
|
||||||
// final tipTop = y - tipHeight - 10.rpx;
|
|
||||||
|
|
||||||
// // 确保tip不会超出画布顶部 - 明确转换为double
|
|
||||||
// final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop;
|
|
||||||
|
|
||||||
// // 绘制tip背景
|
|
||||||
// final rect = RRect.fromRectAndRadius(
|
|
||||||
// Rect.fromLTWH(tipLeft, adjustedTipTop, tipWidth, tipHeight),
|
|
||||||
// Radius.circular(8.rpx),
|
|
||||||
// );
|
|
||||||
// final tipBgPaint = Paint()..color = Colors.black.withOpacity(0.8);
|
|
||||||
// canvas.drawRRect(rect, tipBgPaint);
|
|
||||||
|
|
||||||
// // 绘制tip文字
|
|
||||||
// tp.paint(
|
|
||||||
// canvas,
|
|
||||||
// Offset(tipLeft + 10.rpx, adjustedTipTop + 5.rpx),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
||||||
|
|
||||||
// void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint,
|
|
||||||
// {double dashWidth = 4, double dashSpace = 3}) {
|
|
||||||
// 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;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,410 +1,3 @@
|
|||||||
// import 'dart:ui' as ui;
|
|
||||||
// import 'package:flutter/material.dart';
|
|
||||||
// import 'package:flutterflow_ui/flutterflow_ui.dart';
|
|
||||||
// import 'package:vbvs_app/common/util/FitTool.dart';
|
|
||||||
// import 'package:vbvs_app/common/util/MyUtils.dart';
|
|
||||||
// import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
// class SnoreChartContainer extends StatelessWidget {
|
|
||||||
// final List<dynamic> snoreValues;
|
|
||||||
// final List<dynamic> barData;
|
|
||||||
// final List<dynamic> showLabel;
|
|
||||||
// final int startTime;
|
|
||||||
// final int endTime;
|
|
||||||
|
|
||||||
// const SnoreChartContainer({
|
|
||||||
// required this.snoreValues,
|
|
||||||
// required this.barData,
|
|
||||||
// required this.showLabel,
|
|
||||||
// required this.startTime,
|
|
||||||
// required this.endTime,
|
|
||||||
// super.key,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// SnoreBarOverlay(
|
|
||||||
// barData: barData,
|
|
||||||
// showLabel: showLabel,
|
|
||||||
// startTime: startTime,
|
|
||||||
// endTime: endTime,
|
|
||||||
// ),
|
|
||||||
// Container(height: 32.rpx),
|
|
||||||
// Container(
|
|
||||||
// height: 23.rpx,
|
|
||||||
// child: SnoreWaveform(
|
|
||||||
// snoreValues: snoreValues,
|
|
||||||
// startTime: startTime,
|
|
||||||
// endTime: endTime,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class SnoreBarOverlay extends StatelessWidget {
|
|
||||||
// final List<dynamic> barData;
|
|
||||||
// final List<dynamic> showLabel;
|
|
||||||
// final int startTime;
|
|
||||||
// final int endTime;
|
|
||||||
|
|
||||||
// const SnoreBarOverlay({
|
|
||||||
// required this.barData,
|
|
||||||
// required this.showLabel,
|
|
||||||
// required this.startTime,
|
|
||||||
// required this.endTime,
|
|
||||||
// super.key,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// const double barHeight = 50;
|
|
||||||
// return SizedBox(
|
|
||||||
// height: barHeight,
|
|
||||||
// child: CustomPaint(
|
|
||||||
// size: Size(double.infinity, barHeight),
|
|
||||||
// painter: SnoreBarPainter(
|
|
||||||
// barData: barData,
|
|
||||||
// showLabel: showLabel,
|
|
||||||
// startTime: startTime,
|
|
||||||
// endTime: endTime,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class SnoreBarPainter extends CustomPainter {
|
|
||||||
// final List<dynamic> barData;
|
|
||||||
// final List<dynamic> showLabel;
|
|
||||||
// final int startTime;
|
|
||||||
// final int endTime;
|
|
||||||
|
|
||||||
// SnoreBarPainter({
|
|
||||||
// required this.barData,
|
|
||||||
// required this.showLabel,
|
|
||||||
// required this.startTime,
|
|
||||||
// required this.endTime,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// void paint(Canvas canvas, Size size) {
|
|
||||||
// final double width = size.width;
|
|
||||||
// final double height = size.height;
|
|
||||||
// final double pixelPerMs = width / (endTime - startTime);
|
|
||||||
|
|
||||||
// for (var item in barData) {
|
|
||||||
// final int st = item['st'];
|
|
||||||
// final int et = item['et'];
|
|
||||||
// final int type = item['type'];
|
|
||||||
// int heightInit = 1;
|
|
||||||
|
|
||||||
// final match = showLabel.firstWhere(
|
|
||||||
// (e) => e['type'] == type,
|
|
||||||
// orElse: () => null,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Color barColor = Colors.transparent;
|
|
||||||
// if (match != null) {
|
|
||||||
// final dynamic colorStr = match['color'];
|
|
||||||
// if (colorStr != null && colorStr.toString().isNotEmpty) {
|
|
||||||
// barColor = stringToColor(colorStr);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// final Paint barPaint = Paint()
|
|
||||||
// ..color = barColor
|
|
||||||
// ..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
// final double leftX = (st - startTime) * pixelPerMs;
|
|
||||||
// final double rightX = (et - startTime) * pixelPerMs;
|
|
||||||
// final double barWidth = rightX - leftX;
|
|
||||||
|
|
||||||
// //rem 深睡 中
|
|
||||||
// //浅睡 低
|
|
||||||
// //其他 高
|
|
||||||
// if (type == 1) {
|
|
||||||
// heightInit = 1;
|
|
||||||
// } else if (type == 2 || type == 6) {
|
|
||||||
// heightInit = 2;
|
|
||||||
// } else {
|
|
||||||
// heightInit = 3;
|
|
||||||
// }
|
|
||||||
// final double barHeight = (heightInit + 5).toDouble() * 8;
|
|
||||||
// final double top = height - barHeight;
|
|
||||||
|
|
||||||
// final rect = Rect.fromLTWH(leftX, top, barWidth, barHeight);
|
|
||||||
// canvas.drawRect(rect, barPaint);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class SnoreWaveform extends StatelessWidget {
|
|
||||||
// final List<dynamic> snoreValues;
|
|
||||||
// final int startTime;
|
|
||||||
// final int endTime;
|
|
||||||
|
|
||||||
// const SnoreWaveform({
|
|
||||||
// required this.snoreValues,
|
|
||||||
// required this.startTime,
|
|
||||||
// required this.endTime,
|
|
||||||
// super.key,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return SizedBox(
|
|
||||||
// height: 150,
|
|
||||||
// child: LayoutBuilder(
|
|
||||||
// builder: (context, constraints) {
|
|
||||||
// return CustomPaint(
|
|
||||||
// size: Size(constraints.maxWidth, constraints.maxHeight),
|
|
||||||
// painter: SnoreWaveformPainter(
|
|
||||||
// snoreValues: snoreValues,
|
|
||||||
// startTime: startTime,
|
|
||||||
// endTime: endTime,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class SnoreWaveformPainter extends CustomPainter {
|
|
||||||
// final List<dynamic> snoreValues;
|
|
||||||
// final int startTime;
|
|
||||||
// final int endTime;
|
|
||||||
|
|
||||||
// SnoreWaveformPainter({
|
|
||||||
// required this.snoreValues,
|
|
||||||
// required this.startTime,
|
|
||||||
// required this.endTime,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// void paint(Canvas canvas, Size size) {
|
|
||||||
// final double width = size.width;
|
|
||||||
// final double height = size.height;
|
|
||||||
|
|
||||||
// if (width <= 0 || height <= 0) return;
|
|
||||||
|
|
||||||
// final double totalDuration = (endTime - startTime).toDouble();
|
|
||||||
// if (totalDuration <= 0) return;
|
|
||||||
|
|
||||||
// final double pixelPerMs = width / totalDuration;
|
|
||||||
|
|
||||||
// // 过滤在时间范围内的有效事件
|
|
||||||
// final validEvents = snoreValues.where((e) {
|
|
||||||
// final int st = e['st'];
|
|
||||||
// final int et = e['et'];
|
|
||||||
// // 事件与时间范围有重叠
|
|
||||||
// return !(et <= startTime || st >= endTime);
|
|
||||||
// }).toList();
|
|
||||||
|
|
||||||
// // 计算中心线位置
|
|
||||||
// final double centerY = height / 2;
|
|
||||||
|
|
||||||
// // 统一使用一个颜色(打鼾颜色)
|
|
||||||
// final Color snoreColor = stringToColor("#8E7DEF").withOpacity(0.8);
|
|
||||||
// final Paint barPaint = Paint()
|
|
||||||
// ..color = snoreColor
|
|
||||||
// ..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
// final Paint borderPaint = Paint()
|
|
||||||
// ..color = snoreColor.withOpacity(0.9)
|
|
||||||
// ..style = PaintingStyle.stroke
|
|
||||||
// ..strokeWidth = 0.5;
|
|
||||||
|
|
||||||
// // 固定高度(上下对称)
|
|
||||||
// final double fixedBarHeight = height * 0.3; // 固定为画布高度的30%
|
|
||||||
|
|
||||||
// // 绘制每个打鼾事件(上下对称的柱状图)
|
|
||||||
// for (final event in validEvents) {
|
|
||||||
// final int st = event['st'];
|
|
||||||
// final int et = event['et'];
|
|
||||||
|
|
||||||
// // 计算绘制位置(裁剪到可视范围内)
|
|
||||||
// final double startX = (st - startTime) * pixelPerMs;
|
|
||||||
// final double endX = (et - startTime) * pixelPerMs;
|
|
||||||
|
|
||||||
// // 确保在画布范围内
|
|
||||||
// if (endX < 0 || startX > width) continue;
|
|
||||||
|
|
||||||
// final double drawStartX = startX.clamp(0, width);
|
|
||||||
// final double drawEndX = endX.clamp(0, width);
|
|
||||||
// final double drawWidth = drawEndX - drawStartX;
|
|
||||||
|
|
||||||
// if (drawWidth <= 0) continue;
|
|
||||||
|
|
||||||
// // 绘制上方的柱状图
|
|
||||||
// final double topBarTop = centerY - fixedBarHeight;
|
|
||||||
// final Rect topRect =
|
|
||||||
// Rect.fromLTWH(drawStartX, topBarTop, drawWidth, fixedBarHeight);
|
|
||||||
// canvas.drawRect(topRect, barPaint);
|
|
||||||
// canvas.drawRect(topRect, borderPaint);
|
|
||||||
|
|
||||||
// // 绘制下方的柱状图(对称)
|
|
||||||
// final double bottomBarTop = centerY;
|
|
||||||
// final Rect bottomRect =
|
|
||||||
// Rect.fromLTWH(drawStartX, bottomBarTop, drawWidth, fixedBarHeight);
|
|
||||||
// canvas.drawRect(bottomRect, barPaint);
|
|
||||||
// canvas.drawRect(bottomRect, borderPaint);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 绘制中心线
|
|
||||||
// final Paint axisPaint = Paint()
|
|
||||||
// ..color = Colors.grey.withOpacity(0.3)
|
|
||||||
// ..strokeWidth = 0.5;
|
|
||||||
// canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint);
|
|
||||||
|
|
||||||
// // 绘制时间轴标签
|
|
||||||
// final textPainter = TextPainter(
|
|
||||||
// textAlign: TextAlign.center,
|
|
||||||
// textDirection: ui.TextDirection.ltr,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// final int hourMs = 60 * 60 * 1000;
|
|
||||||
// final int totalHours = (endTime - startTime) ~/ hourMs;
|
|
||||||
|
|
||||||
// // 创建开始和结束时间的DateTime对象
|
|
||||||
// final DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime);
|
|
||||||
// final DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime);
|
|
||||||
|
|
||||||
// // 1. 始终显示开始时间
|
|
||||||
// String label = DateFormat('HH:mm').format(startDt);
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: label,
|
|
||||||
// style: TextStyle(fontSize: 10, color: Colors.grey),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas,
|
|
||||||
// Offset(0 - textPainter.width / 2, height + 2),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // 2. 决定显示策略
|
|
||||||
// if (totalHours <= 8) {
|
|
||||||
// // 小时间段:显示所有整点小时(基于实际时间)
|
|
||||||
// DateTime currentHour = DateTime(
|
|
||||||
// startDt.year,
|
|
||||||
// startDt.month,
|
|
||||||
// startDt.day,
|
|
||||||
// startDt.hour + 1, // 从下一个整点开始
|
|
||||||
// 0,
|
|
||||||
// 0,
|
|
||||||
// 0,
|
|
||||||
// 0);
|
|
||||||
|
|
||||||
// // 如果开始时间本身就是整点,需要调整
|
|
||||||
// if (startDt.minute == 0 &&
|
|
||||||
// startDt.second == 0 &&
|
|
||||||
// startDt.millisecond == 0) {
|
|
||||||
// currentHour = startDt;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// while (currentHour.millisecondsSinceEpoch < endTime) {
|
|
||||||
// int timeMs = currentHour.millisecondsSinceEpoch;
|
|
||||||
|
|
||||||
// // 确保时间在范围内
|
|
||||||
// if (timeMs > startTime && timeMs < endTime) {
|
|
||||||
// double x = (timeMs - startTime) * pixelPerMs;
|
|
||||||
|
|
||||||
// // 跳过太接近边界的时间点(30分钟内不显示)
|
|
||||||
// if (timeMs - startTime < 10 * 60 * 1000 ||
|
|
||||||
// endTime - timeMs < 10 * 60 * 1000) {
|
|
||||||
// currentHour = currentHour.add(Duration(hours: 1));
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// label = "${currentHour.hour}";
|
|
||||||
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: label,
|
|
||||||
// style: TextStyle(fontSize: 10, color: Colors.grey),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas,
|
|
||||||
// Offset(x - textPainter.width / 2, height + 2),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// currentHour = currentHour.add(Duration(hours: 1));
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// // 长时间段:使用自适应间隔
|
|
||||||
// int labelInterval = (totalHours / 6).ceil();
|
|
||||||
|
|
||||||
// // 计算第一个整点标签(对齐整点小时)
|
|
||||||
// DateTime firstLabelHour = DateTime(
|
|
||||||
// startDt.year,
|
|
||||||
// startDt.month,
|
|
||||||
// startDt.day,
|
|
||||||
// startDt.hour + (labelInterval - (startDt.hour % labelInterval)),
|
|
||||||
// 0,
|
|
||||||
// 0,
|
|
||||||
// 0,
|
|
||||||
// 0);
|
|
||||||
|
|
||||||
// // 如果第一个标签在开始时间之前,调整到下一个间隔
|
|
||||||
// if (firstLabelHour.millisecondsSinceEpoch <= startTime) {
|
|
||||||
// firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 绘制中间标签
|
|
||||||
// DateTime currentHour = firstLabelHour;
|
|
||||||
// while (currentHour.millisecondsSinceEpoch < endTime) {
|
|
||||||
// int timeMs = currentHour.millisecondsSinceEpoch;
|
|
||||||
|
|
||||||
// // 跳过太接近边界的时间点(1小时内不显示)
|
|
||||||
// if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) {
|
|
||||||
// double x = (timeMs - startTime) * pixelPerMs;
|
|
||||||
// label = "${currentHour.hour}";
|
|
||||||
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: label,
|
|
||||||
// style: TextStyle(fontSize: 10, color: Colors.grey),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas,
|
|
||||||
// Offset(x - textPainter.width / 2, height + 2),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// currentHour = currentHour.add(Duration(hours: labelInterval));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 3. 始终显示结束时间
|
|
||||||
// label = DateFormat('HH:mm').format(endDt);
|
|
||||||
// textPainter.text = TextSpan(
|
|
||||||
// text: label,
|
|
||||||
// style: TextStyle(fontSize: 10, color: Colors.grey),
|
|
||||||
// );
|
|
||||||
// textPainter.layout();
|
|
||||||
// textPainter.paint(
|
|
||||||
// canvas,
|
|
||||||
// Offset(width - textPainter.width / 2, height + 2),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// bool shouldRepaint(covariant SnoreWaveformPainter oldDelegate) {
|
|
||||||
// return oldDelegate.snoreValues != snoreValues ||
|
|
||||||
// oldDelegate.startTime != startTime ||
|
|
||||||
// oldDelegate.endTime != endTime;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
import 'package:ef/ef.dart';
|
import 'package:ef/ef.dart';
|
||||||
|
|||||||
@@ -25,63 +25,113 @@ class TimeSeriesChart extends StatelessWidget {
|
|||||||
required this.dataPoints,
|
required this.dataPoints,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
// X轴刻度数据
|
// 计算总分钟数
|
||||||
List<XLabel> _generateXLabels() {
|
double get _totalMinutes {
|
||||||
final labels = <XLabel>[];
|
return (endTime - startTime) / (1000 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成X轴刻度标签
|
||||||
|
Map<double, String> _generateXLabels() {
|
||||||
|
final labels = <double, String>{};
|
||||||
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
|
||||||
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
|
||||||
|
|
||||||
// 第一个刻度,原始 startTime,HH:mm格式
|
// 0分钟位置:起始时间
|
||||||
labels.add(XLabel(
|
labels[0.0] =
|
||||||
time: startTime,
|
'${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}';
|
||||||
label:
|
|
||||||
'${startDate.hour.toString().padLeft(2, '0')}:${startDate.minute.toString().padLeft(2, '0')}',
|
|
||||||
));
|
|
||||||
|
|
||||||
// 生成中间整点小时刻度,注意起点向上取整一个小时
|
// 计算总小时数
|
||||||
DateTime current = DateTime(
|
final int hourMs = 60 * 60 * 1000;
|
||||||
|
final int totalHours = (endTime - startTime) ~/ hourMs;
|
||||||
|
|
||||||
|
// 按照参考代码的逻辑,当小时数超过8时,跳着显示
|
||||||
|
if (totalHours <= 8) {
|
||||||
|
// 显示每个整点
|
||||||
|
DateTime currentHour = DateTime(
|
||||||
startDate.year,
|
startDate.year,
|
||||||
startDate.month,
|
startDate.month,
|
||||||
startDate.day,
|
startDate.day,
|
||||||
startDate.hour,
|
startDate.hour,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 如果起始时间不是整点,从下一个整点开始
|
||||||
if (startDate.minute > 0 ||
|
if (startDate.minute > 0 ||
|
||||||
startDate.second > 0 ||
|
startDate.second > 0 ||
|
||||||
startDate.millisecond > 0) {
|
startDate.millisecond > 0) {
|
||||||
// 如果 startTime 不是整点,跳到下一个整点小时
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
current = current.add(Duration(hours: 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (current.isBefore(endDate)) {
|
while (currentHour.millisecondsSinceEpoch < endTime) {
|
||||||
labels.add(XLabel(
|
final int timeMs = currentHour.millisecondsSinceEpoch;
|
||||||
time: current.millisecondsSinceEpoch,
|
|
||||||
label: current.hour.toString(),
|
if (timeMs > startTime && timeMs < endTime) {
|
||||||
));
|
// 检查是否太靠近边界
|
||||||
current = current.add(Duration(hours: 1));
|
if (timeMs - startTime < 10 * 60 * 1000 ||
|
||||||
|
endTime - timeMs < 10 * 60 * 1000) {
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最后一个刻度,原始 endTime,HH:mm格式
|
// 计算从起始时间到当前整点的分钟数
|
||||||
labels.add(XLabel(
|
final minutesFromStart =
|
||||||
time: endTime,
|
(currentHour.millisecondsSinceEpoch - startTime) / (1000 * 60);
|
||||||
label:
|
labels[minutesFromStart] = '${currentHour.hour}';
|
||||||
'${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}',
|
}
|
||||||
));
|
|
||||||
|
currentHour = currentHour.add(Duration(hours: 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 超过8小时,跳着显示
|
||||||
|
final int labelInterval = (totalHours / 6).ceil(); // 分成大约6个标签
|
||||||
|
|
||||||
|
DateTime firstLabelHour = DateTime(
|
||||||
|
startDate.year,
|
||||||
|
startDate.month,
|
||||||
|
startDate.day,
|
||||||
|
startDate.hour + (labelInterval - (startDate.hour % labelInterval)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确保第一个标签在开始时间之后
|
||||||
|
if (firstLabelHour.millisecondsSinceEpoch <= startTime) {
|
||||||
|
firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime currentHour = firstLabelHour;
|
||||||
|
while (currentHour.millisecondsSinceEpoch < endTime) {
|
||||||
|
final int timeMs = currentHour.millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
// 确保标签离边界足够远
|
||||||
|
if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) {
|
||||||
|
final minutesFromStart =
|
||||||
|
(currentHour.millisecondsSinceEpoch - startTime) / (1000 * 60);
|
||||||
|
labels[minutesFromStart] = '${currentHour.hour}';
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHour = currentHour.add(Duration(hours: labelInterval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后位置:结束时间
|
||||||
|
labels[_totalMinutes] =
|
||||||
|
'${endDate.hour.toString().padLeft(2, '0')}:${endDate.minute.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间戳映射到0~(labels.length-1)之间
|
// 时间戳映射到X坐标(分钟数)
|
||||||
double _timeToX(double timestamp, List<XLabel> labels) {
|
double _timeToX(double timestamp) {
|
||||||
int start = labels.first.time;
|
final minutesFromStart = (timestamp - startTime) / (1000 * 60);
|
||||||
int end = labels.last.time;
|
return minutesFromStart.clamp(0.0, _totalMinutes);
|
||||||
double total = (end - start).toDouble();
|
|
||||||
double pos = (timestamp - start).clamp(0, total).toDouble();
|
|
||||||
return pos / total * (labels.length - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final xLabels = _generateXLabels();
|
final xLabels = _generateXLabels();
|
||||||
|
final labelPositions = xLabels.keys.toList()..sort();
|
||||||
final midY = (yMin + yMax) / 2;
|
final midY = (yMin + yMax) / 2;
|
||||||
|
|
||||||
// 将数据点分割成多个连续段,遇到value=-1时断开
|
// 将数据点分割成多个连续段,遇到value=-1时断开
|
||||||
@@ -92,7 +142,7 @@ class TimeSeriesChart extends StatelessWidget {
|
|||||||
if (point.value != -1) {
|
if (point.value != -1) {
|
||||||
// 有效数据点,添加到当前段
|
// 有效数据点,添加到当前段
|
||||||
currentSegment.add(FlSpot(
|
currentSegment.add(FlSpot(
|
||||||
_timeToX(point.timestamp.toDouble(), xLabels),
|
_timeToX(point.timestamp.toDouble()),
|
||||||
point.value,
|
point.value,
|
||||||
));
|
));
|
||||||
} else if (currentSegment.isNotEmpty) {
|
} else if (currentSegment.isNotEmpty) {
|
||||||
@@ -112,7 +162,7 @@ class TimeSeriesChart extends StatelessWidget {
|
|||||||
child: LineChart(
|
child: LineChart(
|
||||||
LineChartData(
|
LineChartData(
|
||||||
minX: 0,
|
minX: 0,
|
||||||
maxX: (xLabels.length - 1).toDouble(),
|
maxX: _totalMinutes,
|
||||||
minY: yMin < 0 ? yMin : 0,
|
minY: yMin < 0 ? yMin : 0,
|
||||||
maxY: yMax,
|
maxY: yMax,
|
||||||
gridData: FlGridData(show: false),
|
gridData: FlGridData(show: false),
|
||||||
@@ -144,37 +194,26 @@ class TimeSeriesChart extends StatelessWidget {
|
|||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
reservedSize: 30,
|
reservedSize: 30,
|
||||||
interval: 1,
|
interval: 1, // 现在每个单位是1分钟
|
||||||
getTitlesWidget: (value, meta) {
|
getTitlesWidget: (value, meta) {
|
||||||
int index = value.toInt();
|
// 四舍五入到最接近的整数
|
||||||
if (index < 0 || index >= xLabels.length)
|
final roundedValue = value.roundToDouble();
|
||||||
return const SizedBox.shrink();
|
|
||||||
|
|
||||||
final dateTime =
|
// 检查是否在标签位置
|
||||||
DateTime.fromMillisecondsSinceEpoch(xLabels[index].time);
|
for (var position in labelPositions) {
|
||||||
|
if ((position - roundedValue).abs() < 0.5) {
|
||||||
if (index == 0 || index == xLabels.length - 1) {
|
final label = xLabels[position] ?? '';
|
||||||
// 开始和结束显示 HH:mm
|
|
||||||
final formatted =
|
|
||||||
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(formatted,
|
child: Text(label,
|
||||||
style: TextStyle(
|
|
||||||
color: themeController.currentColor.sc4,
|
|
||||||
fontSize: 16.rpx)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 中间显示小时H,24小时制,不补零
|
|
||||||
final formatted = '${dateTime.hour}';
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(formatted,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: themeController.currentColor.sc4,
|
color: themeController.currentColor.sc4,
|
||||||
fontSize: 16.rpx)),
|
fontSize: 16.rpx)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -238,9 +277,3 @@ class TimeSeriesChart extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class XLabel {
|
|
||||||
final int time;
|
|
||||||
final String label;
|
|
||||||
XLabel({required this.time, required this.label});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class _AIAdviceWidgetState extends State<AIAdviceWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("AI分析绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class _BreathPauseWidgetState extends State<BreathPauseWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("呼吸暂停监测绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class _SnoreViewWidgetWidgetState extends State<BreathePauseNewWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("呼吸监测绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ class _BreatheStandardWidgetState extends State<BreatheStandardWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("呼吸基准监测绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class _DiseasePercentsWidgetState extends State<DiseasePercentsWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("疾病绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ class _HeartChangeWidgetState extends State<HeartChangeWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("心率变化绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class _HeartHealthWidgetState extends State<HeartHealthWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("心理健康绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ class _HeartPointWidgetState extends State<HeartPointWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("心率点绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class _HrvWidgetState extends State<HrvWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("hrv绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class _SkinPercentWidgetState extends State<SkinPercentWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("皮肤检测绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ class _SleepScoreWidgetState extends State<SleepScoreWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("睡眠评分绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ class _NewSleepViewWidgetState extends State<NewSleepViewWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
es.EasyDartModule.logger.error("打鼾监测绘制异常${e}");
|
es.EasyDartModule.logger.error("睡眠规律性绘制异常${e}");
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user