智慧水务管理系统 - 精河县供水工程综合管理平台

water_monitoring_page.dart 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import 'package:flutter/material.dart';
  2. import 'package:water_management_system/models/water_data_model.dart';
  3. import 'package:water_management_system/services/water_service.dart';
  4. import 'package:water_management_system/utils/constants.dart';
  5. import 'package:water_management_system/widgets/custom_card.dart';
  6. import 'package:water_management_system/widgets/custom_button.dart';
  7. import 'package:fl_chart/fl_chart.dart';
  8. import 'package:intl/intl.dart';
  9. class WaterMonitoringPage extends StatefulWidget {
  10. const WaterMonitoringPage({super.key});
  11. @override
  12. State<WaterMonitoringPage> createState() => _WaterMonitoringPageState();
  13. }
  14. class _WaterMonitoringPageState extends State<WaterMonitoringPage> {
  15. final WaterService _waterService = WaterService();
  16. bool _isLoading = true;
  17. List<WaterDataModel> _waterData = [];
  18. String _selectedArea = '全部区域';
  19. DateTime _selectedDate = DateTime.now();
  20. @override
  21. void initState() {
  22. super.initState();
  23. _loadWaterData();
  24. }
  25. Future<void> _loadWaterData() async {
  26. setState(() {
  27. _isLoading = true;
  28. });
  29. try {
  30. final data = await _waterService.getWaterData(
  31. area: _selectedArea,
  32. date: _selectedDate,
  33. );
  34. setState(() {
  35. _waterData = data;
  36. _isLoading = false;
  37. });
  38. } catch (e) {
  39. setState(() {
  40. _isLoading = false;
  41. });
  42. if (mounted) {
  43. ScaffoldMessenger.of(context).showSnackBar(
  44. SnackBar(
  45. content: Text('加载供水数据失败: $e'),
  46. backgroundColor: AppConstants.errorColor,
  47. ),
  48. );
  49. }
  50. }
  51. }
  52. @override
  53. Widget build(BuildContext context) {
  54. return RefreshIndicator(
  55. onRefresh: _loadWaterData,
  56. child: Scaffold(
  57. body: _isLoading
  58. ? const Center(child: CircularProgressIndicator())
  59. : Column(
  60. children: [
  61. // 搜索和筛选区域
  62. _buildFilterSection(),
  63. // 统计卡片
  64. _buildStatisticsCards(),
  65. // 图表区域
  66. _buildChartSection(),
  67. // 数据列表
  68. Expanded(
  69. child: _buildWaterDataList(),
  70. ),
  71. ],
  72. ),
  73. );
  74. }
  75. Widget _buildFilterSection() {
  76. return Container(
  77. padding: const EdgeInsets.all(16),
  78. decoration: BoxDecoration(
  79. color: Colors.white,
  80. boxShadow: [
  81. BoxShadow(
  82. color: Colors.grey.withOpacity(0.1),
  83. blurRadius: 4,
  84. offset: const Offset(0, 2),
  85. ),
  86. ],
  87. ),
  88. child: Column(
  89. children: [
  90. Row(
  91. children: [
  92. Expanded(
  93. child: DropdownButtonFormField<String>(
  94. value: _selectedArea,
  95. decoration: const InputDecoration(
  96. labelText: '区域选择',
  97. border: OutlineInputBorder(),
  98. ),
  99. items: const [
  100. DropdownMenuItem(value: '全部区域', child: Text('全部区域')),
  101. DropdownMenuItem(value: '东区', child: Text('东区')),
  102. DropdownMenuItem(value: '西区', child: Text('西区')),
  103. DropdownMenuItem(value: '南区', child: Text('南区')),
  104. DropdownMenuItem(value: '北区', child: Text('北区')),
  105. ],
  106. onChanged: (value) {
  107. setState(() {
  108. _selectedArea = value!;
  109. });
  110. },
  111. ),
  112. ),
  113. const SizedBox(width: 16),
  114. Expanded(
  115. child: InkWell(
  116. onTap: _showDatePicker,
  117. child: Container(
  118. padding: const EdgeInsets.symmetric(
  119. horizontal: 16,
  120. vertical: 12,
  121. ),
  122. decoration: BoxDecoration(
  123. border: Border.all(color: Colors.grey[300]!),
  124. borderRadius: BorderRadius.circular(8),
  125. ),
  126. child: Row(
  127. children: [
  128. const Icon(Icons.calendar_today),
  129. const SizedBox(width: 8),
  130. Text(
  131. DateFormat('yyyy-MM-dd').format(_selectedDate),
  132. style: const TextStyle(fontSize: 14),
  133. ),
  134. ],
  135. ),
  136. ),
  137. ),
  138. ),
  139. const SizedBox(width: 16),
  140. CustomButton(
  141. text: '查询',
  142. width: 80,
  143. onPressed: _loadWaterData,
  144. ),
  145. ],
  146. ),
  147. ],
  148. ),
  149. );
  150. }
  151. Widget _buildStatisticsCards() {
  152. final totalDevices = _waterData.length;
  153. final normalDevices = _waterData.where((data) => data.status == 'normal').length;
  154. const warningDevices = 0; // 假设暂无警告
  155. const errorDevices = 0; // 假设暂无错误
  156. return Container(
  157. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  158. child: Row(
  159. children: [
  160. Expanded(
  161. child: CustomCard(
  162. title: '设备总数',
  163. value: totalDevices.toString(),
  164. color: AppConstants.primaryColor,
  165. icon: Icons.device_thermostat,
  166. ),
  167. ),
  168. const SizedBox(width: 12),
  169. Expanded(
  170. child: CustomCard(
  171. title: '正常设备',
  172. value: normalDevices.toString(),
  173. color: AppConstants.successColor,
  174. icon: Icons.check_circle,
  175. ),
  176. ),
  177. const SizedBox(width: 12),
  178. Expanded(
  179. child: CustomCard(
  180. title: '警告设备',
  181. value: warningDevices.toString(),
  182. color: AppConstants.warningColor,
  183. icon: Icons.warning,
  184. ),
  185. ),
  186. const SizedBox(width: 12),
  187. Expanded(
  188. child: CustomCard(
  189. title: '故障设备',
  190. value: errorDevices.toString(),
  191. color: AppConstants.errorColor,
  192. icon: Icons.error,
  193. ),
  194. ),
  195. ],
  196. ),
  197. );
  198. }
  199. Widget _buildChartSection() {
  200. if (_waterData.isEmpty) {
  201. return const SizedBox.shrink();
  202. }
  203. // 准备图表数据
  204. final pressureData = _waterData.map((data) => data.pressure).toList();
  205. final flowData = _waterData.map((data) => data.flowRate).toList();
  206. return Container(
  207. height: 200,
  208. margin: const EdgeInsets.all(16),
  209. padding: const EdgeInsets.all(16),
  210. decoration: BoxDecoration(
  211. color: Colors.white,
  212. borderRadius: BorderRadius.circular(8),
  213. boxShadow: [
  214. BoxShadow(
  215. color: Colors.grey.withOpacity(0.1),
  216. blurRadius: 4,
  217. offset: const Offset(0, 2),
  218. ),
  219. ],
  220. ),
  221. child: Column(
  222. children: [
  223. const Row(
  224. children: [
  225. Icon(Icons.show_chart),
  226. SizedBox(width: 8),
  227. Text('压力和流量趋势'),
  228. ],
  229. ),
  230. const SizedBox(height: 16),
  231. Expanded(
  232. child: LineChart(
  233. LineChartData(
  234. gridData: const FlGridData(show: false),
  235. titlesData: const FlTitlesData(show: false),
  236. borderData: FlBorderData(show: false),
  237. minX: 0,
  238. maxX: _waterData.length.toDouble() - 1,
  239. minY: 0,
  240. maxY: (_waterData.fold(0, (max, data) =>
  241. data.pressure > max ? data.pressure : max) * 1.2).toDouble(),
  242. lineBarsData: [
  243. LineChartBarData(
  244. spots: List.generate(_waterData.length, (index) {
  245. return FlSpot(
  246. index.toDouble(),
  247. _waterData[index].pressure.toDouble(),
  248. );
  249. }),
  250. isCurved: true,
  251. color: AppConstants.primaryColor,
  252. barWidth: 2,
  253. isStrokeCapRound: true,
  254. dotData: const FlDotData(show: false),
  255. ),
  256. LineChartBarData(
  257. spots: List.generate(_waterData.length, (index) {
  258. return FlSpot(
  259. index.toDouble(),
  260. _waterData[index].flowRate.toDouble(),
  261. );
  262. }),
  263. isCurved: true,
  264. color: AppConstants.accentColor,
  265. barWidth: 2,
  266. isStrokeCapRound: true,
  267. dotData: const FlDotData(show: false),
  268. ),
  269. ],
  270. ),
  271. ),
  272. ),
  273. ],
  274. ),
  275. );
  276. }
  277. Widget _buildWaterDataList() {
  278. if (_waterData.isEmpty) {
  279. return const Center(
  280. child: Column(
  281. mainAxisAlignment: MainAxisAlignment.center,
  282. children: [
  283. Icon(Icons.water_drop, size: 64, color: Colors.grey),
  284. SizedBox(height: 16),
  285. Text('暂无供水数据', style: TextStyle(color: Colors.grey)),
  286. ],
  287. ),
  288. );
  289. }
  290. return ListView.separated(
  291. padding: const EdgeInsets.all(16),
  292. itemCount: _waterData.length,
  293. separatorBuilder: (context, index) => const Divider(height: 1),
  294. itemBuilder: (context, index) {
  295. final data = _waterData[index];
  296. return WaterDataCard(data: data);
  297. },
  298. );
  299. }
  300. void _showDatePicker() {
  301. showDatePicker(
  302. context: context,
  303. initialDate: _selectedDate,
  304. firstDate: DateTime(2024, 1, 1),
  305. lastDate: DateTime.now(),
  306. ).then((date) {
  307. if (date != null) {
  308. setState(() {
  309. _selectedDate = date;
  310. });
  311. }
  312. });
  313. }
  314. }
  315. class WaterDataCard extends StatelessWidget {
  316. final WaterDataModel data;
  317. const WaterDataCard({
  318. super.key,
  319. required this.data,
  320. });
  321. @override
  322. Widget build(BuildContext context) {
  323. Color statusColor;
  324. String statusText;
  325. IconData statusIcon;
  326. switch (data.status) {
  327. case 'normal':
  328. statusColor = AppConstants.successColor;
  329. statusText = '正常';
  330. statusIcon = Icons.check_circle;
  331. break;
  332. case 'warning':
  333. statusColor = AppConstants.warningColor;
  334. statusText = '警告';
  335. statusIcon = Icons.warning;
  336. break;
  337. case 'error':
  338. statusColor = AppConstants.errorColor;
  339. statusText = '故障';
  340. statusIcon = Icons.error;
  341. break;
  342. default:
  343. statusColor = Colors.grey;
  344. statusText = '未知';
  345. statusIcon = Icons.help;
  346. }
  347. return CustomCard(
  348. title: '${data.area} - ${data.deviceName}',
  349. subtitle: '设备ID: ${data.deviceId}',
  350. trailing: Row(
  351. mainAxisSize: MainAxisSize.min,
  352. children: [
  353. Icon(statusIcon, color: statusColor),
  354. const SizedBox(width: 4),
  355. Text(
  356. statusText,
  357. style: TextStyle(
  358. color: statusColor,
  359. fontWeight: FontWeight.bold,
  360. ),
  361. ),
  362. ],
  363. ),
  364. children: [
  365. _buildDataRow('压力', '${data.pressure} MPa'),
  366. _buildDataRow('流量', '${data.flowRate} m³/h'),
  367. _buildDataRow('温度', '${data.temperature} °C'),
  368. _buildDataRow('更新时间', DateFormat('yyyy-MM-dd HH:mm:ss').format(data.updateTime)),
  369. ],
  370. );
  371. }
  372. Widget _buildDataRow(String label, String value) {
  373. return Padding(
  374. padding: const EdgeInsets.symmetric(vertical: 4),
  375. child: Row(
  376. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  377. children: [
  378. Text(label, style: const TextStyle(color: Colors.grey)),
  379. Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
  380. ],
  381. ),
  382. );
  383. }
  384. }