在这里插入图片描述

退货申请是电商应用中重要的售后功能。用户在收到商品后,如果商品存在质量问题、与描述不符或其他原因,可以通过退货申请流程进行售后处理。一个完整的退货申请系统需要支持多种退货原因、详细的申请信息、图片上传、物流追踪等功能。本文将详细讲解如何在 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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐