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

dispatch_page.dart 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import 'package:flutter/material.dart';
  2. import 'package:intl/intl.dart';
  3. import '../../services/water_service.dart';
  4. /// 今日值班页面
  5. class DispatchPage extends StatefulWidget {
  6. const DispatchPage({super.key});
  7. @override
  8. State<DispatchPage> createState() => _DispatchPageState();
  9. }
  10. class _DispatchPageState extends State<DispatchPage> {
  11. final WaterService _service = WaterService.instance;
  12. DutyInfo? _dutyInfo;
  13. bool _isLoading = true;
  14. String? _error;
  15. @override
  16. void initState() {
  17. super.initState();
  18. _loadData();
  19. }
  20. Future<void> _loadData() async {
  21. setState(() {
  22. _isLoading = true;
  23. _error = null;
  24. });
  25. try {
  26. final data = await _service.getTodayDuty();
  27. if (mounted) {
  28. setState(() {
  29. _dutyInfo = data;
  30. _isLoading = false;
  31. });
  32. }
  33. } catch (e) {
  34. if (mounted) {
  35. setState(() {
  36. _error = e.toString();
  37. _isLoading = false;
  38. });
  39. }
  40. }
  41. }
  42. @override
  43. Widget build(BuildContext context) {
  44. final theme = Theme.of(context);
  45. return Scaffold(
  46. appBar: AppBar(
  47. title: const Text('今日值班'),
  48. centerTitle: true,
  49. actions: [
  50. IconButton(
  51. icon: const Icon(Icons.refresh),
  52. onPressed: _isLoading ? null : _loadData,
  53. ),
  54. ],
  55. ),
  56. body: _buildBody(theme),
  57. );
  58. }
  59. Widget _buildBody(ThemeData theme) {
  60. if (_isLoading) {
  61. return const Center(child: CircularProgressIndicator());
  62. }
  63. if (_error != null) {
  64. return Center(
  65. child: Column(
  66. mainAxisSize: MainAxisSize.min,
  67. children: [
  68. Icon(Icons.error_outline, size: 48, color: theme.colorScheme.error),
  69. const SizedBox(height: 16),
  70. Text('加载失败', style: theme.textTheme.titleMedium),
  71. const SizedBox(height: 16),
  72. FilledButton.icon(
  73. onPressed: _loadData,
  74. icon: const Icon(Icons.refresh),
  75. label: const Text('重试'),
  76. ),
  77. ],
  78. ),
  79. );
  80. }
  81. final info = _dutyInfo!;
  82. final dateFormat = DateFormat('yyyy年MM月dd日');
  83. return RefreshIndicator(
  84. onRefresh: _loadData,
  85. child: ListView(
  86. padding: const EdgeInsets.all(16),
  87. children: [
  88. // 班次概览卡片
  89. _ShiftOverviewCard(
  90. date: dateFormat.format(info.date),
  91. shiftName: info.shiftName,
  92. shiftTime: info.shiftTime,
  93. memberCount: info.members.length + 1,
  94. instructionCount: info.instructions.length,
  95. ),
  96. const SizedBox(height: 20),
  97. // 值班长
  98. _SectionHeader(title: '值班长', icon: Icons.star),
  99. const SizedBox(height: 8),
  100. _PersonCard(
  101. person: info.leader,
  102. isLeader: true,
  103. ),
  104. const SizedBox(height: 20),
  105. // 值班人员
  106. _SectionHeader(title: '值班人员', icon: Icons.people),
  107. const SizedBox(height: 8),
  108. ...info.members.map((member) => Padding(
  109. padding: const EdgeInsets.only(bottom: 8),
  110. child: _PersonCard(person: member),
  111. )),
  112. const SizedBox(height: 20),
  113. // 调度指令台账
  114. _SectionHeader(
  115. title: '调度指令台账',
  116. icon: Icons.assignment,
  117. trailing: Text(
  118. '${info.instructions.length} 条',
  119. style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
  120. ),
  121. ),
  122. const SizedBox(height: 8),
  123. ...info.instructions.map((instruction) => Padding(
  124. padding: const EdgeInsets.only(bottom: 8),
  125. child: _InstructionCard(instruction: instruction),
  126. )),
  127. const SizedBox(height: 16),
  128. ],
  129. ),
  130. );
  131. }
  132. }
  133. /// 班次概览卡片
  134. class _ShiftOverviewCard extends StatelessWidget {
  135. final String date;
  136. final String shiftName;
  137. final String shiftTime;
  138. final int memberCount;
  139. final int instructionCount;
  140. const _ShiftOverviewCard({
  141. required this.date,
  142. required this.shiftName,
  143. required this.shiftTime,
  144. required this.memberCount,
  145. required this.instructionCount,
  146. });
  147. @override
  148. Widget build(BuildContext context) {
  149. final theme = Theme.of(context);
  150. final primary = theme.colorScheme.primary;
  151. return Card(
  152. elevation: 0,
  153. color: theme.colorScheme.primaryContainer.withAlpha(80),
  154. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
  155. child: Padding(
  156. padding: const EdgeInsets.all(20),
  157. child: Column(
  158. children: [
  159. Row(
  160. mainAxisAlignment: MainAxisAlignment.center,
  161. children: [
  162. Icon(Icons.calendar_today, size: 18, color: primary),
  163. const SizedBox(width: 8),
  164. Text(
  165. date,
  166. style: TextStyle(
  167. fontSize: 16,
  168. fontWeight: FontWeight.w600,
  169. color: primary,
  170. ),
  171. ),
  172. const SizedBox(width: 12),
  173. Container(
  174. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
  175. decoration: BoxDecoration(
  176. color: primary.withAlpha(30),
  177. borderRadius: BorderRadius.circular(12),
  178. ),
  179. child: Text(
  180. shiftName,
  181. style: TextStyle(fontSize: 12, color: primary, fontWeight: FontWeight.w600),
  182. ),
  183. ),
  184. ],
  185. ),
  186. const SizedBox(height: 12),
  187. Row(
  188. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  189. children: [
  190. _InfoColumn(icon: Icons.schedule, label: '值班时间', value: shiftTime),
  191. Container(width: 1, height: 40, color: Colors.grey.shade300),
  192. _InfoColumn(icon: Icons.people, label: '值班人数', value: '$memberCount 人'),
  193. Container(width: 1, height: 40, color: Colors.grey.shade300),
  194. _InfoColumn(icon: Icons.assignment, label: '调度指令', value: '$instructionCount 条'),
  195. ],
  196. ),
  197. ],
  198. ),
  199. ),
  200. );
  201. }
  202. }
  203. class _InfoColumn extends StatelessWidget {
  204. final IconData icon;
  205. final String label;
  206. final String value;
  207. const _InfoColumn({required this.icon, required this.label, required this.value});
  208. @override
  209. Widget build(BuildContext context) {
  210. return Column(
  211. children: [
  212. Icon(icon, size: 20, color: Colors.grey.shade600),
  213. const SizedBox(height: 4),
  214. Text(label, style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
  215. const SizedBox(height: 2),
  216. Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
  217. ],
  218. );
  219. }
  220. }
  221. /// 段落标题
  222. class _SectionHeader extends StatelessWidget {
  223. final String title;
  224. final IconData icon;
  225. final Widget? trailing;
  226. const _SectionHeader({required this.title, required this.icon, this.trailing});
  227. @override
  228. Widget build(BuildContext context) {
  229. final theme = Theme.of(context);
  230. return Row(
  231. children: [
  232. Icon(icon, size: 20, color: theme.colorScheme.primary),
  233. const SizedBox(width: 8),
  234. Text(title, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)),
  235. const Spacer(),
  236. if (trailing != null) trailing!,
  237. ],
  238. );
  239. }
  240. }
  241. /// 值班人员卡片
  242. class _PersonCard extends StatelessWidget {
  243. final DutyPerson person;
  244. final bool isLeader;
  245. const _PersonCard({required this.person, this.isLeader = false});
  246. @override
  247. Widget build(BuildContext context) {
  248. final theme = Theme.of(context);
  249. return Card(
  250. elevation: 0,
  251. shape: RoundedRectangleBorder(
  252. borderRadius: BorderRadius.circular(12),
  253. side: BorderSide(
  254. color: isLeader ? theme.colorScheme.primary.withAlpha(80) : Colors.grey.shade200,
  255. ),
  256. ),
  257. child: Padding(
  258. padding: const EdgeInsets.all(14),
  259. child: Row(
  260. children: [
  261. CircleAvatar(
  262. radius: 22,
  263. backgroundColor: isLeader
  264. ? theme.colorScheme.primary.withAlpha(30)
  265. : Colors.grey.shade100,
  266. child: Text(
  267. person.name.substring(0, 1),
  268. style: TextStyle(
  269. fontSize: 16,
  270. fontWeight: FontWeight.w600,
  271. color: isLeader ? theme.colorScheme.primary : Colors.grey.shade700,
  272. ),
  273. ),
  274. ),
  275. const SizedBox(width: 12),
  276. Expanded(
  277. child: Column(
  278. crossAxisAlignment: CrossAxisAlignment.start,
  279. children: [
  280. Row(
  281. children: [
  282. Text(
  283. person.name,
  284. style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
  285. ),
  286. if (isLeader) ...[
  287. const SizedBox(width: 6),
  288. Container(
  289. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
  290. decoration: BoxDecoration(
  291. color: Colors.amber.shade100,
  292. borderRadius: BorderRadius.circular(4),
  293. ),
  294. child: Text(
  295. '值班长',
  296. style: TextStyle(fontSize: 10, color: Colors.amber.shade800),
  297. ),
  298. ),
  299. ],
  300. ],
  301. ),
  302. const SizedBox(height: 4),
  303. Text(
  304. person.role,
  305. style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
  306. ),
  307. ],
  308. ),
  309. ),
  310. // 拨打电话按钮
  311. Container(
  312. decoration: BoxDecoration(
  313. color: Colors.green.shade50,
  314. borderRadius: BorderRadius.circular(8),
  315. ),
  316. child: IconButton(
  317. icon: Icon(Icons.phone, size: 20, color: Colors.green.shade700),
  318. onPressed: () {
  319. ScaffoldMessenger.of(context).showSnackBar(
  320. SnackBar(
  321. content: Text('拨打电话: ${person.phone}'),
  322. duration: const Duration(seconds: 2),
  323. ),
  324. );
  325. },
  326. ),
  327. ),
  328. ],
  329. ),
  330. ),
  331. );
  332. }
  333. }
  334. /// 调度指令卡片
  335. class _InstructionCard extends StatelessWidget {
  336. final DutyInstruction instruction;
  337. const _InstructionCard({required this.instruction});
  338. @override
  339. Widget build(BuildContext context) {
  340. final theme = Theme.of(context);
  341. final statusColor = Color(instruction.status.color);
  342. return Card(
  343. elevation: 0,
  344. shape: RoundedRectangleBorder(
  345. borderRadius: BorderRadius.circular(12),
  346. side: BorderSide(color: Colors.grey.shade200),
  347. ),
  348. child: Padding(
  349. padding: const EdgeInsets.all(14),
  350. child: Column(
  351. crossAxisAlignment: CrossAxisAlignment.start,
  352. children: [
  353. Row(
  354. children: [
  355. Expanded(
  356. child: Text(
  357. instruction.title,
  358. style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
  359. ),
  360. ),
  361. Container(
  362. padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  363. decoration: BoxDecoration(
  364. color: statusColor.withAlpha(30),
  365. borderRadius: BorderRadius.circular(6),
  366. ),
  367. child: Text(
  368. instruction.status.label,
  369. style: TextStyle(fontSize: 11, color: statusColor, fontWeight: FontWeight.w500),
  370. ),
  371. ),
  372. ],
  373. ),
  374. const SizedBox(height: 8),
  375. Text(
  376. instruction.content,
  377. style: TextStyle(fontSize: 13, color: Colors.grey.shade700, height: 1.5),
  378. ),
  379. const SizedBox(height: 10),
  380. Row(
  381. children: [
  382. Icon(Icons.access_time, size: 12, color: Colors.grey.shade500),
  383. const SizedBox(width: 4),
  384. Text(
  385. instruction.time,
  386. style: TextStyle(fontSize: 11, color: Colors.grey.shade500),
  387. ),
  388. const SizedBox(width: 16),
  389. Icon(Icons.person_outline, size: 12, color: Colors.grey.shade500),
  390. const SizedBox(width: 4),
  391. Text(
  392. instruction.issuer,
  393. style: TextStyle(fontSize: 11, color: Colors.grey.shade500),
  394. ),
  395. ],
  396. ),
  397. ],
  398. ),
  399. ),
  400. );
  401. }
  402. }