| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715 |
- import 'package:flutter/material.dart';
- import '../../services/patrol_service.dart';
- import 'track_page.dart';
-
- /// 巡检任务详情页面
- class TaskDetailPage extends StatefulWidget {
- final String taskId;
- const TaskDetailPage({super.key, required this.taskId});
-
- @override
- State<TaskDetailPage> createState() => _TaskDetailPageState();
- }
-
- class _TaskDetailPageState extends State<TaskDetailPage> {
- final PatrolService _service = PatrolService.instance;
- PatrolTask? _task;
- bool _loading = true;
- bool _submitting = false;
- int _currentPointIndex = 0;
-
- @override
- void initState() {
- super.initState();
- _loadTask();
- }
-
- Future<void> _loadTask() async {
- setState(() => _loading = true);
- final task = await _service.getTaskDetail(widget.taskId);
- if (mounted) {
- setState(() {
- _task = task;
- _loading = false;
- // 找到第一个未完成的巡检点
- if (task != null && task.status == PatrolTaskStatus.ongoing) {
- for (int i = 0; i < task.points.length; i++) {
- if (task.points[i].status == null) {
- _currentPointIndex = i;
- break;
- }
- }
- }
- });
- }
- }
-
- @override
- Widget build(BuildContext context) {
- if (_loading) {
- return Scaffold(
- appBar: AppBar(title: const Text('任务详情')),
- body: const Center(child: CircularProgressIndicator()),
- );
- }
-
- if (_task == null) {
- return Scaffold(
- appBar: AppBar(title: const Text('任务详情')),
- body: const Center(child: Text('任务不存在')),
- );
- }
-
- return Scaffold(
- appBar: AppBar(
- title: Text(_task!.routeName),
- actions: [
- IconButton(
- icon: const Icon(Icons.map),
- tooltip: 'GPS轨迹',
- onPressed: () => _openTrackPage(),
- ),
- ],
- ),
- body: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildInfoCard(),
- const SizedBox(height: 16),
- _buildProgressCard(),
- const SizedBox(height: 16),
- _buildPointsSection(),
- if (_task!.status == PatrolTaskStatus.ongoing) ...[
- const SizedBox(height: 16),
- _buildReportSection(),
- ],
- ],
- ),
- ),
- );
- }
-
- Widget _buildInfoCard() {
- return Card(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- const Icon(Icons.info_outline, size: 20, color: Colors.blue),
- const SizedBox(width: 8),
- Text('任务信息',
- style: Theme.of(context).textTheme.titleMedium),
- ],
- ),
- const Divider(height: 20),
- _InfoRow(label: '任务编号', value: _task!.id),
- _InfoRow(label: '巡检路线', value: _task!.routeName),
- _InfoRow(label: '计划日期', value: _task!.dateStr),
- _InfoRow(label: '执行人', value: _task!.assignee),
- _InfoRow(label: '巡检点数', value: '${_task!.totalPoints}个'),
- _InfoRow(label: '任务描述', value: _task!.description),
- if (_task!.status == PatrolTaskStatus.completed) ...[
- if (_task!.result != null)
- _InfoRow(
- label: '巡检结果',
- value: _task!.result!.label,
- valueColor: _task!.result == PatrolResult.normal
- ? Colors.green
- : Colors.orange,
- ),
- if (_task!.issues != null)
- _InfoRow(label: '发现问题', value: _task!.issues!),
- if (_task!.reportTime != null)
- _InfoRow(
- label: '上报时间',
- value:
- '${_task!.reportTime!.hour.toString().padLeft(2, '0')}:'
- '${_task!.reportTime!.minute.toString().padLeft(2, '0')}',
- ),
- ],
- ],
- ),
- ),
- );
- }
-
- Widget _buildProgressCard() {
- return Card(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Icon(
- _task!.status == PatrolTaskStatus.completed
- ? Icons.check_circle
- : _task!.status == PatrolTaskStatus.ongoing
- ? Icons.play_circle
- : Icons.schedule,
- size: 20,
- color: _task!.status == PatrolTaskStatus.completed
- ? Colors.green
- : _task!.status == PatrolTaskStatus.ongoing
- ? Colors.blue
- : Colors.orange,
- ),
- const SizedBox(width: 8),
- Text('巡检进度',
- style: Theme.of(context).textTheme.titleMedium),
- ],
- ),
- const SizedBox(height: 12),
- Row(
- children: [
- Expanded(
- child: ClipRRect(
- borderRadius: BorderRadius.circular(4),
- child: LinearProgressIndicator(
- value: _task!.progress,
- backgroundColor: Colors.grey.shade200,
- minHeight: 8,
- ),
- ),
- ),
- const SizedBox(width: 12),
- Text(
- '${(_task!.progress * 100).toInt()}%',
- style: const TextStyle(
- fontWeight: FontWeight.w600, fontSize: 16),
- ),
- ],
- ),
- const SizedBox(height: 8),
- Text(
- '已完成 ${_task!.completedPoints} / ${_task!.totalPoints} 个巡检点',
- style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _buildPointsSection() {
- return Card(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- const Icon(Icons.location_on, size: 20, color: Colors.red),
- const SizedBox(width: 8),
- Text('巡检点列表',
- style: Theme.of(context).textTheme.titleMedium),
- ],
- ),
- const SizedBox(height: 12),
- ...List.generate(_task!.points.length, (i) {
- final point = _task!.points[i];
- final isCompleted = point.status != null;
- final isCurrent =
- _task!.status == PatrolTaskStatus.ongoing &&
- i == _currentPointIndex;
-
- return _PointTile(
- index: i + 1,
- point: point,
- isCompleted: isCompleted,
- isCurrent: isCurrent,
- onTap: _task!.status == PatrolTaskStatus.ongoing
- ? () => _selectPoint(i)
- : null,
- );
- }),
- ],
- ),
- ),
- );
- }
-
- Widget _buildReportSection() {
- final currentPoint =
- _currentPointIndex < _task!.points.length
- ? _task!.points[_currentPointIndex]
- : null;
-
- if (currentPoint == null || currentPoint.status != null) {
- return Card(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
- child: Padding(
- padding: const EdgeInsets.all(24),
- child: Column(
- children: [
- const Icon(Icons.check_circle, size: 48, color: Colors.green),
- const SizedBox(height: 12),
- const Text('所有巡检点已完成上报',
- style: TextStyle(fontSize: 16)),
- const SizedBox(height: 16),
- FilledButton.icon(
- onPressed: _submitting ? null : _completeTask,
- icon: _submitting
- ? const SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2, color: Colors.white))
- : const Icon(Icons.send),
- label: Text(_submitting ? '提交中...' : '完成巡检任务'),
- ),
- ],
- ),
- ),
- );
- }
-
- return _PointReportForm(
- point: currentPoint,
- pointIndex: _currentPointIndex + 1,
- submitting: _submitting,
- onSubmit: (status, remark, photos) =>
- _submitPointReport(currentPoint, status, remark, photos),
- );
- }
-
- void _selectPoint(int index) {
- setState(() => _currentPointIndex = index);
- }
-
- Future<void> _submitPointReport(
- PatrolPoint point,
- PatrolPointStatus status,
- String? remark,
- List<String>? photos,
- ) async {
- setState(() => _submitting = true);
- final success = await _service.submitPointReport(
- taskId: _task!.id,
- pointId: point.id,
- status: status,
- remark: remark,
- photoPaths: photos,
- );
- if (mounted) {
- setState(() {
- _submitting = false;
- if (success) {
- point.status = status;
- point.remark = remark;
- point.photos = photos;
- // 移到下一个未完成的点
- for (int i = 0; i < _task!.points.length; i++) {
- if (_task!.points[i].status == null) {
- _currentPointIndex = i;
- break;
- }
- }
- }
- });
- if (success) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('${point.name} 上报成功'),
- backgroundColor: Colors.green,
- ),
- );
- }
- }
- }
-
- Future<void> _completeTask() async {
- final confirmed = await showDialog<bool>(
- context: context,
- builder: (ctx) => AlertDialog(
- title: const Text('确认完成'),
- content: const Text('确定要完成本次巡检任务吗?'),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(ctx, false),
- child: const Text('取消'),
- ),
- FilledButton(
- onPressed: () => Navigator.pop(ctx, true),
- child: const Text('确认完成'),
- ),
- ],
- ),
- );
- if (confirmed != true) return;
-
- setState(() => _submitting = true);
- await _service.completeTask(
- taskId: _task!.id,
- result: PatrolResult.normal,
- summary: '巡检完成,所有点位正常',
- );
- if (mounted) {
- setState(() => _submitting = false);
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('巡检任务已完成'),
- backgroundColor: Colors.green,
- ),
- );
- Navigator.pop(context);
- }
- }
-
- void _openTrackPage() {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (_) => TrackPage(taskId: _task!.id, taskName: _task!.routeName),
- ),
- );
- }
- }
-
- class _InfoRow extends StatelessWidget {
- final String label;
- final String value;
- final Color? valueColor;
-
- const _InfoRow({required this.label, required this.value, this.valueColor});
-
- @override
- Widget build(BuildContext context) {
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 4),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- SizedBox(
- width: 70,
- child: Text(
- label,
- style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
- ),
- ),
- Expanded(
- child: Text(
- value,
- style: TextStyle(
- fontSize: 13,
- color: valueColor ?? Colors.black87,
- fontWeight:
- valueColor != null ? FontWeight.w500 : FontWeight.normal,
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
-
- class _PointTile extends StatelessWidget {
- final int index;
- final PatrolPoint point;
- final bool isCompleted;
- final bool isCurrent;
- final VoidCallback? onTap;
-
- const _PointTile({
- required this.index,
- required this.point,
- required this.isCompleted,
- required this.isCurrent,
- this.onTap,
- });
-
- @override
- Widget build(BuildContext context) {
- Color bgColor;
- IconData icon;
- if (isCompleted) {
- bgColor = point.status == PatrolPointStatus.normal
- ? Colors.green.shade50
- : Colors.orange.shade50;
- icon = point.status == PatrolPointStatus.normal
- ? Icons.check_circle
- : Icons.warning;
- } else if (isCurrent) {
- bgColor = Colors.blue.shade50;
- icon = Icons.radio_button_checked;
- } else {
- bgColor = Colors.grey.shade50;
- icon = Icons.radio_button_unchecked;
- }
-
- return Container(
- margin: const EdgeInsets.only(bottom: 8),
- decoration: BoxDecoration(
- color: bgColor,
- borderRadius: BorderRadius.circular(8),
- border: isCurrent
- ? Border.all(color: Colors.blue, width: 1.5)
- : null,
- ),
- child: ListTile(
- onTap: onTap,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
- leading: CircleAvatar(
- backgroundColor: isCompleted
- ? (point.status == PatrolPointStatus.normal
- ? Colors.green
- : Colors.orange)
- : isCurrent
- ? Colors.blue
- : Colors.grey.shade300,
- child: isCompleted
- ? Icon(icon, color: Colors.white, size: 20)
- : Text(
- '$index',
- style: TextStyle(
- color: isCurrent ? Colors.white : Colors.grey.shade700,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- title: Text(
- point.name,
- style: TextStyle(
- fontWeight: isCurrent ? FontWeight.w600 : FontWeight.normal,
- fontSize: 14,
- ),
- ),
- subtitle: Text(
- isCompleted && point.remark != null
- ? point.remark!
- : point.type.label,
- style: TextStyle(
- fontSize: 12,
- color: Colors.grey.shade600,
- ),
- ),
- trailing: isCurrent
- ? const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.blue)
- : null,
- ),
- );
- }
- }
-
- class _PointReportForm extends StatefulWidget {
- final PatrolPoint point;
- final int pointIndex;
- final bool submitting;
- final Future<void> Function(
- PatrolPointStatus status, String? remark, List<String>? photos) onSubmit;
-
- const _PointReportForm({
- required this.point,
- required this.pointIndex,
- required this.submitting,
- required this.onSubmit,
- });
-
- @override
- State<_PointReportForm> createState() => _PointReportFormState();
- }
-
- class _PointReportFormState extends State<_PointReportForm> {
- PatrolPointStatus _selectedStatus = PatrolPointStatus.normal;
- final _remarkController = TextEditingController();
- final List<String> _photos = [];
-
- @override
- void dispose() {
- _remarkController.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return Card(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
- color: Colors.blue.shade50,
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- const Icon(Icons.edit_note, size: 20, color: Colors.blue),
- const SizedBox(width: 8),
- Expanded(
- child: Text(
- '上报: ${widget.point.name} (${widget.pointIndex})',
- style: Theme.of(context)
- .textTheme
- .titleMedium
- ?.copyWith(color: Colors.blue.shade800),
- ),
- ),
- ],
- ),
- const SizedBox(height: 16),
- // 状态选择
- const Text('巡检状态', style: TextStyle(fontWeight: FontWeight.w500)),
- const SizedBox(height: 8),
- Row(
- children: PatrolPointStatus.values.map((s) {
- final isSelected = _selectedStatus == s;
- Color color;
- switch (s) {
- case PatrolPointStatus.normal:
- color = Colors.green;
- break;
- case PatrolPointStatus.abnormal:
- color = Colors.red;
- break;
- case PatrolPointStatus.maintenance:
- color = Colors.orange;
- break;
- }
- return Expanded(
- child: Padding(
- padding: const EdgeInsets.only(right: 8),
- child: ChoiceChip(
- label: Text(s.label),
- selected: isSelected,
- selectedColor: color.withAlpha(40),
- labelStyle: TextStyle(
- color: isSelected ? color : Colors.grey,
- ),
- side: BorderSide(
- color: isSelected ? color : Colors.grey.shade300,
- ),
- onSelected: (_) => setState(() => _selectedStatus = s),
- ),
- ),
- );
- }).toList(),
- ),
- const SizedBox(height: 16),
- // 备注
- const Text('备注', style: TextStyle(fontWeight: FontWeight.w500)),
- const SizedBox(height: 8),
- TextField(
- controller: _remarkController,
- maxLines: 3,
- decoration: InputDecoration(
- hintText: '请输入巡检备注(可选)',
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- filled: true,
- fillColor: Colors.white,
- ),
- ),
- const SizedBox(height: 16),
- // 拍照上传
- Row(
- children: [
- const Text('拍照记录',
- style: TextStyle(fontWeight: FontWeight.w500)),
- const SizedBox(width: 8),
- Text(
- '(${_photos.length}/6)',
- style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
- ),
- ],
- ),
- const SizedBox(height: 8),
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: [
- ..._photos.asMap().entries.map(
- (entry) => Stack(
- children: [
- Container(
- width: 72,
- height: 72,
- decoration: BoxDecoration(
- color: Colors.grey.shade300,
- borderRadius: BorderRadius.circular(8),
- ),
- child: const Icon(Icons.image,
- color: Colors.grey, size: 32),
- ),
- Positioned(
- top: -4,
- right: -4,
- child: GestureDetector(
- onTap: () =>
- setState(() => _photos.removeAt(entry.key)),
- child: Container(
- width: 20,
- height: 20,
- decoration: const BoxDecoration(
- color: Colors.red,
- shape: BoxShape.circle,
- ),
- child: const Icon(Icons.close,
- size: 12, color: Colors.white),
- ),
- ),
- ),
- ],
- ),
- ),
- if (_photos.length < 6)
- GestureDetector(
- onTap: () => setState(
- () => _photos.add('photo_${_photos.length + 1}')),
- child: Container(
- width: 72,
- height: 72,
- decoration: BoxDecoration(
- border: Border.all(color: Colors.blue, width: 1.5),
- borderRadius: BorderRadius.circular(8),
- ),
- child: const Icon(Icons.add_a_photo,
- color: Colors.blue, size: 24),
- ),
- ),
- ],
- ),
- const SizedBox(height: 16),
- // 提交按钮
- SizedBox(
- width: double.infinity,
- child: FilledButton.icon(
- onPressed: widget.submitting
- ? null
- : () async {
- await widget.onSubmit(
- _selectedStatus,
- _remarkController.text.isEmpty
- ? null
- : _remarkController.text,
- _photos.isEmpty ? null : _photos,
- );
- _remarkController.clear();
- setState(() => _photos.clear());
- },
- icon: widget.submitting
- ? const SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2, color: Colors.white))
- : const Icon(Icons.send, size: 18),
- label: Text(widget.submitting ? '提交中...' : '提交上报'),
- ),
- ),
- ],
- ),
- ),
- );
- }
- }
|