Flutter附近自助洗车点查询应用开发教程

项目概述

本教程将带你开发一个功能完整的Flutter附近自助洗车点查询应用。这款应用专为车主设计,提供智能洗车点定位、实时状态查询、预约服务和用户评价功能,让车主能够快速找到最适合的自助洗车服务点。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

应用特色

  • 智能定位系统:基于GPS定位,自动搜索附近的自助洗车点
  • 实时状态查询:显示洗车点的营业状态、设备可用性和排队情况
  • 多维度筛选:按距离、价格、评分、服务类型等条件筛选
  • 预约服务功能:支持在线预约洗车时间,避免排队等待
  • 用户评价系统:查看其他用户的真实评价和照片分享
  • 导航集成:一键导航到选定的洗车点
  • 收藏管理:收藏常用洗车点,快速访问
  • 价格对比:对比不同洗车点的服务价格和优惠活动

技术栈

  • 框架:Flutter 3.x
  • 语言:Dart
  • UI组件:Material Design 3
  • 状态管理:StatefulWidget
  • 地图服务:模拟地图功能(可集成高德地图、百度地图等)
  • 数据存储:内存存储(可扩展为本地数据库)

项目结构设计

核心数据模型

1. 洗车点模型(CarWashStation)
class CarWashStation {
  final String id;              // 唯一标识
  final String name;            // 洗车点名称
  final String address;         // 详细地址
  final double latitude;        // 纬度
  final double longitude;       // 经度
  final double distance;        // 距离用户的距离(公里)
  final String phone;           // 联系电话
  final List<String> services;  // 提供的服务类型
  final Map<String, double> prices; // 服务价格
  final double rating;          // 平均评分
  final int reviewCount;        // 评价数量
  final String operatingHours;  // 营业时间
  final StationStatus status;   // 当前状态
  final List<String> photos;    // 照片列表
  final List<String> features;  // 特色功能
  final int availableSpots;     // 可用车位数
  final int totalSpots;         // 总车位数
  final DateTime lastUpdated;   // 最后更新时间
  bool isFavorite;             // 是否收藏
}
2. 用户评价模型(Review)
class Review {
  final String id;
  final String stationId;
  final String userId;
  final String userName;
  final String userAvatar;
  final double rating;
  final String content;
  final List<String> photos;
  final DateTime createTime;
  final List<String> tags;      // 评价标签
  final String serviceType;     // 使用的服务类型
  final bool isRecommended;     // 是否推荐
}
3. 预约记录模型(Reservation)
class Reservation {
  final String id;
  final String stationId;
  final String userId;
  final DateTime reservationTime;
  final String serviceType;
  final double estimatedPrice;
  final ReservationStatus status;
  final String notes;
  final DateTime createTime;
  final String? cancellationReason;
}
4. 服务类型模型(ServiceType)
class ServiceType {
  final String id;
  final String name;
  final String description;
  final double basePrice;
  final int estimatedDuration; // 预计耗时(分钟)
  final List<String> includes; // 包含的服务项目
  final String icon;
}

枚举定义

enum StationStatus {
  open,         // 营业中
  closed,       // 已关闭
  busy,         // 繁忙
  maintenance,  // 维护中
  full,         // 已满
}

enum ReservationStatus {
  pending,      // 待确认
  confirmed,    // 已确认
  inProgress,   // 进行中
  completed,    // 已完成
  cancelled,    // 已取消
  expired,      // 已过期
}

enum FilterType {
  distance,     // 按距离
  rating,       // 按评分
  price,        // 按价格
  availability, // 按可用性
}

enum ServiceCategory {
  basic,        // 基础洗车
  premium,      // 精洗
  interior,     // 内饰清洁
  waxing,       // 打蜡
  detailing,    // 精细护理
}

页面架构

应用采用底部导航栏设计,包含五个主要页面:

  1. 地图页面:显示附近洗车点的地图分布和实时信息
  2. 列表页面:以列表形式展示洗车点详细信息
  3. 预约页面:管理用户的预约记录和预约服务
  4. 收藏页面:管理收藏的洗车点和快速访问
  5. 个人页面:用户信息、设置和应用功能

详细实现步骤

第一步:项目初始化

创建新的Flutter项目:

flutter create car_wash_finder
cd car_wash_finder

第二步:主应用结构

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '附近自助洗车点查询',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const CarWashFinderHomePage(),
    );
  }
}

第三步:数据初始化

创建示例洗车点数据:

void _initializeData() {
  _carWashStations = [
    CarWashStation(
      id: '1',
      name: '蓝天自助洗车',
      address: '北京市朝阳区建国路88号',
      latitude: 39.9042,
      longitude: 116.4074,
      distance: 0.8,
      phone: '010-12345678',
      services: ['基础洗车', '精洗', '内饰清洁', '打蜡'],
      prices: {
        '基础洗车': 15.0,
        '精洗': 25.0,
        '内饰清洁': 20.0,
        '打蜡': 35.0,
      },
      rating: 4.5,
      reviewCount: 128,
      operatingHours: '24小时营业',
      status: StationStatus.open,
      photos: [],
      features: ['24小时营业', '免费停车', '移动支付', '会员优惠'],
      availableSpots: 3,
      totalSpots: 6,
      lastUpdated: DateTime.now().subtract(const Duration(minutes: 5)),
      isFavorite: true,
    ),
    // 更多洗车点数据...
  ];
}

第四步:地图页面实现

地图主界面
Widget _buildMapPage() {
  return Stack(
    children: [
      // 地图容器
      Container(
        width: double.infinity,
        height: double.infinity,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [Colors.blue.shade100, Colors.blue.shade50],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
          ),
        ),
        child: _buildMapView(),
      ),
      
      // 顶部搜索栏
      Positioned(
        top: 0,
        left: 0,
        right: 0,
        child: _buildSearchBar(),
      ),
      
      // 底部洗车点信息卡片
      if (_selectedStation != null)
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: _buildStationInfoCard(_selectedStation!),
        ),
      
      // 右侧功能按钮
      Positioned(
        right: 16,
        top: 100,
        child: _buildMapControls(),
      ),
    ],
  );
}

Widget _buildMapView() {
  return Container(
    child: Stack(
      children: [
        // 模拟地图背景
        Container(
          decoration: BoxDecoration(
            color: Colors.grey.shade200,
            image: const DecorationImage(
              image: AssetImage('assets/map_pattern.png'),
              repeat: ImageRepeat.repeat,
              opacity: 0.1,
            ),
          ),
        ),
        
        // 洗车点标记
        ..._filteredStations.map((station) => _buildStationMarker(station)),
        
        // 用户位置标记
        _buildUserLocationMarker(),
      ],
    ),
  );
}

Widget _buildStationMarker(CarWashStation station) {
  final isSelected = _selectedStation?.id == station.id;
  
  return Positioned(
    left: _getMarkerX(station.longitude),
    top: _getMarkerY(station.latitude),
    child: GestureDetector(
      onTap: () => _selectStation(station),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        width: isSelected ? 60 : 40,
        height: isSelected ? 60 : 40,
        decoration: BoxDecoration(
          color: _getStatusColor(station.status),
          shape: BoxShape.circle,
          border: Border.all(color: Colors.white, width: 3),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.2),
              blurRadius: 8,
              spreadRadius: 2,
            ),
          ],
        ),
        child: Icon(
          Icons.local_car_wash,
          color: Colors.white,
          size: isSelected ? 30 : 20,
        ),
      ),
    ),
  );
}
搜索栏组件
Widget _buildSearchBar() {
  return Container(
    margin: const EdgeInsets.all(16),
    padding: const EdgeInsets.symmetric(horizontal: 16),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(25),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.1),
          blurRadius: 10,
          spreadRadius: 2,
        ),
      ],
    ),
    child: Row(
      children: [
        const Icon(Icons.search, color: Colors.grey),
        const SizedBox(width: 12),
        Expanded(
          child: TextField(
            decoration: const InputDecoration(
              hintText: '搜索附近洗车点...',
              border: InputBorder.none,
            ),
            onChanged: (value) {
              setState(() {
                _searchQuery = value;
              });
              _filterStations();
            },
          ),
        ),
        IconButton(
          icon: const Icon(Icons.tune, color: Colors.blue),
          onPressed: _showFilterDialog,
        ),
      ],
    ),
  );
}

第五步:列表页面实现

洗车点列表
Widget _buildListPage() {
  return Column(
    children: [
      // 排序和筛选栏
      Container(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Expanded(
              child: DropdownButtonFormField<FilterType>(
                decoration: const InputDecoration(
                  labelText: '排序方式',
                  border: OutlineInputBorder(),
                ),
                value: _currentFilter,
                items: FilterType.values.map((filter) => DropdownMenuItem(
                  value: filter,
                  child: Text(_getFilterText(filter)),
                )).toList(),
                onChanged: (value) {
                  setState(() {
                    _currentFilter = value!;
                  });
                  _sortStations();
                },
              ),
            ),
            const SizedBox(width: 12),
            ElevatedButton.icon(
              onPressed: _showFilterDialog,
              icon: const Icon(Icons.filter_list),
              label: const Text('筛选'),
            ),
          ],
        ),
      ),
      
      // 洗车点列表
      Expanded(
        child: _filteredStations.isEmpty
            ? _buildEmptyState()
            : ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: _filteredStations.length,
                itemBuilder: (context, index) => _buildStationCard(_filteredStations[index]),
              ),
      ),
    ],
  );
}

Widget _buildStationCard(CarWashStation station) {
  return Card(
    elevation: 4,
    margin: const EdgeInsets.only(bottom: 16),
    child: InkWell(
      onTap: () => _showStationDetail(station),
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 标题行
            Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        station.name,
                        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 4),
                      Row(
                        children: [
                          Icon(Icons.location_on, size: 16, color: Colors.grey.shade600),
                          const SizedBox(width: 4),
                          Expanded(
                            child: Text(
                              station.address,
                              style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    Container(
                      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                      decoration: BoxDecoration(
                        color: _getStatusColor(station.status).withOpacity(0.1),
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: Text(
                        _getStatusText(station.status),
                        style: TextStyle(
                          color: _getStatusColor(station.status),
                          fontSize: 12,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      '${station.distance.toStringAsFixed(1)}km',
                      style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
                    ),
                  ],
                ),
              ],
            ),
            
            const SizedBox(height: 12),
            
            // 评分和价格信息
            Row(
              children: [
                Row(
                  children: [
                    Icon(Icons.star, color: Colors.amber, size: 16),
                    const SizedBox(width: 4),
                    Text('${station.rating.toStringAsFixed(1)}', 
                         style: const TextStyle(fontWeight: FontWeight.w500)),
                    Text(' (${station.reviewCount})', 
                         style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
                  ],
                ),
                const Spacer(),
                Text('起价 ¥${_getMinPrice(station).toStringAsFixed(0)}', 
                     style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green)),
              ],
            ),
            
            const SizedBox(height: 12),
            
            // 服务标签
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: station.services.take(3).map((service) => Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: Colors.blue.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Text(
                  service,
                  style: const TextStyle(fontSize: 12, color: Colors.blue),
                ),
              )).toList(),
            ),
            
            const SizedBox(height: 12),
            
            // 操作按钮
            Row(
              children: [
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () => _navigateToStation(station),
                    icon: const Icon(Icons.directions, size: 16),
                    label: const Text('导航'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: () => _makeReservation(station),
                    icon: const Icon(Icons.schedule, size: 16),
                    label: const Text('预约'),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton(
                  onPressed: () => _toggleFavorite(station),
                  icon: Icon(
                    station.isFavorite ? Icons.favorite : Icons.favorite_border,
                    color: station.isFavorite ? Colors.red : Colors.grey,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

第六步:预约功能实现

预约页面
Widget _buildReservationPage() {
  return DefaultTabController(
    length: 2,
    child: Column(
      children: [
        const TabBar(
          tabs: [
            Tab(text: '我的预约'),
            Tab(text: '预约历史'),
          ],
        ),
        Expanded(
          child: TabBarView(
            children: [
              _buildActiveReservations(),
              _buildReservationHistory(),
            ],
          ),
        ),
      ],
    ),
  );
}

Widget _buildActiveReservations() {
  final activeReservations = _reservations.where((r) => 
      r.status == ReservationStatus.pending || 
      r.status == ReservationStatus.confirmed ||
      r.status == ReservationStatus.inProgress).toList();

  return activeReservations.isEmpty
      ? _buildEmptyState('暂无活跃预约', '快去预约洗车服务吧!')
      : ListView.builder(
          padding: const EdgeInsets.all(16),
          itemCount: activeReservations.length,
          itemBuilder: (context, index) => _buildReservationCard(activeReservations[index]),
        );
}

Widget _buildReservationCard(Reservation reservation) {
  final station = _getStationById(reservation.stationId);
  if (station == null) return const SizedBox();

  return Card(
    elevation: 4,
    margin: const EdgeInsets.only(bottom: 16),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 预约状态和时间
          Row(
            children: [
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                decoration: BoxDecoration(
                  color: _getReservationStatusColor(reservation.status).withOpacity(0.1),
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Text(
                  _getReservationStatusText(reservation.status),
                  style: TextStyle(
                    color: _getReservationStatusColor(reservation.status),
                    fontSize: 12,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              const Spacer(),
              Text(
                _formatDateTime(reservation.reservationTime),
                style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
              ),
            ],
          ),
          
          const SizedBox(height: 12),
          
          // 洗车点信息
          Row(
            children: [
              Icon(Icons.local_car_wash, color: Colors.blue, size: 20),
              const SizedBox(width: 8),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(station.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                    Text(station.address, style: TextStyle(color: Colors.grey.shade600, fontSize: 14)),
                  ],
                ),
              ),
            ],
          ),
          
          const SizedBox(height: 12),
          
          // 服务信息
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.grey.shade50,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('服务类型', style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
                      Text(reservation.serviceType, style: const TextStyle(fontWeight: FontWeight.w500)),
                    ],
                  ),
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    Text('预估费用', style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
                    Text(${reservation.estimatedPrice.toStringAsFixed(0)}', 
                         style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green)),
                  ],
                ),
              ],
            ),
          ),
          
          if (reservation.notes.isNotEmpty) ...[
            const SizedBox(height: 8),
            Text('备注: ${reservation.notes}', style: TextStyle(color: Colors.grey.shade600, fontSize: 14)),
          ],
          
          const SizedBox(height: 12),
          
          // 操作按钮
          Row(
            children: [
              if (reservation.status == ReservationStatus.pending || reservation.status == ReservationStatus.confirmed) ...[
                Expanded(
                  child: OutlinedButton(
                    onPressed: () => _cancelReservation(reservation),
                    child: const Text('取消预约'),
                  ),
                ),
                const SizedBox(width: 12),
              ],
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: () => _navigateToStation(station),
                  icon: const Icon(Icons.directions, size: 16),
                  label: const Text('导航前往'),
                ),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

第七步:洗车点详情页面

class StationDetailPage extends StatefulWidget {
  final CarWashStation station;
  final Function(CarWashStation) onFavoriteToggle;

  const StationDetailPage({
    super.key,
    required this.station,
    required this.onFavoriteToggle,
  });

  
  State<StationDetailPage> createState() => _StationDetailPageState();
}

class _StationDetailPageState extends State<StationDetailPage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.station.name),
        actions: [
          IconButton(
            icon: Icon(
              widget.station.isFavorite ? Icons.favorite : Icons.favorite_border,
              color: widget.station.isFavorite ? Colors.red : null,
            ),
            onPressed: () {
              widget.onFavoriteToggle(widget.station);
              setState(() {});
            },
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 照片轮播
            _buildPhotoCarousel(),
            
            // 基本信息
            _buildBasicInfo(),
            
            // 服务价格
            _buildServicePrices(),
            
            // 特色功能
            _buildFeatures(),
            
            // 用户评价
            _buildReviews(),
          ],
        ),
      ),
      bottomNavigationBar: _buildBottomActions(),
    );
  }

  Widget _buildBasicInfo() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Expanded(
                child: Text(
                  widget.station.name,
                  style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
              ),
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                decoration: BoxDecoration(
                  color: _getStatusColor(widget.station.status).withOpacity(0.1),
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Text(
                  _getStatusText(widget.station.status),
                  style: TextStyle(
                    color: _getStatusColor(widget.station.status),
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ],
          ),
          
          const SizedBox(height: 12),
          
          // 评分和距离
          Row(
            children: [
              Icon(Icons.star, color: Colors.amber, size: 20),
              const SizedBox(width: 4),
              Text('${widget.station.rating.toStringAsFixed(1)}', 
                   style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
              Text(' (${widget.station.reviewCount}条评价)', 
                   style: TextStyle(color: Colors.grey.shade600)),
              const Spacer(),
              Icon(Icons.location_on, color: Colors.grey.shade600, size: 16),
              const SizedBox(width: 4),
              Text('${widget.station.distance.toStringAsFixed(1)}km', 
                   style: TextStyle(color: Colors.grey.shade600)),
            ],
          ),
          
          const SizedBox(height: 12),
          
          // 地址和联系方式
          _buildInfoRow(Icons.location_on, widget.station.address),
          _buildInfoRow(Icons.phone, widget.station.phone),
          _buildInfoRow(Icons.access_time, widget.station.operatingHours),
          
          const SizedBox(height: 12),
          
          // 车位信息
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.blue.withOpacity(0.1),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Row(
              children: [
                Icon(Icons.local_parking, color: Colors.blue),
                const SizedBox(width: 8),
                Text('车位情况: '),
                Text('${widget.station.availableSpots}/${widget.station.totalSpots}', 
                     style: const TextStyle(fontWeight: FontWeight.bold)),
                const Spacer(),
                Text(
                  widget.station.availableSpots > 0 ? '有空位' : '已满',
                  style: TextStyle(
                    color: widget.station.availableSpots > 0 ? Colors.green : Colors.red,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoRow(IconData icon, String text) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Row(
        children: [
          Icon(icon, size: 16, color: Colors.grey.shade600),
          const SizedBox(width: 8),
          Expanded(child: Text(text, style: TextStyle(color: Colors.grey.shade700))),
        ],
      ),
    );
  }

  Widget _buildBottomActions() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 10,
            spreadRadius: 2,
          ),
        ],
      ),
      child: Row(
        children: [
          Expanded(
            child: OutlinedButton.icon(
              onPressed: () => _navigateToStation(),
              icon: const Icon(Icons.directions),
              label: const Text('导航'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: ElevatedButton.icon(
              onPressed: () => _makeReservation(),
              icon: const Icon(Icons.schedule),
              label: const Text('立即预约'),
            ),
          ),
        ],
      ),
    );
  }
}

核心功能详解

1. 地理位置计算

实现距离计算和附近搜索:

class LocationService {
  static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
    const double earthRadius = 6371; // 地球半径(公里)
    
    double dLat = _toRadians(lat2 - lat1);
    double dLon = _toRadians(lon2 - lon1);
    
    double a = sin(dLat / 2) * sin(dLat / 2) +
        cos(_toRadians(lat1)) * cos(_toRadians(lat2)) *
        sin(dLon / 2) * sin(dLon / 2);
    
    double c = 2 * atan2(sqrt(a), sqrt(1 - a));
    
    return earthRadius * c;
  }
  
  static double _toRadians(double degree) {
    return degree * pi / 180;
  }
  
  static List<CarWashStation> findNearbyStations(
    List<CarWashStation> stations,
    double userLat,
    double userLon,
    double radiusKm,
  ) {
    return stations.where((station) {
      double distance = calculateDistance(userLat, userLon, station.latitude, station.longitude);
      return distance <= radiusKm;
    }).toList();
  }
}

2. 智能筛选系统

实现多维度筛选功能:

class StationFilter {
  static List<CarWashStation> filterStations(
    List<CarWashStation> stations, {
    double? maxDistance,
    double? minRating,
    double? maxPrice,
    List<String>? requiredServices,
    StationStatus? status,
    bool? hasAvailableSpots,
  }) {
    return stations.where((station) {
      // 距离筛选
      if (maxDistance != null && station.distance > maxDistance) {
        return false;
      }
      
      // 评分筛选
      if (minRating != null && station.rating < minRating) {
        return false;
      }
      
      // 价格筛选
      if (maxPrice != null) {
        double minStationPrice = station.prices.values.reduce(min);
        if (minStationPrice > maxPrice) {
          return false;
        }
      }
      
      // 服务类型筛选
      if (requiredServices != null && requiredServices.isNotEmpty) {
        bool hasAllServices = requiredServices.every((service) => 
            station.services.contains(service));
        if (!hasAllServices) {
          return false;
        }
      }
      
      // 状态筛选
      if (status != null && station.status != status) {
        return false;
      }
      
      // 车位可用性筛选
      if (hasAvailableSpots == true && station.availableSpots <= 0) {
        return false;
      }
      
      return true;
    }).toList();
  }
}

3. 预约管理系统

实现预约创建和管理:

class ReservationManager {
  static Future<Reservation> createReservation({
    required String stationId,
    required String userId,
    required DateTime reservationTime,
    required String serviceType,
    required double estimatedPrice,
    String notes = '',
  }) async {
    final reservation = Reservation(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      stationId: stationId,
      userId: userId,
      reservationTime: reservationTime,
      serviceType: serviceType,
      estimatedPrice: estimatedPrice,
      status: ReservationStatus.pending,
      notes: notes,
      createTime: DateTime.now(),
      cancellationReason: null,
    );
    
    // 模拟网络请求
    await Future.delayed(const Duration(seconds: 1));
    
    return reservation;
  }
  
  static Future<bool> cancelReservation(String reservationId, String reason) async {
    // 模拟取消预约
    await Future.delayed(const Duration(milliseconds: 500));
    return true;
  }
  
  static bool canCancelReservation(Reservation reservation) {
    // 预约时间前30分钟可以取消
    final now = DateTime.now();
    final timeDiff = reservation.reservationTime.difference(now);
    return timeDiff.inMinutes > 30;
  }
}

性能优化

1. 地图渲染优化

使用虚拟化技术优化大量标记点的渲染:

class MapRenderer {
  static List<CarWashStation> getVisibleStations(
    List<CarWashStation> stations,
    double centerLat,
    double centerLon,
    double zoomLevel,
  ) {
    // 根据缩放级别计算可见范围
    double visibleRadius = _calculateVisibleRadius(zoomLevel);
    
    return stations.where((station) {
      double distance = LocationService.calculateDistance(
        centerLat, centerLon, station.latitude, station.longitude);
      return distance <= visibleRadius;
    }).toList();
  }
  
  static double _calculateVisibleRadius(double zoomLevel) {
    // 根据缩放级别返回可见半径
    return 10.0 / zoomLevel; // 简化计算
  }
}

2. 数据缓存策略

实现智能数据缓存:

class DataCache {
  static final Map<String, CacheItem> _cache = {};
  static const Duration cacheExpiry = Duration(minutes: 5);
  
  static void cacheStationData(String key, List<CarWashStation> data) {
    _cache[key] = CacheItem(data: data, timestamp: DateTime.now());
  }
  
  static List<CarWashStation>? getCachedStationData(String key) {
    final item = _cache[key];
    if (item == null) return null;
    
    if (DateTime.now().difference(item.timestamp) > cacheExpiry) {
      _cache.remove(key);
      return null;
    }
    
    return item.data;
  }
  
  static void clearExpiredCache() {
    final now = DateTime.now();
    _cache.removeWhere((key, item) => 
        now.difference(item.timestamp) > cacheExpiry);
  }
}

class CacheItem {
  final List<CarWashStation> data;
  final DateTime timestamp;
  
  CacheItem({required this.data, required this.timestamp});
}

扩展功能

1. 地图服务集成

可以集成高德地图或百度地图SDK:

dependencies:
  flutter:
    sdk: flutter
  amap_flutter_map: ^3.0.0  # 高德地图
  # 或者
  flutter_baidu_mapapi_map: ^6.0.0  # 百度地图

2. 定位服务

使用geolocator插件获取用户位置:

dependencies:
  geolocator: ^10.1.0

3. 推送通知

集成firebase_messaging实现预约提醒:

dependencies:
  firebase_messaging: ^14.7.9

总结

本教程详细介绍了Flutter附近自助洗车点查询应用的完整开发过程,涵盖了:

  • 数据模型设计:洗车点信息、用户评价、预约记录的合理建模
  • UI界面开发:Material Design 3风格的现代化界面
  • 功能实现:地图展示、列表查询、预约管理、收藏功能
  • 地理位置服务:距离计算、附近搜索、导航集成
  • 性能优化:地图渲染优化、数据缓存策略
  • 扩展功能:地图SDK集成、定位服务、推送通知

这款应用不仅功能完整,而且代码结构清晰,易于维护和扩展。通过本教程的学习,你可以掌握Flutter应用开发的核心技能,为后续开发更复杂的位置服务类应用打下坚实基础。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐