feat: 新增订单拨号功能,重构订单页与登录页
This commit is contained in:
1 parent
774403785c
commit
411ec8be23
9 files changed
+875
-482
No files matched your search
@@ -66,6 +66,34 @@ class Order {
|
||||
/// 是否处于「现场勘察中(1)」,仅该状态可维护勘察明细。
|
||||
bool get surveyEditable => source == 'survey' && status == 1;
|
||||
|
||||
/// 是否已完成:报修(2)、安装/维保(1)、勘察已归档(3)。
|
||||
bool get isCompleted => switch (source) {
|
||||
'repair' => status == 2,
|
||||
'install' || 'maintenance' => status == 1,
|
||||
'survey' => status == 3,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// 当前状态文案,用于「进行中 / 已完成」列表的状态标签。
|
||||
String get statusText => switch (source) {
|
||||
'survey' => switch (status) {
|
||||
0 => '待指派',
|
||||
1 => '现场勘察中',
|
||||
2 => '待技术复核',
|
||||
3 => '已归档',
|
||||
5 => '已取消',
|
||||
_ => '进行中',
|
||||
},
|
||||
'repair' => switch (status) {
|
||||
0 => '待处理',
|
||||
1 => '处理中',
|
||||
2 => '已完成',
|
||||
_ => '进行中',
|
||||
},
|
||||
'install' || 'maintenance' => status == 1 ? '已完成' : '待上门',
|
||||
_ => isCompleted ? '已完成' : '进行中',
|
||||
};
|
||||
|
||||
/// 来源中文名,用于收益明细副标题等场景。
|
||||
String get sourceName => switch (source) {
|
||||
'repair' => '家庭电路维修',
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../core/network/api_response.dart';
|
||||
import '../../../core/providers.dart';
|
||||
@@ -33,9 +34,14 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
final String source;
|
||||
final int id;
|
||||
|
||||
/// 地图占位暂时隐藏(代码保留,后续接入真实地图时改回 true)。
|
||||
static const _showMap = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final detail = ref.watch(orderDetailProvider((source: source, id: id)));
|
||||
final order = detail.value;
|
||||
final showBottomAction = order != null && !order.isCompleted;
|
||||
|
||||
return Scaffold(
|
||||
body: XPageShell(
|
||||
@@ -70,15 +76,16 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
},
|
||||
),
|
||||
// 底部固定操作栏,对应源 `.bottom-action`
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: _BottomAction(
|
||||
onStart: () => showAppToast('已开始出发', success: true),
|
||||
// 已完成订单不再展示出发操作
|
||||
if (showBottomAction)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: _BottomAction(
|
||||
onStart: () => showAppToast('已开始出发', success: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -87,11 +94,11 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
Widget _buildContent(BuildContext context, Order order) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 120),
|
||||
padding: EdgeInsets.only(bottom: order.isCompleted ? 24 : 120),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_MapSection(address: order.address),
|
||||
if (_showMap) _MapSection(address: order.address),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -392,7 +399,7 @@ class _CustomerCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () => showAppToast('拨打电话'),
|
||||
onTap: () => _callPhone(order.customerPhone),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
width: 40,
|
||||
@@ -744,6 +751,21 @@ class _DotPatternPainter extends CustomPainter {
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// 调起系统拨号盘。
|
||||
Future<void> _callPhone(String raw) async {
|
||||
final phone = raw.replaceAll(RegExp(r'[^0-9+]'), '');
|
||||
if (phone.isEmpty) {
|
||||
showAppToast('该客户暂无联系方式');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final ok = await launchUrl(Uri(scheme: 'tel', path: phone));
|
||||
if (!ok) showAppToast('无法调起拨号');
|
||||
} catch (_) {
|
||||
showAppToast('无法调起拨号');
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开单张照片大图预览。
|
||||
void _previewPhoto(BuildContext context, String url) {
|
||||
showDialog<void>(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../core/network/api_response.dart';
|
||||
import '../../../core/providers.dart';
|
||||
@@ -20,10 +21,10 @@ import '../data/order_models.dart';
|
||||
String _money(num v) =>
|
||||
v == v.roundToDouble() ? v.toStringAsFixed(0) : formatMoney(v);
|
||||
|
||||
/// 订单 Tab(待接订单),对应源 `pages/order/index.vue`。
|
||||
/// 订单 Tab:顶部三档切换「进行中 / 待接单 / 已完成」。
|
||||
///
|
||||
/// 顶栏与地图区固定,只有下方订单列表滚动;沿用源 `.orders-section`
|
||||
/// 24px 顶部圆角「上移压住地图」的观感(把地图高度相应减 24,视觉等价)。
|
||||
/// 「进行中」「已完成」来自 `myOrders`,待接单来自订单池;地图区域暂时隐藏,
|
||||
/// `_showMap` 置 false 保留占位实现,后续接入真实地图时改回 true 即可。
|
||||
class OrderTab extends ConsumerStatefulWidget {
|
||||
const OrderTab({super.key});
|
||||
|
||||
@@ -32,10 +33,34 @@ class OrderTab extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _OrderTabState extends ConsumerState<OrderTab> {
|
||||
/// 地图占位暂时隐藏(代码保留)。
|
||||
static const _showMap = false;
|
||||
|
||||
static const _segments = <String>['进行中', '待接单', '已完成'];
|
||||
|
||||
/// 待接单的类型筛选,null 表示全部。
|
||||
static const _typeFilters = <(String?, String)>[
|
||||
(null, '全部'),
|
||||
('repair', '报修'),
|
||||
('install', '安装'),
|
||||
('maintenance', '维保'),
|
||||
('survey', '勘察'),
|
||||
];
|
||||
|
||||
/// 0 进行中 / 1 待接单 / 2 已完成。
|
||||
int _segment = 0;
|
||||
String? _typeFilter;
|
||||
|
||||
/// 本会话内已接单的订单 id(接单后列表会刷新移除,这里用于渲染「已接单」态)。
|
||||
final _grabbed = <String>{};
|
||||
String? _grabbing;
|
||||
|
||||
/// 只订阅当前分段的列表,切换时旧的 autoDispose 实例自动释放。
|
||||
AsyncValue<List<Order>> _watchActive() => switch (_segment) {
|
||||
1 => ref.watch(orderPoolProvider),
|
||||
_ => ref.watch(myOrdersProvider(_segment == 0 ? 'pending' : 'completed')),
|
||||
};
|
||||
|
||||
Future<void> _handleGrab(Order order) async {
|
||||
if (_grabbing != null || _grabbed.contains(order.id)) return;
|
||||
setState(() => _grabbing = order.id);
|
||||
@@ -45,9 +70,14 @@ class _OrderTabState extends ConsumerState<OrderTab> {
|
||||
.grab(order.source, order.intId);
|
||||
if (!mounted) return;
|
||||
if (res.isOk) {
|
||||
setState(() => _grabbed.add(order.id));
|
||||
setState(() {
|
||||
_grabbed.add(order.id);
|
||||
// 接单成功后订单转入「进行中」,顺手切到该分段,避免用户还要自己找
|
||||
_segment = 0;
|
||||
});
|
||||
showAppToast('接单成功', success: true);
|
||||
ref.invalidate(orderPoolProvider);
|
||||
ref.invalidate(myOrdersProvider);
|
||||
} else {
|
||||
showAppToast(res.message.isEmpty ? '接单失败' : res.message);
|
||||
}
|
||||
@@ -58,9 +88,41 @@ class _OrderTabState extends ConsumerState<OrderTab> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
ref.invalidate(orderPoolProvider);
|
||||
ref.invalidate(myOrdersProvider);
|
||||
try {
|
||||
switch (_segment) {
|
||||
case 1:
|
||||
await ref.read(orderPoolProvider.future);
|
||||
case 0:
|
||||
await ref.read(myOrdersProvider('pending').future);
|
||||
default:
|
||||
await ref.read(myOrdersProvider('completed').future);
|
||||
}
|
||||
} catch (_) {
|
||||
// 失败由列表的 hasError 分支展示,这里只负责结束下拉动画
|
||||
}
|
||||
}
|
||||
|
||||
/// 联系客户:调起系统拨号盘。
|
||||
Future<void> _callCustomer(Order order) async {
|
||||
final phone = order.customerPhone.replaceAll(RegExp(r'[^0-9+]'), '');
|
||||
if (phone.isEmpty) {
|
||||
showAppToast('该客户暂无联系方式');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final ok = await launchUrl(Uri(scheme: 'tel', path: phone));
|
||||
if (!ok && mounted) showAppToast('无法调起拨号');
|
||||
} catch (_) {
|
||||
if (mounted) showAppToast('无法调起拨号');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pool = ref.watch(orderPoolProvider);
|
||||
final active = _watchActive();
|
||||
final unread = ref.watch(noticeUnreadCountProvider);
|
||||
|
||||
return XPageShell(
|
||||
@@ -72,93 +134,176 @@ class _OrderTabState extends ConsumerState<OrderTab> {
|
||||
trailingBadge: unread,
|
||||
onTrailingTap: () => context.push('/notice'),
|
||||
),
|
||||
child: RefreshIndicator(
|
||||
color: AppColors.primary,
|
||||
onRefresh: () async {
|
||||
ref.invalidate(orderPoolProvider);
|
||||
await ref.read(orderPoolProvider.future);
|
||||
},
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
// 源地图高 320,被下方 24px 圆角区块遮住底部 → 这里直接取 296
|
||||
SizedBox(height: 296, child: const _MapPlaceholder()),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.bgPage,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(AppRadius.xxl),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 120),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSegments(),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
color: AppColors.primary,
|
||||
onRefresh: _refresh,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildSectionHeader(pool),
|
||||
const SizedBox(height: 16),
|
||||
..._buildBody(pool),
|
||||
// 源地图高 320,被下方 24px 圆角区块遮住底部 → 这里直接取 296
|
||||
if (_showMap)
|
||||
const SizedBox(height: 296, child: _MapPlaceholder()),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.bgPage,
|
||||
borderRadius: _showMap
|
||||
? const BorderRadius.vertical(
|
||||
top: Radius.circular(AppRadius.xxl),
|
||||
)
|
||||
: BorderRadius.zero,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 120),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildSectionHeader(active),
|
||||
if (_segment == 1) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildTypeFilter(),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
..._buildBody(active),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 顶部三档分段控件。
|
||||
Widget _buildSegments() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i = 0; i < _segments.length; i++)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _segment = i),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: _segment == i
|
||||
? AppColors.surfaceContainerLowest
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
boxShadow: _segment == i
|
||||
? const [
|
||||
BoxShadow(
|
||||
color: Color(0x14000000),
|
||||
blurRadius: 6,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
_segments[i],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: _segment == i
|
||||
? FontWeight.w700
|
||||
: FontWeight.w500,
|
||||
color: _segment == i
|
||||
? AppColors.onSurface
|
||||
: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(AsyncValue<List<Order>> pool) {
|
||||
final count = pool.value?.length;
|
||||
return Row(
|
||||
Widget _buildSectionHeader(AsyncValue<List<Order>> active) {
|
||||
final count = active.value?.length;
|
||||
final (title, loadingText, unit) = switch (_segment) {
|
||||
0 => ('进行中派单', '正在获取进行中的派单…', ' 个进行中'),
|
||||
1 => ('待接订单', '正在获取附近订单…', ' 个待接订单'),
|
||||
_ => ('已完成订单', '正在获取已完成订单…', ' 个已完成'),
|
||||
};
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'待接订单',
|
||||
style: AppTypography.navLargeTitle.copyWith(
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
count == null ? '正在获取附近订单…' : '附近有 $count 个订单',
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: AppTypography.navLargeTitle.copyWith(
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => showAppToast('筛选功能'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'筛选',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const XIcon('filter', size: 18, color: AppColors.primary),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
count == null ? loadingText : '共 $count$unit',
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildBody(AsyncValue<List<Order>> pool) {
|
||||
if (pool.isLoading && !pool.hasValue) {
|
||||
/// 待接单的类型筛选。
|
||||
Widget _buildTypeFilter() {
|
||||
return SizedBox(
|
||||
height: 34,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _typeFilters.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final (value, label) = _typeFilters[i];
|
||||
final selected = _typeFilter == value;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _typeFilter = value),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: AppColors.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: selected ? Colors.white : AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildBody(AsyncValue<List<Order>> active) {
|
||||
if (active.isLoading && !active.hasValue) {
|
||||
return const [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 60),
|
||||
@@ -175,31 +320,62 @@ class _OrderTabState extends ConsumerState<OrderTab> {
|
||||
),
|
||||
];
|
||||
}
|
||||
if (pool.hasError && !pool.hasValue) {
|
||||
if (active.hasError && !active.hasValue) {
|
||||
return [
|
||||
XPlaceholder(text: '订单加载失败\n${pool.error}'),
|
||||
XPlaceholder(text: '订单加载失败\n${active.error}'),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => ref.invalidate(orderPoolProvider),
|
||||
onPressed: () {
|
||||
ref.invalidate(orderPoolProvider);
|
||||
ref.invalidate(myOrdersProvider);
|
||||
},
|
||||
child: const Text('点击重试'),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final orders = pool.value ?? const <Order>[];
|
||||
final orders = active.value ?? const <Order>[];
|
||||
if (_segment == 1) {
|
||||
final filtered = _typeFilter == null
|
||||
? orders
|
||||
: orders.where((o) => o.source == _typeFilter).toList();
|
||||
if (filtered.isEmpty) {
|
||||
return [
|
||||
XPlaceholder(
|
||||
text: _typeFilter == null ? '附近暂无待接订单' : '该类型暂无待接订单',
|
||||
),
|
||||
];
|
||||
}
|
||||
return [
|
||||
for (final order in filtered)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _OrderCard(
|
||||
order: order,
|
||||
grabbing: _grabbing == order.id,
|
||||
grabbed: _grabbed.contains(order.id),
|
||||
onGrab: () => _handleGrab(order),
|
||||
onTap: () => context.push('/order/${order.source}/${order.intId}'),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (orders.isEmpty) {
|
||||
return const [XPlaceholder(text: '附近暂无待接订单')];
|
||||
return [
|
||||
XPlaceholder(
|
||||
text: _segment == 0 ? '暂无进行中的派单' : '暂无已完成订单',
|
||||
),
|
||||
];
|
||||
}
|
||||
return [
|
||||
for (final order in orders)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _OrderCard(
|
||||
child: _MyOrderCard(
|
||||
order: order,
|
||||
grabbing: _grabbing == order.id,
|
||||
grabbed: _grabbed.contains(order.id),
|
||||
onGrab: () => _handleGrab(order),
|
||||
onCall: () => _callCustomer(order),
|
||||
onTap: () => context.push('/order/${order.source}/${order.intId}'),
|
||||
),
|
||||
),
|
||||
@@ -410,6 +586,206 @@ class _OrderCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 进行中 / 已完成订单卡片:状态标签 + 联系客户 + 查看详情。
|
||||
class _MyOrderCard extends StatelessWidget {
|
||||
const _MyOrderCard({
|
||||
required this.order,
|
||||
required this.onCall,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final Order order;
|
||||
final VoidCallback onCall;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: AppColors.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.borderSoft),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
boxShadow: AppShadows.orderCard,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'#${order.displayNo}',
|
||||
style: AppTypography.labelCaps.copyWith(
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
XTypeChip(type: order.type, text: order.listTypeText),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_statusChip(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
order.title,
|
||||
style: AppTypography.headlineMd.copyWith(
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 2),
|
||||
child: XIcon(
|
||||
'location',
|
||||
size: 16,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
order.address.isEmpty ? '地址待确认' : order.address,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
const XIcon(
|
||||
'clock',
|
||||
size: 16,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
order.schedule,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'¥${_money(order.amount)}',
|
||||
style: AppTypography.bodyLg.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _actionButton(
|
||||
label: '联系客户',
|
||||
icon: 'call',
|
||||
filled: false,
|
||||
onTap: onCall,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _actionButton(
|
||||
label: '查看详情',
|
||||
icon: 'forward',
|
||||
filled: true,
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusChip() {
|
||||
final completed = order.isCompleted;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: completed ? AppColors.surfaceContainer : AppColors.accentSubtle,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Text(
|
||||
order.statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: completed ? AppColors.onSurfaceVariant : AppColors.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionButton({
|
||||
required String label,
|
||||
required String icon,
|
||||
required bool filled,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final foreground = filled ? Colors.white : AppColors.onSurface;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 46,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: filled ? AppColors.primary : AppColors.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: filled
|
||||
? null
|
||||
: Border.all(color: const Color(0x80C3C5D9)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
XIcon(icon, size: 18, color: foreground),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: foreground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 接单中的旋转刷新图标。
|
||||
class _SpinningRefresh extends StatefulWidget {
|
||||
const _SpinningRefresh();
|
||||
|
||||
Reference in new issue
Block a user