jjsos_autosos_flutter/lib/pages/order/conduct_page.dart

455 lines
14 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<ConductPage> createState() => _ConductPageState();
}
class _ConductPageState extends ConsumerState<ConductPage> {
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<void> _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<void> _arrive() async {
EasyLoading.show(status: '上报中…');
try {
await ref.read(orderServiceProvider).arrive();
EasyLoading.dismiss();
Toast.success('已标记到达现场');
_load();
} catch (e) {
EasyLoading.dismiss();
Toast.error('上报失败:$e');
}
}
Future<void> _cancel() async {
final ok = await showDialog<bool>(
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<void> _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<void> _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<Widget> 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),
],
),
);
}
}