import 'dart:async'; import 'package:autosos_flutter/model/order.dart'; import 'package:autosos_flutter/provider/providers.dart'; import 'package:autosos_flutter/router/app_router.dart'; import 'package:autosos_flutter/util/toast.dart'; import 'package:autosos_flutter/widget/multi_state_view.dart'; import 'package:autosos_flutter/widget/watermark.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; /// 救援中页(对应 Android ConductActivity) /// 步骤:开始 → 到达现场 → 拍照 → 签字 → 收款 → 完成 class ConductPage extends ConsumerStatefulWidget { final int orderId; const ConductPage({super.key, required this.orderId}); @override ConsumerState createState() => _ConductPageState(); } class _ConductPageState extends ConsumerState { MultiState _state = MultiState.loading; Order? _order; Timer? _ticker; Duration _elapsed = Duration.zero; @override void initState() { super.initState(); _load(); _ticker = Timer.periodic(const Duration(seconds: 1), (_) { if (!mounted || _order == null) return; setState(() { final start = _order!.acceptedAt ?? _order!.createdAt; _elapsed = DateTime.now().difference(start); }); }); } @override void dispose() { _ticker?.cancel(); super.dispose(); } Future _load() async { setState(() => _state = MultiState.loading); try { final resp = await ref.read(orderApiProvider).orderDetail(widget.orderId); if (!mounted) return; if (!resp.isSuccess || resp.data == null) { setState(() => _state = MultiState.error); Toast.error(resp.message ?? '订单不存在'); return; } final o = resp.data!; setState(() { _order = o; _state = MultiState.success; }); ref.read(orderServiceProvider).setCurrent(o); } catch (e) { if (mounted) setState(() => _state = MultiState.error); } } Future _arrive() async { EasyLoading.show(status: '上报中…'); try { await ref.read(orderServiceProvider).arrive(); EasyLoading.dismiss(); Toast.success('已标记到达现场'); _load(); } catch (e) { EasyLoading.dismiss(); Toast.error('上报失败:$e'); } } Future _cancel() async { final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('取消订单'), content: const Text('确定要取消此订单吗?取消后无法恢复。'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('不取消'), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () => Navigator.pop(ctx, true), child: const Text('确认取消'), ), ], ), ); if (ok != true) return; try { await ref.read(orderServiceProvider).cancel(); Toast.success('已取消'); if (mounted) context.go(Routes.home); } catch (e) { Toast.error('取消失败:$e'); } } Future _call(String? phone) async { if (phone == null || phone.isEmpty) { Toast.error('客户未留电话'); return; } final uri = Uri.parse('tel:$phone'); if (await canLaunchUrl(uri)) { await launchUrl(uri); } else { Toast.error('无法拨打电话'); } } Future _navigate() async { final o = _order; if (o?.lat == null || o?.lng == null) { Toast.error('暂无位置信息'); return; } // 高德地图 URL Scheme final uri = Uri.parse( 'androidamap://navi?sourceApplication=jjsos&lat=${o!.lat}&lon=${o.lng}&dev=0&style=2', ); if (await canLaunchUrl(uri)) { await launchUrl(uri); } else { // 退回到 web 版 final web = Uri.parse( 'https://uri.amap.com/navigation?to=${o.lng},${o.lat},${o.address ?? "救援地点"}&mode=car', ); await launchUrl(web, mode: LaunchMode.externalApplication); } } String _fmt(Duration d) { String two(int n) => n.toString().padLeft(2, '0'); return '${two(d.inHours)}:${two(d.inMinutes.remainder(60))}:${two(d.inSeconds.remainder(60))}'; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('救援中'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _load), ], ), body: MultiStateView( state: _state, onRetry: _load, child: _order == null ? const SizedBox.shrink() : _body(_order!), ), ); } Widget _body(Order o) { final isArrived = o.status == OrderStatus.conducting || o.status == OrderStatus.photographing || o.status == OrderStatus.signing || o.status == OrderStatus.collecting; return Stack( children: [ ListView( padding: const EdgeInsets.all(12), children: [ // 倒计时卡片 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.red.shade400, Colors.red.shade700], ), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.timer, color: Colors.white), const SizedBox(width: 8), Text( '已耗时 ${_fmt(_elapsed)}', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), const Spacer(), Text('订单号:${o.orderNo}', style: const TextStyle(color: Colors.white, fontSize: 12)), ], ), ), const SizedBox(height: 12), // 客户信息 _section( title: '客户信息', children: [ _kv('联系人', o.customerName ?? '—'), _kv('电话', o.customerPhone ?? '—'), if (o.carNo != null) _kv('车牌', o.carNo!), if (o.carType != null) _kv('车型', o.carType!), if (o.description != null) _kv('描述', o.description!), ], ), const SizedBox(height: 12), // 救援地点 _section( title: '救援地点', children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), child: Row( children: [ const Icon(Icons.location_on, color: Colors.red, size: 18), const SizedBox(width: 4), Expanded( child: Text( o.address ?? '—', style: const TextStyle(fontSize: 14), ), ), TextButton.icon( onPressed: _navigate, icon: const Icon(Icons.directions, size: 16), label: const Text('导航'), ), TextButton.icon( onPressed: () => _call(o.customerPhone), icon: const Icon(Icons.phone, size: 16), label: const Text('呼叫'), ), ], ), ), ], ), const SizedBox(height: 12), // 步骤流程 _section( title: '服务流程', children: [ _Step( active: true, done: isArrived, title: '到达现场', child: isArrived ? Text('已到达:${o.arrivedAt ?? "—"}', style: const TextStyle(color: Colors.grey)) : ElevatedButton( onPressed: _arrive, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, ), child: const Text('我已到达现场'), ), ), _Step( active: isArrived, done: o.photoUrls.isNotEmpty, title: '拍照取证', child: ElevatedButton( onPressed: isArrived ? () => context .push('${Routes.orderPhotograph}/${o.id}') .then((_) => _load()) : null, child: Text( o.photoUrls.isEmpty ? '开始拍照' : '已拍 ${o.photoUrls.length} 张,继续', ), ), ), _Step( active: o.photoUrls.isNotEmpty, done: o.signatureUrl != null, title: '客户签字', child: ElevatedButton( onPressed: o.photoUrls.isNotEmpty ? () => context .push('${Routes.orderAutograph}/${o.id}') .then((_) => _load()) : null, child: const Text('让客户签字'), ), ), _Step( active: o.signatureUrl != null, done: o.payMethod != null, title: '客户付款', child: ElevatedButton( onPressed: o.signatureUrl != null ? () => context .push('${Routes.orderCollection}/${o.id}') .then((_) => _load()) : null, child: const Text('发起收款'), ), ), _Step( active: o.payMethod != null, done: o.status == OrderStatus.completed, title: '完成订单', child: ElevatedButton( onPressed: o.payMethod != null ? () => context .push('${Routes.orderComplete}/${o.id}') .then((_) => _load()) : null, style: ElevatedButton.styleFrom( backgroundColor: Colors.green, ), child: const Text('提交完成'), ), ), ], ), const SizedBox(height: 12), // 取消订单 OutlinedButton.icon( onPressed: _cancel, icon: const Icon(Icons.cancel_outlined, color: Colors.red), label: const Text( '取消订单', style: TextStyle(color: Colors.red), ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.red), minimumSize: const Size.fromHeight(48), ), ), const SizedBox(height: 80), ], ), // 全屏水印 const IgnorePointer( child: Watermark( text: '啾啾救援 © 内部资料', ), ), ], ); } Widget _section({required String title, required List children}) { return Material( color: Colors.white, borderRadius: BorderRadius.circular(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), child: Text( title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), ), ), ...children, const SizedBox(height: 8), ], ), ); } Widget _kv(String k, String v) => Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 60, child: Text(k, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)), ), Expanded( child: Text(v, style: const TextStyle(fontSize: 14)), ), ], ), ); } class _Step extends StatelessWidget { final bool active; final bool done; final String title; final Widget child; const _Step({ required this.active, required this.done, required this.title, required this.child, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( shape: BoxShape.circle, color: done ? Colors.green : (active ? Theme.of(context).primaryColor : Colors.grey.shade300), ), child: Icon( done ? Icons.check : Icons.circle, size: 14, color: Colors.white, ), ), const SizedBox(width: 8), SizedBox( width: 70, child: Text( title, style: TextStyle( fontSize: 14, color: active ? Colors.black : Colors.grey, ), ), ), Expanded(child: child), ], ), ); } }