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

quality_page.dart 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import 'package:flutter/material.dart';
  2. import 'package:intl/intl.dart';
  3. import '../../services/water_service.dart';
  4. /// 水质查看页面
  5. class QualityPage extends StatefulWidget {
  6. const QualityPage({super.key});
  7. @override
  8. State<QualityPage> createState() => _QualityPageState();
  9. }
  10. class _QualityPageState extends State<QualityPage> with SingleTickerProviderStateMixin {
  11. final WaterService _service = WaterService.instance;
  12. List<QualitySample> _samples = [];
  13. bool _isLoading = true;
  14. String? _error;
  15. late TabController _tabController;
  16. static const _categories = [
  17. QualityCategory.rawWater,
  18. QualityCategory.factoryWater,
  19. QualityCategory.endWater,
  20. ];
  21. @override
  22. void initState() {
  23. super.initState();
  24. _tabController = TabController(length: _categories.length, vsync: this);
  25. _tabController.addListener(() {
  26. if (!_tabController.indexIsChanging) {
  27. setState(() {}); // trigger rebuild for filtered data
  28. }
  29. });
  30. _loadData();
  31. }
  32. @override
  33. void dispose() {
  34. _tabController.dispose();
  35. super.dispose();
  36. }
  37. Future<void> _loadData() async {
  38. setState(() {
  39. _isLoading = true;
  40. _error = null;
  41. });
  42. try {
  43. final data = await _service.getQualityData();
  44. if (mounted) {
  45. setState(() {
  46. _samples = data;
  47. _isLoading = false;
  48. });
  49. }
  50. } catch (e) {
  51. if (mounted) {
  52. setState(() {
  53. _error = e.toString();
  54. _isLoading = false;
  55. });
  56. }
  57. }
  58. }
  59. List<QualitySample> get _currentSamples {
  60. final category = _categories[_tabController.index];
  61. return _samples.where((s) => s.category == category).toList();
  62. }
  63. @override
  64. Widget build(BuildContext context) {
  65. final theme = Theme.of(context);
  66. return Scaffold(
  67. appBar: AppBar(
  68. title: const Text('水质查看'),
  69. centerTitle: true,
  70. actions: [
  71. IconButton(
  72. icon: const Icon(Icons.refresh),
  73. onPressed: _isLoading ? null : _loadData,
  74. ),
  75. ],
  76. bottom: TabBar(
  77. controller: _tabController,
  78. tabs: _categories.map((c) => Tab(text: c.label)).toList(),
  79. ),
  80. ),
  81. body: _buildBody(theme),
  82. );
  83. }
  84. Widget _buildBody(ThemeData theme) {
  85. if (_isLoading) {
  86. return const Center(child: CircularProgressIndicator());
  87. }
  88. if (_error != null) {
  89. return Center(
  90. child: Column(
  91. mainAxisSize: MainAxisSize.min,
  92. children: [
  93. Icon(Icons.error_outline, size: 48, color: theme.colorScheme.error),
  94. const SizedBox(height: 16),
  95. Text('加载失败', style: theme.textTheme.titleMedium),
  96. const SizedBox(height: 16),
  97. FilledButton.icon(
  98. onPressed: _loadData,
  99. icon: const Icon(Icons.refresh),
  100. label: const Text('重试'),
  101. ),
  102. ],
  103. ),
  104. );
  105. }
  106. final samples = _currentSamples;
  107. if (samples.isEmpty) {
  108. return Center(
  109. child: Column(
  110. mainAxisSize: MainAxisSize.min,
  111. children: [
  112. Icon(Icons.science_outlined, size: 48, color: Colors.grey.shade400),
  113. const SizedBox(height: 16),
  114. Text('暂无水质数据', style: theme.textTheme.titleMedium),
  115. ],
  116. ),
  117. );
  118. }
  119. return RefreshIndicator(
  120. onRefresh: _loadData,
  121. child: ListView.builder(
  122. padding: const EdgeInsets.all(12),
  123. itemCount: samples.length,
  124. itemBuilder: (context, index) => _QualityCard(sample: samples[index]),
  125. ),
  126. );
  127. }
  128. }
  129. /// 水质数据卡片
  130. class _QualityCard extends StatelessWidget {
  131. final QualitySample sample;
  132. const _QualityCard({required this.sample});
  133. @override
  134. Widget build(BuildContext context) {
  135. final theme = Theme.of(context);
  136. final dateFormat = DateFormat('MM-dd HH:mm');
  137. final indicators = sample.getIndicators();
  138. final allCompliant = sample.isCompliant;
  139. final statusColor = allCompliant ? Colors.green : Colors.red;
  140. return Card(
  141. margin: const EdgeInsets.only(bottom: 12),
  142. elevation: 0,
  143. shape: RoundedRectangleBorder(
  144. borderRadius: BorderRadius.circular(12),
  145. side: BorderSide(
  146. color: allCompliant ? Colors.grey.shade200 : Colors.red.withAlpha(100),
  147. ),
  148. ),
  149. child: Padding(
  150. padding: const EdgeInsets.all(16),
  151. child: Column(
  152. crossAxisAlignment: CrossAxisAlignment.start,
  153. children: [
  154. // 标题行
  155. Row(
  156. children: [
  157. Icon(
  158. allCompliant ? Icons.check_circle : Icons.error,
  159. size: 20,
  160. color: statusColor,
  161. ),
  162. const SizedBox(width: 8),
  163. Expanded(
  164. child: Text(
  165. sample.source,
  166. style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
  167. ),
  168. ),
  169. Container(
  170. padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
  171. decoration: BoxDecoration(
  172. color: statusColor.withAlpha(25),
  173. borderRadius: BorderRadius.circular(6),
  174. ),
  175. child: Text(
  176. allCompliant ? '达标' : '不达标',
  177. style: TextStyle(
  178. fontSize: 12,
  179. color: statusColor,
  180. fontWeight: FontWeight.w600,
  181. ),
  182. ),
  183. ),
  184. ],
  185. ),
  186. const SizedBox(height: 6),
  187. // 采样时间
  188. Row(
  189. children: [
  190. Icon(Icons.access_time, size: 12, color: Colors.grey.shade500),
  191. const SizedBox(width: 4),
  192. Text(
  193. '采样时间: ${dateFormat.format(sample.sampleTime)}',
  194. style: TextStyle(fontSize: 11, color: Colors.grey.shade500),
  195. ),
  196. ],
  197. ),
  198. const SizedBox(height: 14),
  199. // 指标表格
  200. Container(
  201. decoration: BoxDecoration(
  202. color: Colors.grey.shade50,
  203. borderRadius: BorderRadius.circular(8),
  204. ),
  205. child: Column(
  206. children: [
  207. // 表头
  208. Container(
  209. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  210. decoration: BoxDecoration(
  211. color: Colors.grey.shade100,
  212. borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
  213. ),
  214. child: Row(
  215. children: [
  216. _TableHeaderCell(text: '检测指标', flex: 2),
  217. _TableHeaderCell(text: '检测值', flex: 2),
  218. _TableHeaderCell(text: '标准值', flex: 2),
  219. _TableHeaderCell(text: '结果', flex: 1),
  220. ],
  221. ),
  222. ),
  223. // 数据行
  224. ...indicators.map((indicator) => Container(
  225. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  226. decoration: BoxDecoration(
  227. border: Border(
  228. bottom: BorderSide(color: Colors.grey.shade200, width: 0.5),
  229. ),
  230. ),
  231. child: Row(
  232. children: [
  233. Expanded(
  234. flex: 2,
  235. child: Text(
  236. indicator.name,
  237. style: const TextStyle(fontSize: 12),
  238. ),
  239. ),
  240. Expanded(
  241. flex: 2,
  242. child: Text(
  243. indicator.value,
  244. style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
  245. ),
  246. ),
  247. Expanded(
  248. flex: 2,
  249. child: Text(
  250. indicator.standard,
  251. style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
  252. ),
  253. ),
  254. Expanded(
  255. flex: 1,
  256. child: Icon(
  257. indicator.isCompliant ? Icons.check : Icons.close,
  258. size: 16,
  259. color: indicator.isCompliant ? Colors.green : Colors.red,
  260. ),
  261. ),
  262. ],
  263. ),
  264. )),
  265. ],
  266. ),
  267. ),
  268. // 不达标提示
  269. if (!allCompliant) ...[
  270. const SizedBox(height: 12),
  271. Container(
  272. padding: const EdgeInsets.all(10),
  273. decoration: BoxDecoration(
  274. color: Colors.red.shade50,
  275. borderRadius: BorderRadius.circular(8),
  276. border: Border.all(color: Colors.red.shade200),
  277. ),
  278. child: Row(
  279. children: [
  280. Icon(Icons.warning, size: 16, color: Colors.red.shade700),
  281. const SizedBox(width: 8),
  282. Expanded(
  283. child: Text(
  284. '部分指标超标,请关注并及时处理',
  285. style: TextStyle(fontSize: 12, color: Colors.red.shade700),
  286. ),
  287. ),
  288. ],
  289. ),
  290. ),
  291. ],
  292. ],
  293. ),
  294. ),
  295. );
  296. }
  297. }
  298. class _TableHeaderCell extends StatelessWidget {
  299. final String text;
  300. final int flex;
  301. const _TableHeaderCell({required this.text, required this.flex});
  302. @override
  303. Widget build(BuildContext context) {
  304. return Expanded(
  305. flex: flex,
  306. child: Text(
  307. text,
  308. style: TextStyle(fontSize: 11, color: Colors.grey.shade700, fontWeight: FontWeight.w600),
  309. ),
  310. );
  311. }
  312. }