Flutter for OpenHarmony 商城App实战 - 退货申请实现
本文介绍了在Flutter for OpenHarmony项目中实现退货申请功能的关键设计。主要包括退货原因数据模型(支持质量问题、发货错误等5种类型,并区分证据要求)和退货申请数据模型(包含完整申请信息、状态追踪和时间记录)。系统实现了从申请创建到完成的完整流程,支持证据上传、状态变更和权限控制,为电商应用提供了完善的售后解决方案。

退货申请是电商应用中重要的售后功能。用户在收到商品后,如果商品存在质量问题、与描述不符或其他原因,可以通过退货申请流程进行售后处理。一个完整的退货申请系统需要支持多种退货原因、详细的申请信息、图片上传、物流追踪等功能。本文将详细讲解如何在 Flutter for OpenHarmony 项目中实现一个功能完整的退货申请页面,包括退货原因选择、申请信息填写、证据上传、申请状态追踪和售后进度管理等功能。
退货原因数据模型
退货申请的第一步是选择退货原因。ReturnReason 类定义了所有可能的退货原因。
// 退货原因枚举
enum ReturnReasonType {
defective, // 商品损坏/质量问题
wrongItem, // 发错商品
notAsDescribed, // 与描述不符
changedMind, // 不想要了
other, // 其他原因
}
class ReturnReason {
const ReturnReason({
required this.type, // 原因类型
required this.title, // 原因标题
required this.description, // 原因描述
required this.requiresEvidence, // 是否需要证据
});
final ReturnReasonType type;
final String title;
final String description;
final bool requiresEvidence;
// 获取所有退货原因
static List<ReturnReason> getAll() {
return [
const ReturnReason(
type: ReturnReasonType.defective,
title: '商品损坏/有质量问题',
description: '商品存在破损、缺陷或质量问题',
requiresEvidence: true,
),
const ReturnReason(
type: ReturnReasonType.wrongItem,
title: '发错商品',
description: '收到的商品与订单不符',
requiresEvidence: true,
),
const ReturnReason(
type: ReturnReasonType.notAsDescribed,
title: '与描述不符',
description: '商品与网站描述或图片不一致',
requiresEvidence: true,
),
const ReturnReason(
type: ReturnReasonType.changedMind,
title: '不想要了',
description: '改变主意,不需要此商品',
requiresEvidence: false,
),
const ReturnReason(
type: ReturnReasonType.other,
title: '其他原因',
description: '其他退货原因',
requiresEvidence: false,
),
];
}
// 根据类型获取原因
static ReturnReason? getByType(ReturnReasonType type) {
try {
return getAll().firstWhere((r) => r.type == type);
} catch (e) {
return null;
}
}
}
这个退货原因模型展示了如何管理退货原因:
原因分类:
- 质量问题:商品损坏或有质量缺陷
- 发货错误:收到的商品与订单不符
- 描述不符:商品与网站描述不一致
- 改变主意:用户不需要此商品
- 其他原因:其他退货原因
证据要求:
- 质量问题、发货错误、描述不符需要上传证据
- 改变主意和其他原因可选上传证据
- 根据原因类型动态显示证据上传选项
数据管理:
getAll()返回所有可用的退货原因getByType()根据类型查找特定原因- 支持灵活的原因查询和管理
退货申请数据模型
退货申请包含完整的申请信息和状态追踪。
// 退货申请状态
enum ReturnStatus {
pending, // 待审核
approved, // 已批准
rejected, // 已拒绝
shipped, // 已发货
received, // 已收货
completed, // 已完成
cancelled, // 已取消
}
class ReturnRequest {
const ReturnRequest({
required this.id, // 申请ID
required this.orderId, // 订单ID
required this.reason, // 退货原因
required this.description, // 详细描述
required this.status, // 申请状态
required this.createdAt, // 创建时间
this.evidenceUrls = const [], // 证据图片URL列表
this.approvedAt, // 批准时间
this.rejectionReason, // 拒绝原因
this.trackingNumber, // 退货物流单号
this.shippedAt, // 发货时间
this.receivedAt, // 收货时间
this.refundAmount, // 退款金额
this.completedAt, // 完成时间
});
final String id;
final String orderId;
final ReturnReasonType reason;
final String description;
final ReturnStatus status;
final DateTime createdAt;
final List<String> evidenceUrls;
final DateTime? approvedAt;
final String? rejectionReason;
final String? trackingNumber;
final DateTime? shippedAt;
final DateTime? receivedAt;
final double? refundAmount;
final DateTime? completedAt;
// 获取状态文本
String get statusText {
switch (status) {
case ReturnStatus.pending:
return '待审核';
case ReturnStatus.approved:
return '已批准';
case ReturnStatus.rejected:
return '已拒绝';
case ReturnStatus.shipped:
return '已发货';
case ReturnStatus.received:
return '已收货';
case ReturnStatus.completed:
return '已完成';
case ReturnStatus.cancelled:
return '已取消';
}
}
// 获取状态颜色
Color getStatusColor() {
switch (status) {
case ReturnStatus.pending:
return Colors.orange;
case ReturnStatus.approved:
return Colors.blue;
case ReturnStatus.rejected:
return Colors.red;
case ReturnStatus.shipped:
return Colors.purple;
case ReturnStatus.received:
return Colors.cyan;
case ReturnStatus.completed:
return Colors.green;
case ReturnStatus.cancelled:
return Colors.grey;
}
}
// 是否可以取消申请
bool get canBeCancelled {
return status == ReturnStatus.pending || status == ReturnStatus.approved;
}
// 是否可以编辑申请
bool get canBeEdited {
return status == ReturnStatus.pending;
}
}
这个退货申请模型展示了如何管理退货申请:
申请信息:
- 包含订单ID、退货原因、详细描述
- 记录证据图片URL列表
- 支持多张证据图片上传
状态管理:
- 从待审核到已完成的完整流程
- 每个状态对应不同的操作权限
- 支持拒绝和取消操作
时间追踪:
- 记录申请创建、批准、发货、收货等关键时间点
- 支持完整的时间线展示
- 便于用户了解申请进度
业务逻辑:
canBeCancelled判断是否可以取消申请canBeEdited判断是否可以编辑申请getStatusColor()获取状态对应的颜色
退货申请页面
退货申请页面是用户提交退货申请的主要界面。
class ReturnsPage extends StatefulWidget {
const ReturnsPage({super.key});
State<ReturnsPage> createState() => _ReturnsPageState();
}
class _ReturnsPageState extends State<ReturnsPage> {
// 选中的退货原因
ReturnReasonType? _selectedReason;
// 详细描述控制器
final _descriptionController = TextEditingController();
// 证据图片列表
final List<String> _evidenceUrls = [];
// 提交状态
bool _submitting = false;
void dispose() {
_descriptionController.dispose();
super.dispose();
}
// 获取选中的退货原因
ReturnReason? get _selectedReturnReason {
if (_selectedReason == null) return null;
return ReturnReason.getByType(_selectedReason!);
}
// 提交退货申请
Future<void> _submitReturn() async {
// 验证必填项
if (_selectedReason == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请选择退货原因')),
);
return;
}
if (_descriptionController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请填写详细描述')),
);
return;
}
// 检查是否需要证据
if (_selectedReturnReason?.requiresEvidence == true &&
_evidenceUrls.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请上传证据图片')),
);
return;
}
setState(() => _submitting = true);
try {
// 模拟API调用
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
setState(() => _submitting = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('退货申请已提交,请等待审核'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
} catch (e) {
if (!mounted) return;
setState(() => _submitting = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提交失败:$e')),
);
}
}
// 添加证据图片
void _addEvidence() {
// 模拟图片选择
setState(() {
_evidenceUrls.add(
'https://via.placeholder.com/200?text=Evidence${_evidenceUrls.length + 1}',
);
});
}
// 移除证据图片
void _removeEvidence(int index) {
setState(() => _evidenceUrls.removeAt(index));
}
Widget build(BuildContext context) {
return SimpleScaffoldPage(
title: '申请退货',
child: ListView(
padding: const EdgeInsets.all(12),
children: [
// 退货原因选择卡片
_buildReasonCard(),
const SizedBox(height: 12),
// 详细描述卡片
_buildDescriptionCard(),
const SizedBox(height: 12),
// 证据上传卡片(如果需要)
if (_selectedReturnReason?.requiresEvidence == true)
_buildEvidenceCard(),
if (_selectedReturnReason?.requiresEvidence == true)
const SizedBox(height: 12),
// 提交按钮
_buildSubmitButton(),
const SizedBox(height: 12),
],
),
);
}
// 构建退货原因卡片
Widget _buildReasonCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择退货原因',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
...ReturnReason.getAll().map((reason) {
return RadioListTile<ReturnReasonType>(
value: reason.type,
groupValue: _selectedReason,
onChanged: (value) {
setState(() => _selectedReason = value);
},
title: Text(reason.title),
subtitle: Text(reason.description),
contentPadding: EdgeInsets.zero,
);
}).toList(),
],
),
),
);
}
// 构建详细描述卡片
Widget _buildDescriptionCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'详细描述',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
TextField(
controller: _descriptionController,
maxLines: 5,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
hintText: '请详细描述退货原因和商品问题...',
counterText: '${_descriptionController.text.length}/500',
),
maxLength: 500,
),
],
),
),
);
}
// 构建证据上传卡片
Widget _buildEvidenceCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'上传证据',
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'${_evidenceUrls.length}/5',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
const SizedBox(height: 12),
// 证据图片网格
if (_evidenceUrls.isNotEmpty)
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _evidenceUrls.length,
itemBuilder: (context, index) {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
_evidenceUrls[index],
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Container(
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported),
),
),
),
Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: () => _removeEvidence(index),
child: Container(
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(4),
child: const Icon(
Icons.close,
color: Colors.white,
size: 16,
),
),
),
),
],
);
},
),
if (_evidenceUrls.isNotEmpty)
const SizedBox(height: 12),
// 添加图片按钮
if (_evidenceUrls.length < 5)
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.add_a_photo),
label: const Text('添加图片'),
onPressed: _addEvidence,
),
),
],
),
),
);
}
// 构建提交按钮
Widget _buildSubmitButton() {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submitting ? null : _submitReturn,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
_submitting ? '提交中...' : '提交退货申请',
style: const TextStyle(fontSize: 16),
),
),
),
);
}
}
这个退货申请页面展示了如何实现完整的申请流程:
页面结构:
- 退货原因选择卡片:用户选择退货原因
- 详细描述卡片:用户填写详细描述
- 证据上传卡片:根据原因类型动态显示
- 提交按钮:提交申请
用户交互:
- 单选按钮选择退货原因
- 文本输入框填写详细描述
- 图片网格展示上传的证据
- 支持添加和删除证据图片
表单验证:
- 检查必填项是否完成
- 根据原因类型检查证据要求
- 提供友好的错误提示
退货申请列表
用户可以查看所有的退货申请及其状态。
class ReturnRequestsListPage extends StatelessWidget {
const ReturnRequestsListPage({super.key});
Widget build(BuildContext context) {
// 模拟退货申请列表
final returnRequests = [
ReturnRequest(
id: 'return_001',
orderId: '1001',
reason: ReturnReasonType.defective,
description: '商品收到时已经破损,无法使用',
status: ReturnStatus.approved,
createdAt: DateTime.now().subtract(const Duration(days: 5)),
evidenceUrls: [
'https://via.placeholder.com/200?text=Damage1',
'https://via.placeholder.com/200?text=Damage2',
],
approvedAt: DateTime.now().subtract(const Duration(days: 4)),
trackingNumber: 'SF123456789',
shippedAt: DateTime.now().subtract(const Duration(days: 3)),
refundAmount: 156.99,
),
ReturnRequest(
id: 'return_002',
orderId: '1002',
reason: ReturnReasonType.notAsDescribed,
description: '商品颜色与网站图片不符',
status: ReturnStatus.pending,
createdAt: DateTime.now().subtract(const Duration(days: 2)),
evidenceUrls: [
'https://via.placeholder.com/200?text=Color',
],
),
ReturnRequest(
id: 'return_003',
orderId: '1003',
reason: ReturnReasonType.changedMind,
description: '改变主意,不需要此商品',
status: ReturnStatus.rejected,
createdAt: DateTime.now().subtract(const Duration(days: 1)),
rejectionReason: '该商品已超过退货期限',
),
];
return SimpleScaffoldPage(
title: '我的退货申请',
child: returnRequests.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.assignment_return,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
const Text('暂无退货申请'),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: returnRequests.length,
itemBuilder: (context, index) {
final request = returnRequests[index];
return _buildReturnRequestCard(context, request);
},
),
);
}
// 构建退货申请卡片
Widget _buildReturnRequestCard(
BuildContext context,
ReturnRequest request,
) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 申请头部
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'申请号:${request.id}',
style: Theme.of(context).textTheme.bodySmall,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: request.getStatusColor().withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
request.statusText,
style: TextStyle(
color: request.getStatusColor(),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
// 申请信息
Row(
children: [
Icon(
Icons.info_outline,
size: 20,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'订单号:${request.orderId}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 4),
Text(
'原因:${ReturnReason.getByType(request.reason)?.title ?? '未知'}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
const SizedBox(height: 12),
// 申请时间
Row(
children: [
Icon(
Icons.schedule,
size: 20,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Text(
_formatDate(request.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
// 拒绝原因(如果有)
if (request.rejectionReason != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Icon(
Icons.error_outline,
size: 20,
color: Colors.red,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'拒绝原因:${request.rejectionReason}',
style: const TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
),
),
],
const SizedBox(height: 12),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
// 查看详情
},
child: const Text('详情'),
),
const SizedBox(width: 8),
if (request.canBeCancelled)
TextButton(
onPressed: () {
// 取消申请
},
child: const Text(
'取消',
style: TextStyle(color: Colors.red),
),
),
],
),
],
),
),
);
}
// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
}
这个退货申请列表展示了如何管理多个申请:
列表展示:
- 显示所有退货申请
- 按时间倒序排列
- 支持空状态提示
申请信息:
- 申请号和订单号
- 退货原因和申请时间
- 申请状态和对应颜色
- 拒绝原因(如果有)
用户操作:
- 查看申请详情
- 取消待审核或已批准的申请
- 快速了解申请进度
退货申请详情页
用户可以查看退货申请的完整详情和进度。
class ReturnRequestDetailPage extends StatelessWidget {
const ReturnRequestDetailPage({
super.key,
required this.returnId, // 退货申请ID
});
final String returnId;
Widget build(BuildContext context) {
// 模拟获取申请详情
final request = ReturnRequest(
id: returnId,
orderId: '1001',
reason: ReturnReasonType.defective,
description: '商品收到时已经破损,无法使用。包装盒有明显压痕,内部商品也有划伤。',
status: ReturnStatus.approved,
createdAt: DateTime.now().subtract(const Duration(days: 5)),
evidenceUrls: [
'https://via.placeholder.com/300?text=Damage1',
'https://via.placeholder.com/300?text=Damage2',
],
approvedAt: DateTime.now().subtract(const Duration(days: 4)),
trackingNumber: 'SF123456789',
shippedAt: DateTime.now().subtract(const Duration(days: 3)),
refundAmount: 156.99,
);
return SimpleScaffoldPage(
title: '退货详情',
child: ListView(
padding: const EdgeInsets.all(12),
children: [
// 申请状态卡片
_buildStatusCard(context, request),
const SizedBox(height: 12),
// 申请信息卡片
_buildInfoCard(context, request),
const SizedBox(height: 12),
// 证据图片卡片
if (request.evidenceUrls.isNotEmpty)
_buildEvidenceCard(context, request),
if (request.evidenceUrls.isNotEmpty)
const SizedBox(height: 12),
// 物流信息卡片
if (request.trackingNumber != null)
_buildLogisticsCard(context, request),
if (request.trackingNumber != null)
const SizedBox(height: 12),
// 进度时间线卡片
_buildTimelineCard(context, request),
const SizedBox(height: 12),
// 操作按钮
_buildActionButtons(context, request),
],
),
);
}
// 构建状态卡片
Widget _buildStatusCard(BuildContext context, ReturnRequest request) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'申请状态',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: request.getStatusColor().withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
_getStatusIcon(request.status),
color: request.getStatusColor(),
size: 32,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
request.statusText,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(
_getStatusDescription(request),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
],
),
),
);
}
// 构建申请信息卡片
Widget _buildInfoCard(BuildContext context, ReturnRequest request) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'申请信息',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
_InfoRow(
label: '申请号',
value: request.id,
),
const Divider(),
_InfoRow(
label: '订单号',
value: request.orderId,
),
const Divider(),
_InfoRow(
label: '退货原因',
value: ReturnReason.getByType(request.reason)?.title ?? '未知',
),
const Divider(),
_InfoRow(
label: '申请时间',
value: _formatDateTime(request.createdAt),
),
if (request.refundAmount != null) ...[
const Divider(),
_InfoRow(
label: '退款金额',
value: '¥${request.refundAmount!.toStringAsFixed(2)}',
),
],
const SizedBox(height: 12),
Text(
'详细描述',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
),
child: Text(
request.description,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
),
);
}
// 构建证据图片卡片
Widget _buildEvidenceCard(BuildContext context, ReturnRequest request) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'证据图片',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: request.evidenceUrls.length,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
request.evidenceUrls[index],
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
Container(
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported),
),
),
);
},
),
],
),
),
);
}
// 构建物流信息卡片
Widget _buildLogisticsCard(BuildContext context, ReturnRequest request) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'退货物流',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Row(
children: [
Icon(
Icons.local_shipping,
color: Colors.blue[600],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'快递单号:${request.trackingNumber}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 4),
if (request.shippedAt != null)
Text(
'发货时间:${_formatDateTime(request.shippedAt!)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
icon: const Icon(Icons.track_changes),
label: const Text('查看物流详情'),
onPressed: () {
// 查看物流详情
},
),
),
],
),
),
);
}
// 构建进度时间线卡片
Widget _buildTimelineCard(BuildContext context, ReturnRequest request) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'处理进度',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
_TimelineItem(
title: '申请提交',
time: request.createdAt,
isCompleted: true,
isActive: request.status == ReturnStatus.pending,
),
_TimelineItem(
title: '审核批准',
time: request.approvedAt,
isCompleted: request.status.index >= ReturnStatus.approved.index,
isActive: request.status == ReturnStatus.approved,
),
_TimelineItem(
title: '已发货',
time: request.shippedAt,
isCompleted: request.status.index >= ReturnStatus.shipped.index,
isActive: request.status == ReturnStatus.shipped,
),
_TimelineItem(
title: '已收货',
time: request.receivedAt,
isCompleted: request.status.index >= ReturnStatus.received.index,
isActive: request.status == ReturnStatus.received,
),
_TimelineItem(
title: '已完成',
time: request.completedAt,
isCompleted: request.status == ReturnStatus.completed,
isActive: false,
),
],
),
),
);
}
// 构建操作按钮
Widget _buildActionButtons(BuildContext context, ReturnRequest request) {
return Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
// 联系客服
},
child: const Text('联系客服'),
),
),
const SizedBox(width: 12),
if (request.canBeCancelled)
Expanded(
child: ElevatedButton(
onPressed: () {
// 取消申请
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('取消申请'),
),
),
],
);
}
// 获取状态图标
IconData _getStatusIcon(ReturnStatus status) {
switch (status) {
case ReturnStatus.pending:
return Icons.schedule;
case ReturnStatus.approved:
return Icons.check_circle;
case ReturnStatus.rejected:
return Icons.cancel;
case ReturnStatus.shipped:
return Icons.local_shipping;
case ReturnStatus.received:
return Icons.done_all;
case ReturnStatus.completed:
return Icons.verified;
case ReturnStatus.cancelled:
return Icons.close;
}
}
// 获取状态描述
String _getStatusDescription(ReturnRequest request) {
switch (request.status) {
case ReturnStatus.pending:
return '等待审核中';
case ReturnStatus.approved:
return '已批准,请准备退货';
case ReturnStatus.rejected:
return '申请已被拒绝';
case ReturnStatus.shipped:
return '已发货,${request.trackingNumber ?? ''}';
case ReturnStatus.received:
return '已收货,处理中';
case ReturnStatus.completed:
return '已完成,退款已发放';
case ReturnStatus.cancelled:
return '申请已取消';
}
}
// 格式化日期时间
String _formatDateTime(DateTime? date) {
if (date == null) return '待处理';
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} '
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
}
// 信息行组件
class _InfoRow extends StatelessWidget {
const _InfoRow({required this.label, required this.value});
final String label;
final String value;
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: Theme.of(context).textTheme.bodySmall),
Text(value, style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
)),
],
),
);
}
}
// 时间线项组件
class _TimelineItem extends StatelessWidget {
const _TimelineItem({
required this.title,
required this.time,
required this.isCompleted,
required this.isActive,
});
final String title;
final DateTime? time;
final bool isCompleted;
final bool isActive;
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCompleted ? Colors.green : Colors.grey[300],
border: isActive ? Border.all(color: Colors.blue, width: 2) : null,
),
child: isCompleted
? const Icon(Icons.check, size: 18, color: Colors.white)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.bodySmall),
if (time != null)
Text(
_formatTime(time!),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey,
),
),
],
),
),
],
),
);
}
String _formatTime(DateTime time) {
return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')}';
}
}
这个退货申请详情页展示了完整的申请信息:
页面结构:
- 申请状态卡片:显示当前状态和进度
- 申请信息卡片:显示申请号、订单号、原因等
- 证据图片卡片:展示上传的证据
- 物流信息卡片:显示退货物流信息
- 进度时间线卡片:展示完整的处理流程
信息展示:
- 清晰的状态指示和描述
- 详细的申请信息和描述
- 证据图片网格展示
- 物流单号和发货时间
用户交互:
- 查看物流详情
- 联系客服
- 取消申请(如果允许)
退货申请状态管理
应用状态需要管理退货申请的完整生命周期。
// 扩展AppState以支持退货申请管理
extension ReturnManagement on AppState {
// 退货申请列表
final List<ReturnRequest> _returnRequests = [];
List<ReturnRequest> get returnRequests =>
List.unmodifiable(_returnRequests);
// 创建退货申请
Future<bool> createReturnRequest({
required String orderId,
required ReturnReasonType reason,
required String description,
required List<String> evidenceUrls,
}) async {
try {
// 模拟API调用
await Future.delayed(const Duration(seconds: 1));
final request = ReturnRequest(
id: 'return_${DateTime.now().millisecondsSinceEpoch}',
orderId: orderId,
reason: reason,
description: description,
status: ReturnStatus.pending,
createdAt: DateTime.now(),
evidenceUrls: evidenceUrls,
);
_returnRequests.insert(0, request);
notifyListeners();
return true;
} catch (e) {
return false;
}
}
// 更新申请状态
void updateReturnStatus(String returnId, ReturnStatus status) {
final index = _returnRequests.indexWhere((r) => r.id == returnId);
if (index >= 0) {
final request = _returnRequests[index];
_returnRequests[index] = ReturnRequest(
id: request.id,
orderId: request.orderId,
reason: request.reason,
description: request.description,
status: status,
createdAt: request.createdAt,
evidenceUrls: request.evidenceUrls,
approvedAt: status == ReturnStatus.approved
? DateTime.now()
: request.approvedAt,
trackingNumber: request.trackingNumber,
shippedAt: request.shippedAt,
receivedAt: request.receivedAt,
refundAmount: request.refundAmount,
completedAt: status == ReturnStatus.completed
? DateTime.now()
: request.completedAt,
);
notifyListeners();
}
}
// 取消申请
bool cancelReturnRequest(String returnId) {
final index = _returnRequests.indexWhere((r) => r.id == returnId);
if (index >= 0) {
final request = _returnRequests[index];
if (request.canBeCancelled) {
_returnRequests[index] = ReturnRequest(
id: request.id,
orderId: request.orderId,
reason: request.reason,
description: request.description,
status: ReturnStatus.cancelled,
createdAt: request.createdAt,
evidenceUrls: request.evidenceUrls,
);
notifyListeners();
return true;
}
}
return false;
}
// 获取特定订单的退货申请
ReturnRequest? getReturnRequestByOrderId(String orderId) {
try {
return _returnRequests.firstWhere((r) => r.orderId == orderId);
} catch (e) {
return null;
}
}
// 获取特定状态的申请
List<ReturnRequest> getReturnRequestsByStatus(ReturnStatus status) {
return _returnRequests.where((r) => r.status == status).toList();
}
// 获取待审核的申请数
int get pendingReturnCount =>
_returnRequests.where((r) => r.status == ReturnStatus.pending).length;
}
这个状态管理展示了如何管理退货申请:
申请操作:
createReturnRequest创建新的退货申请updateReturnStatus更新申请状态cancelReturnRequest取消申请
查询功能:
getReturnRequestByOrderId获取订单的申请getReturnRequestsByStatus按状态查询申请pendingReturnCount获取待审核申请数
状态管理:
- 维护申请列表
- 自动更新时间戳
- 通知所有监听者
退货流程集成
将退货功能集成到订单详情页面。
// 在订单详情页面中添加退货按钮
class OrderDetailPage extends StatelessWidget {
const OrderDetailPage({
super.key,
required this.orderId,
});
final String orderId;
Widget build(BuildContext context) {
final appState = AppStateScope.of(context);
return Scaffold(
appBar: AppBar(title: const Text('订单详情')),
body: AnimatedBuilder(
animation: appState,
builder: (context, _) {
final order = appState.orders.firstWhere(
(o) => o.id == orderId,
orElse: () => null,
);
if (order == null) {
return const Center(child: Text('订单不存在'));
}
// 检查是否已有退货申请
final existingReturn =
appState.getReturnRequestByOrderId(orderId);
return ListView(
padding: const EdgeInsets.all(12),
children: [
// 订单信息卡片
_buildOrderInfoCard(order),
const SizedBox(height: 12),
// 退货申请状态卡片
if (existingReturn != null)
_buildReturnStatusCard(context, existingReturn),
if (existingReturn != null)
const SizedBox(height: 12),
// 操作按钮
_buildActionButtons(
context,
order,
existingReturn,
),
],
);
},
),
);
}
// 构建订单信息卡片
Widget _buildOrderInfoCard(Order order) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('订单号:${order.id}'),
const SizedBox(height: 8),
Text('总金额:¥${order.totalUsd.toStringAsFixed(2)}'),
],
),
),
);
}
// 构建退货申请状态卡片
Widget _buildReturnStatusCard(
BuildContext context,
ReturnRequest request,
) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('退货申请'),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: request.getStatusColor().withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
request.statusText,
style: TextStyle(
color: request.getStatusColor(),
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Text(
'申请号:${request.id}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
}
// 构建操作按钮
Widget _buildActionButtons(
BuildContext context,
Order order,
ReturnRequest? existingReturn,
) {
return Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
// 联系客服
},
child: const Text('联系客服'),
),
),
const SizedBox(width: 12),
if (order.status == OrderStatus.delivered &&
existingReturn == null)
Expanded(
child: ElevatedButton(
onPressed: () {
// 申请退货
Navigator.of(context).pushNamed(AppRoutes.returns);
},
child: const Text('申请退货'),
),
),
if (existingReturn != null)
Expanded(
child: ElevatedButton(
onPressed: () {
// 查看退货详情
},
child: const Text('查看退货'),
),
),
],
);
}
}
这个集成展示了如何在订单流程中加入退货功能:
流程集成:
- 在订单详情页显示退货申请状态
- 已收货订单可以申请退货
- 已有申请的订单显示申请状态
用户体验:
- 快速访问退货功能
- 清晰的申请状态显示
- 便捷的操作按钮
总结
退货申请的实现涉及多个重要的技术点。首先是退货原因和申请数据模型的设计,支持多种原因和完整的申请信息。其次是退货申请页面的实现,提供直观的表单和证据上传功能。再次是申请列表和详情页面的设计,展示申请进度和状态。最后是状态管理和流程集成,确保退货功能与订单系统的无缝协作。
这种设计确保了退货申请的功能完整性和用户体验的流畅性。用户可以轻松提交退货申请、上传证据、追踪申请进度,整个售后流程自然而直观。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)