Flutter实时充电桩查询:智能定位附近充电站

项目简介

实时充电桩查询是一款专为电动车主打造的Flutter应用,帮助用户快速找到附近的充电站,实时查看充电桩空闲状态和充电费用。通过智能筛选和排序功能,让充电变得更加便捷高效。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

核心功能

  • 充电站查询:显示附近充电站列表
  • 实时状态:查看充电桩空闲/充电中/故障状态
  • 费用查询:显示电费和服务费明细
  • 智能筛选:按充电类型、可用性筛选
  • 多种排序:按距离、可用数、评分排序
  • 详细信息:运营商、营业时间、联系电话
  • 充电桩详情:功率、接口类型、实时状态
  • 配套设施:停车场、便利店、WiFi等
  • 费用计算:预估充电时间和费用
  • 数据刷新:实时更新充电桩状态
  • 数据持久化:本地保存充电站数据

技术特点

  • Material Design 3设计风格
  • TabBar多页面架构
  • 实时状态更新
  • 智能筛选排序
  • 费用自动计算
  • 响应式卡片布局
  • SharedPreferences数据持久化
  • 模拟数据生成

核心代码实现

1. 充电站数据模型

class ChargingStation {
  String id;
  String name;              // 充电站名称
  String address;           // 地址
  double latitude;          // 纬度
  double longitude;         // 经度
  String operator;          // 运营商
  List<ChargingPile> piles; // 充电桩列表
  double distance;          // 距离
  String openTime;          // 营业时间
  String phone;             // 联系电话
  List<String> facilities;  // 配套设施
  double rating;            // 评分

  // 总充电桩数
  int get totalPiles => piles.length;
  
  // 空闲充电桩数
  int get availablePiles => piles.where((p) => p.status == '空闲').length;
  
  // 充电中数量
  int get chargingPiles => piles.where((p) => p.status == '充电中').length;
  
  // 故障数量
  int get faultPiles => piles.where((p) => p.status == '故障').length;
  
  // 可用性文本
  String get availabilityText => '$availablePiles/$totalPiles 可用';
}

模型字段说明

字段 类型 说明
id String 唯一标识符
name String 充电站名称
address String 详细地址
latitude double 纬度坐标
longitude double 经度坐标
operator String 运营商名称
piles List 充电桩列表
distance double 距离(km)
openTime String 营业时间
phone String 联系电话
facilities List 配套设施
rating double 用户评分

计算属性

  • totalPiles:总充电桩数量
  • availablePiles:空闲充电桩数量
  • chargingPiles:充电中数量
  • faultPiles:故障数量
  • availabilityText:可用性描述

2. 充电桩数据模型

class ChargingPile {
  String id;              // 充电桩编号
  String type;            // 类型(快充/慢充)
  double power;           // 功率(kW)
  String status;          // 状态
  double price;           // 电费
  double serviceFee;      // 服务费
  String connector;       // 接口类型

  // 总费用
  double get totalPrice => price + serviceFee;

  // 功率文本
  String get powerText => '${power}kW';

  // 状态颜色
  Color get statusColor {
    switch (status) {
      case '空闲':
        return Colors.green;
      case '充电中':
        return Colors.orange;
      case '故障':
        return Colors.red;
      default:
        return Colors.grey;
    }
  }
}

充电桩字段说明

字段 类型 说明
id String 充电桩编号
type String 快充/慢充
power double 功率(kW)
status String 空闲/充电中/故障
price double 电费(元/度)
serviceFee double 服务费(元/度)
connector String 接口类型

计算属性

  • totalPrice:总费用(电费+服务费)
  • powerText:功率显示文本
  • statusColor:状态对应颜色

3. 智能筛选排序

void _applyFilters() {
  setState(() {
    _filteredStations = _stations.where((station) {
      // 筛选有空闲桩的站点
      if (_onlyAvailable && station.availablePiles == 0) {
        return false;
      }
      // 筛选充电类型
      if (_filterType != '全部') {
        final hasType = station.piles.any((pile) => pile.type == _filterType);
        if (!hasType) return false;
      }
      return true;
    }).toList();

    // 排序
    switch (_sortBy) {
      case '距离':
        _filteredStations.sort((a, b) => a.distance.compareTo(b.distance));
        break;
      case '可用数':
        _filteredStations.sort((a, b) => b.availablePiles.compareTo(a.availablePiles));
        break;
      case '评分':
        _filteredStations.sort((a, b) => b.rating.compareTo(a.rating));
        break;
    }
  });
}

筛选条件

  • 仅显示有空闲桩
  • 按充电类型筛选(全部/快充/慢充)

排序方式

  • 按距离排序(从近到远)
  • 按可用数排序(从多到少)
  • 按评分排序(从高到低)

4. 筛选对话框

void _showFilterDialog() {
  showModalBottomSheet(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setModalState) => Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('筛选与排序'),
            // 排序方式
            Wrap(
              spacing: 8,
              children: ['距离', '可用数', '评分'].map((sort) {
                return ChoiceChip(
                  label: Text(sort),
                  selected: _sortBy == sort,
                  onSelected: (selected) {
                    setModalState(() => _sortBy = sort);
                    setState(() => _sortBy = sort);
                    _applyFilters();
                  },
                );
              }).toList(),
            ),
            // 充电类型
            Wrap(
              spacing: 8,
              children: ['全部', '快充', '慢充'].map((type) {
                return ChoiceChip(
                  label: Text(type),
                  selected: _filterType == type,
                  onSelected: (selected) {
                    setModalState(() => _filterType = type);
                    setState(() => _filterType = type);
                    _applyFilters();
                  },
                );
              }).toList(),
            ),
            // 仅显示有空闲桩
            SwitchListTile(
              title: const Text('仅显示有空闲桩'),
              value: _onlyAvailable,
              onChanged: (value) {
                setModalState(() => _onlyAvailable = value);
                setState(() => _onlyAvailable = value);
                _applyFilters();
              },
            ),
          ],
        ),
      ),
    ),
  );
}

ModalBottomSheet特点

  • 从底部弹出
  • StatefulBuilder管理内部状态
  • ChoiceChip单选筹码
  • SwitchListTile开关选项

5. 状态统计卡片

Widget _buildSummaryCard() {
  final totalStations = _filteredStations.length;
  final totalPiles = _filteredStations.fold(0, (sum, s) => sum + s.totalPiles);
  final availablePiles = _filteredStations.fold(0, (sum, s) => sum + s.availablePiles);

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _buildSummaryItem(Icons.ev_station, '充电站', '$totalStations个'),
          _buildSummaryItem(Icons.power, '充电桩', '$totalPiles个'),
          _buildSummaryItem(
            Icons.check_circle,
            '可用',
            '$availablePiles个',
            color: Colors.green,
          ),
        ],
      ),
    ),
  );
}

统计信息

  • 充电站总数
  • 充电桩总数
  • 可用充电桩数

6. 充电站卡片

Widget _buildStationCard(ChargingStation station) {
  return Card(
    child: InkWell(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (_) => StationDetailPage(station: station),
          ),
        );
      },
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 标题和可用性
            Row(
              children: [
                Expanded(child: Text(station.name)),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 4,
                  ),
                  decoration: BoxDecoration(
                    color: station.availablePiles > 0
                        ? Colors.green[50]
                        : Colors.red[50],
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    station.availabilityText,
                    style: TextStyle(
                      color: station.availablePiles > 0
                          ? Colors.green
                          : Colors.red,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ],
            ),
            // 运营商和评分
            Row(
              children: [
                const Icon(Icons.business, size: 14),
                Text(station.operator),
                const Icon(Icons.star, size: 14, color: Colors.amber),
                Text(station.rating.toStringAsFixed(1)),
              ],
            ),
            // 地址
            Row(
              children: [
                const Icon(Icons.location_on, size: 14),
                Expanded(child: Text(station.address)),
              ],
            ),
            // 距离和营业时间
            Row(
              children: [
                const Icon(Icons.navigation, size: 14, color: Colors.blue),
                Text('${station.distance.toStringAsFixed(1)}km'),
                const Icon(Icons.access_time, size: 14),
                Text(station.openTime),
              ],
            ),
            // 状态统计
            Row(
              children: [
                _buildPileStatusChip('空闲', station.availablePiles, Colors.green),
                _buildPileStatusChip('充电中', station.chargingPiles, Colors.orange),
                _buildPileStatusChip('故障', station.faultPiles, Colors.red),
              ],
            ),
            // 配套设施
            if (station.facilities.isNotEmpty)
              Wrap(
                children: station.facilities.map((facility) {
                  return Chip(label: Text(facility));
                }).toList(),
              ),
          ],
        ),
      ),
    ),
  );
}

卡片信息

  • 充电站名称和可用性
  • 运营商和评分
  • 地址和距离
  • 营业时间
  • 状态统计(空闲/充电中/故障)
  • 配套设施

7. 状态指示器

Widget _buildPileStatusChip(String label, int count, Color color) {
  return Container(
    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    decoration: BoxDecoration(
      color: color.withOpacity(0.1),
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: color.withOpacity(0.3)),
    ),
    child: Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: 8,
          height: 8,
          decoration: BoxDecoration(
            color: color,
            shape: BoxShape.circle,
          ),
        ),
        const SizedBox(width: 4),
        Text(
          '$label $count',
          style: TextStyle(
            color: color,
            fontSize: 12,
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    ),
  );
}

状态指示器特点

  • 圆点颜色标识
  • 半透明背景
  • 边框装饰
  • 数量显示

8. 充电桩详情卡片

Widget _buildPileCard(ChargingPile pile) {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          // 充电桩图标和信息
          Row(
            children: [
              Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: pile.statusColor.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Icon(
                  Icons.ev_station,
                  color: pile.statusColor,
                  size: 32,
                ),
              ),
              Expanded(
                child: Column(
                  children: [
                    Row(
                      children: [
                        Text(pile.id),
                        Container(
                          child: Text(pile.type),
                        ),
                      ],
                    ),
                    Row(
                      children: [
                        Icon(Icons.bolt, size: 14),
                        Text(pile.powerText),
                        Icon(Icons.cable, size: 14),
                        Text(pile.connector),
                      ],
                    ),
                  ],
                ),
              ),
              Container(
                child: Text(pile.status),
              ),
            ],
          ),
          // 费用信息
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Column(
                children: [
                  const Text('电费'),
                  Text(${pile.price.toStringAsFixed(2)}/度'),
                ],
              ),
              Column(
                children: [
                  const Text('服务费'),
                  Text(${pile.serviceFee.toStringAsFixed(2)}/度'),
                ],
              ),
              Column(
                children: [
                  const Text('总计'),
                  Text(
                    ${pile.totalPrice.toStringAsFixed(2)}/度',
                    style: const TextStyle(color: Colors.red),
                  ),
                ],
              ),
            ],
          ),
          // 开始充电按钮
          if (pile.status == '空闲')
            FilledButton.icon(
              onPressed: () => _showChargingDialog(pile),
              icon: const Icon(Icons.flash_on),
              label: const Text('开始充电'),
            ),
        ],
      ),
    ),
  );
}

充电桩卡片信息

  • 充电桩编号和类型
  • 功率和接口类型
  • 实时状态
  • 电费和服务费
  • 总费用
  • 开始充电按钮(仅空闲时显示)

9. 充电费用计算

void _showChargingDialog(ChargingPile pile) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('开始充电'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text('充电桩:${pile.id}'),
          Text('功率:${pile.powerText}'),
          Text('费用:¥${pile.totalPrice.toStringAsFixed(2)}/度'),
          const Text('预计充电30度需要:'),
          Text(
            '费用:¥${(pile.totalPrice * 30).toStringAsFixed(2)}',
            style: const TextStyle(fontSize: 18, color: Colors.red),
          ),
          Text(
            '时间:约${(30 / pile.power * 60).toStringAsFixed(0)}分钟',
            style: const TextStyle(fontSize: 18, color: Colors.blue),
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        FilledButton(
          onPressed: () {
            Navigator.pop(context);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('充电已开始,请插入充电枪')),
            );
          },
          child: const Text('确认'),
        ),
      ],
    ),
  );
}

费用计算公式

  • 总费用 = (电费 + 服务费) × 充电度数
  • 充电时间 = 充电度数 / 充电功率

示例

  • 充电30度
  • 电费1.5元/度,服务费0.8元/度
  • 功率60kW
  • 总费用 = (1.5 + 0.8) × 30 = 69元
  • 充电时间 = 30 / 60 × 60 = 30分钟

技术要点详解

ModalBottomSheet详解

从底部弹出的模态对话框:

showModalBottomSheet(
  context: context,
  isScrollControlled: true,  // 允许滚动
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
  ),
  builder: (context) => Container(
    padding: const EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // 内容
      ],
    ),
  ),
);

ModalBottomSheet特点

  • 从底部滑出
  • 半透明背景
  • 点击外部关闭
  • 支持滚动内容

ChoiceChip单选筹码

单选筹码组件:

Wrap(
  spacing: 8,
  children: options.map((option) {
    return ChoiceChip(
      label: Text(option),
      selected: _selected == option,
      onSelected: (selected) {
        setState(() {
          _selected = option;
        });
      },
      selectedColor: Colors.blue,
      backgroundColor: Colors.grey[200],
    );
  }).toList(),
)

ChoiceChip vs FilterChip

  • ChoiceChip:单选
  • FilterChip:多选

List.fold累加计算

计算列表总和:

// 计算总充电桩数
final totalPiles = stations.fold(0, (sum, station) => sum + station.totalPiles);

// 计算总费用
final totalCost = piles.fold(0.0, (sum, pile) => sum + pile.totalPrice);

// 计算平均评分
final avgRating = stations.fold(0.0, (sum, s) => sum + s.rating) / stations.length;

fold方法

  • 第一个参数:初始值
  • 第二个参数:累加函数
  • 返回累加结果

List.where筛选

筛选列表元素:

// 筛选空闲充电桩
final availablePiles = piles.where((p) => p.status == '空闲').toList();

// 筛选快充桩
final fastPiles = piles.where((p) => p.type == '快充').toList();

// 筛选距离小于5km的站点
final nearbyStations = stations.where((s) => s.distance < 5).toList();

// 多条件筛选
final filtered = stations.where((s) {
  return s.availablePiles > 0 && s.distance < 3;
}).toList();

List.any判断

判断列表中是否存在满足条件的元素:

// 判断是否有快充桩
final hasFastCharging = station.piles.any((p) => p.type == '快充');

// 判断是否有空闲桩
final hasAvailable = station.piles.any((p) => p.status == '空闲');

// 判断是否有故障桩
final hasFault = station.piles.any((p) => p.status == '故障');

any vs every

  • any:至少有一个满足条件
  • every:所有元素都满足条件

充电桩查询功能详解

充电桩状态管理

充电桩有三种状态:

空闲

充电中

故障

充电桩

当前状态

绿色显示

橙色显示

红色显示

可以开始充电

等待充电完成

维修中

状态说明

状态 颜色 说明 操作
空闲 绿色 可以使用 可开始充电
充电中 橙色 正在使用 等待或选择其他桩
故障 红色 无法使用 联系运营商维修

费用计算详解

充电费用由两部分组成:

class ChargingCostCalculator {
  // 计算充电费用
  double calculateCost(
    double electricityPrice,  // 电费
    double serviceFee,         // 服务费
    double kwh,                // 充电度数
  ) {
    return (electricityPrice + serviceFee) * kwh;
  }
  
  // 计算充电时间
  double calculateTime(
    double kwh,     // 充电度数
    double power,   // 充电功率(kW)
  ) {
    return kwh / power;  // 小时
  }
  
  // 预估充电信息
  Map<String, dynamic> estimateCharging({
    required double batteryCapacity,  // 电池容量
    required double currentSoc,       // 当前电量百分比
    required double targetSoc,        // 目标电量百分比
    required double power,            // 充电功率
    required double price,            // 电费
    required double serviceFee,       // 服务费
  }) {
    final needKwh = batteryCapacity * (targetSoc - currentSoc) / 100;
    final cost = calculateCost(price, serviceFee, needKwh);
    final time = calculateTime(needKwh, power);
    
    return {
      'kwh': needKwh,
      'cost': cost,
      'time': time * 60,  // 转换为分钟
    };
  }
}

// 使用示例
final calculator = ChargingCostCalculator();
final estimate = calculator.estimateCharging(
  batteryCapacity: 60,  // 60kWh电池
  currentSoc: 20,       // 当前20%
  targetSoc: 80,        // 充到80%
  power: 60,            // 60kW快充
  price: 1.5,           // 电费1.5元/度
  serviceFee: 0.8,      // 服务费0.8元/度
);

print('需要充电:${estimate['kwh']}度');
print('费用:${estimate['cost']}元');
print('时间:${estimate['time']}分钟');

距离计算

计算两点之间的距离(简化版):

class DistanceCalculator {
  // 计算两点距离(km)
  double calculateDistance(
    double lat1,
    double lon1,
    double lat2,
    double lon2,
  ) {
    const R = 6371;  // 地球半径(km)
    
    final dLat = _toRadians(lat2 - lat1);
    final dLon = _toRadians(lon2 - lon1);
    
    final a = sin(dLat / 2) * sin(dLat / 2) +
        cos(_toRadians(lat1)) *
            cos(_toRadians(lat2)) *
            sin(dLon / 2) *
            sin(dLon / 2);
    
    final c = 2 * atan2(sqrt(a), sqrt(1 - a));
    
    return R * c;
  }
  
  double _toRadians(double degree) {
    return degree * pi / 180;
  }
}

// 使用示例
final calculator = DistanceCalculator();
final distance = calculator.calculateDistance(
  39.9042,   // 北京天安门纬度
  116.4074,  // 北京天安门经度
  31.2304,   // 上海外滩纬度
  121.4737,  // 上海外滩经度
);
print('距离:${distance.toStringAsFixed(1)}km');

功能扩展建议

1. 地图集成

使用高德地图显示充电站:

import 'package:amap_flutter_map/amap_flutter_map.dart';

class ChargingMapPage extends StatefulWidget {
  final List<ChargingStation> stations;
  
  const ChargingMapPage({super.key, required this.stations});
  
  
  State<ChargingMapPage> createState() => _ChargingMapPageState();
}

class _ChargingMapPageState extends State<ChargingMapPage> {
  late AMapController _mapController;
  Set<Marker> _markers = {};
  
  
  void initState() {
    super.initState();
    _loadMarkers();
  }
  
  void _loadMarkers() {
    for (var station in widget.stations) {
      _markers.add(
        Marker(
          position: LatLng(station.latitude, station.longitude),
          infoWindow: InfoWindow(
            title: station.name,
            snippet: station.availabilityText,
          ),
          icon: BitmapDescriptor.fromAssetImage(
            'assets/images/charging_station.png',
          ),
        ),
      );
    }
  }
  
  
  Widget build(BuildContext context) {
    return AMapWidget(
      initialCameraPosition: CameraPosition(
        target: LatLng(39.9042, 116.4074),
        zoom: 12,
      ),
      markers: _markers,
      onMapCreated: (controller) {
        _mapController = controller;
      },
    );
  }
}

2. 导航功能

调用第三方地图导航:

import 'package:url_launcher/url_launcher.dart';

class NavigationService {
  // 高德地图导航
  Future<void> navigateWithAmap(
    double latitude,
    double longitude,
    String name,
  ) async {
    final url = 'amapuri://route/plan/?'
        'dlat=$latitude&dlon=$longitude&dname=$name&dev=0&t=0';
    
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      // 打开网页版
      final webUrl = 'https://uri.amap.com/navigation?'
          'to=$longitude,$latitude,$name&mode=car&policy=1&src=myapp&coordinate=gaode';
      await launch(webUrl);
    }
  }
  
  // 百度地图导航
  Future<void> navigateWithBaidu(
    double latitude,
    double longitude,
    String name,
  ) async {
    final url = 'baidumap://map/direction?'
        'destination=latlng:$latitude,$longitude|name:$name&mode=driving';
    
    if (await canLaunch(url)) {
      await launch(url);
    }
  }
  
  // 选择导航方式
  void showNavigationOptions(
    BuildContext context,
    ChargingStation station,
  ) {
    showModalBottomSheet(
      context: context,
      builder: (context) => Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.map),
            title: const Text('高德地图'),
            onTap: () {
              Navigator.pop(context);
              navigateWithAmap(
                station.latitude,
                station.longitude,
                station.name,
              );
            },
          ),
          ListTile(
            leading: const Icon(Icons.map),
            title: const Text('百度地图'),
            onTap: () {
              Navigator.pop(context);
              navigateWithBaidu(
                station.latitude,
                station.longitude,
                station.name,
              );
            },
          ),
        ],
      ),
    );
  }
}

3. 预约充电

预约充电功能:

class ChargingReservation {
  String id;
  String stationId;
  String pileId;
  DateTime reservationTime;
  int duration;  // 预约时长(分钟)
  String status;  // 待确认/已确认/已取消
  
  ChargingReservation({
    required this.id,
    required this.stationId,
    required this.pileId,
    required this.reservationTime,
    required this.duration,
    this.status = '待确认',
  });
}

class ReservationService {
  // 创建预约
  Future<ChargingReservation> createReservation({
    required ChargingStation station,
    required ChargingPile pile,
    required DateTime time,
    required int duration,
  }) async {
    final reservation = ChargingReservation(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      stationId: station.id,
      pileId: pile.id,
      reservationTime: time,
      duration: duration,
    );
    
    // 保存预约
    await _saveReservation(reservation);
    
    // 发送通知
    await _sendNotification(reservation);
    
    return reservation;
  }
  
  // 取消预约
  Future<void> cancelReservation(String reservationId) async {
    // 更新状态
    // 发送取消通知
  }
  
  // 获取我的预约
  Future<List<ChargingReservation>> getMyReservations() async {
    // 从本地或服务器获取
    return [];
  }
}

// 预约对话框
void showReservationDialog(
  BuildContext context,
  ChargingStation station,
  ChargingPile pile,
) {
  DateTime selectedTime = DateTime.now().add(const Duration(hours: 1));
  int duration = 60;
  
  showDialog(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setState) => AlertDialog(
        title: const Text('预约充电'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              title: const Text('预约时间'),
              subtitle: Text(
                '${selectedTime.hour}:${selectedTime.minute.toString().padLeft(2, '0')}',
              ),
              trailing: const Icon(Icons.access_time),
              onTap: () async {
                final time = await showTimePicker(
                  context: context,
                  initialTime: TimeOfDay.fromDateTime(selectedTime),
                );
                if (time != null) {
                  setState(() {
                    selectedTime = DateTime(
                      selectedTime.year,
                      selectedTime.month,
                      selectedTime.day,
                      time.hour,
                      time.minute,
                    );
                  });
                }
              },
            ),
            ListTile(
              title: const Text('预约时长'),
              subtitle: Text('$duration分钟'),
              trailing: DropdownButton<int>(
                value: duration,
                items: [30, 60, 90, 120].map((d) {
                  return DropdownMenuItem(
                    value: d,
                    child: Text('$d分钟'),
                  );
                }).toList(),
                onChanged: (value) {
                  setState(() {
                    duration = value!;
                  });
                },
              ),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          FilledButton(
            onPressed: () {
              // 创建预约
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('预约成功')),
              );
            },
            child: const Text('确认'),
          ),
        ],
      ),
    ),
  );
}

4. 充电记录

记录充电历史:

class ChargingRecord {
  String id;
  String stationName;
  String pileId;
  DateTime startTime;
  DateTime? endTime;
  double kwh;
  double cost;
  String status;  // 充电中/已完成
  
  ChargingRecord({
    required this.id,
    required this.stationName,
    required this.pileId,
    required this.startTime,
    this.endTime,
    this.kwh = 0,
    this.cost = 0,
    this.status = '充电中',
  });
  
  Duration get duration {
    if (endTime == null) return Duration.zero;
    return endTime!.difference(startTime);
  }
}

class ChargingHistoryPage extends StatelessWidget {
  final List<ChargingRecord> records;
  
  const ChargingHistoryPage({super.key, required this.records});
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('充电记录')),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: records.length,
        itemBuilder: (context, index) {
          final record = records[index];
          return Card(
            child: ListTile(
              leading: CircleAvatar(
                backgroundColor: record.status == '充电中'
                    ? Colors.orange
                    : Colors.green,
                child: Icon(
                  record.status == '充电中'
                      ? Icons.battery_charging_full
                      : Icons.check,
                  color: Colors.white,
                ),
              ),
              title: Text(record.stationName),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('充电桩:${record.pileId}'),
                  Text(
                    '${record.startTime.month}/${record.startTime.day} '
                    '${record.startTime.hour}:${record.startTime.minute.toString().padLeft(2, '0')}',
                  ),
                ],
              ),
              trailing: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  Text(
                    '${record.kwh.toStringAsFixed(1)}度',
                    style: const TextStyle(fontWeight: FontWeight.bold),
                  ),
                  Text(
                    ${record.cost.toStringAsFixed(2)}',
                    style: const TextStyle(color: Colors.red),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

5. 收藏功能

收藏常用充电站:

class FavoriteService {
  List<String> _favoriteIds = [];
  
  // 添加收藏
  Future<void> addFavorite(String stationId) async {
    if (!_favoriteIds.contains(stationId)) {
      _favoriteIds.add(stationId);
      await _saveFavorites();
    }
  }
  
  // 取消收藏
  Future<void> removeFavorite(String stationId) async {
    _favoriteIds.remove(stationId);
    await _saveFavorites();
  }
  
  // 是否已收藏
  bool isFavorite(String stationId) {
    return _favoriteIds.contains(stationId);
  }
  
  // 获取收藏列表
  List<ChargingStation> getFavoriteStations(
    List<ChargingStation> allStations,
  ) {
    return allStations
        .where((s) => _favoriteIds.contains(s.id))
        .toList();
  }
  
  Future<void> _saveFavorites() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setStringList('favorite_stations', _favoriteIds);
  }
}

// 收藏按钮
IconButton(
  icon: Icon(
    _favoriteService.isFavorite(station.id)
        ? Icons.favorite
        : Icons.favorite_border,
    color: _favoriteService.isFavorite(station.id)
        ? Colors.red
        : null,
  ),
  onPressed: () {
    setState(() {
      if (_favoriteService.isFavorite(station.id)) {
        _favoriteService.removeFavorite(station.id);
      } else {
        _favoriteService.addFavorite(station.id);
      }
    });
  },
)

6. 价格对比

对比不同充电站价格:

class PriceComparison {
  // 获取价格范围
  Map<String, double> getPriceRange(List<ChargingStation> stations) {
    if (stations.isEmpty) {
      return {'min': 0, 'max': 0, 'avg': 0};
    }
    
    final prices = stations
        .expand((s) => s.piles)
        .map((p) => p.totalPrice)
        .toList();
    
    prices.sort();
    
    return {
      'min': prices.first,
      'max': prices.last,
      'avg': prices.reduce((a, b) => a + b) / prices.length,
    };
  }
  
  // 按价格排序
  List<ChargingStation> sortByPrice(
    List<ChargingStation> stations,
    {bool ascending = true},
  ) {
    final sorted = stations.toList();
    sorted.sort((a, b) {
      final aPrice = a.piles.isEmpty
          ? 0
          : a.piles.map((p) => p.totalPrice).reduce((a, b) => a + b) /
              a.piles.length;
      final bPrice = b.piles.isEmpty
          ? 0
          : b.piles.map((p) => p.totalPrice).reduce((a, b) => a + b) /
              b.piles.length;
      return ascending
          ? aPrice.compareTo(bPrice)
          : bPrice.compareTo(aPrice);
    });
    return sorted;
  }
}

7. 通知提醒

充电完成通知:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class ChargingNotificationService {
  final FlutterLocalNotificationsPlugin _notifications =
      FlutterLocalNotificationsPlugin();
  
  Future<void> initialize() async {
    const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosSettings = DarwinInitializationSettings();
    const settings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );
    
    await _notifications.initialize(settings);
  }
  
  // 充电完成通知
  Future<void> showChargingCompleteNotification({
    required String stationName,
    required double kwh,
    required double cost,
  }) async {
    await _notifications.show(
      0,
      '充电完成',
      '$stationName 充电${kwh.toStringAsFixed(1)}度,费用¥${cost.toStringAsFixed(2)}',
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'charging_complete',
          '充电完成',
          importance: Importance.high,
          priority: Priority.high,
        ),
      ),
    );
  }
  
  // 预约提醒
  Future<void> scheduleReservationReminder({
    required DateTime reservationTime,
    required String stationName,
  }) async {
    await _notifications.zonedSchedule(
      1,
      '充电预约提醒',
      '$stationName 预约时间即将到达',
      tz.TZDateTime.from(
        reservationTime.subtract(const Duration(minutes: 15)),
        tz.local,
      ),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'reservation_reminder',
          '预约提醒',
          importance: Importance.high,
        ),
      ),
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
    );
  }
}

8. 统计分析

充电数据统计:

class ChargingStatistics {
  List<ChargingRecord> records;
  
  ChargingStatistics(this.records);
  
  // 总充电次数
  int get totalCharges => records.length;
  
  // 总充电度数
  double get totalKwh {
    return records.fold(0.0, (sum, r) => sum + r.kwh);
  }
  
  // 总费用
  double get totalCost {
    return records.fold(0.0, (sum, r) => sum + r.cost);
  }
  
  // 平均每次充电度数
  double get averageKwh {
    if (records.isEmpty) return 0;
    return totalKwh / records.length;
  }
  
  // 平均每次费用
  double get averageCost {
    if (records.isEmpty) return 0;
    return totalCost / records.length;
  }
  
  // 按月统计
  Map<String, double> getMonthlyStats() {
    final monthly = <String, double>{};
    for (var record in records) {
      final key = '${record.startTime.year}-${record.startTime.month}';
      monthly[key] = (monthly[key] ?? 0) + record.cost;
    }
    return monthly;
  }
  
  // 最常用充电站
  String getMostUsedStation() {
    if (records.isEmpty) return '';
    final stationCount = <String, int>{};
    for (var record in records) {
      stationCount[record.stationName] =
          (stationCount[record.stationName] ?? 0) + 1;
    }
    return stationCount.entries
        .reduce((a, b) => a.value > b.value ? a : b)
        .key;
  }
}

常见问题解答

Q1: 如何实现实时定位功能?

A: 使用geolocator包获取位置:

import 'package:geolocator/geolocator.dart';

class LocationService {
  // 获取当前位置
  Future<Position?> getCurrentLocation() async {
    // 检查权限
    final permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      final request = await Geolocator.requestPermission();
      if (request == LocationPermission.denied) {
        return null;
      }
    }
    
    // 获取位置
    try {
      final position = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high,
      );
      return position;
    } catch (e) {
      print('获取位置失败: $e');
      return null;
    }
  }
  
  // 计算距离并排序
  Future<void> updateDistances(
    List<ChargingStation> stations,
  ) async {
    final position = await getCurrentLocation();
    if (position == null) return;
    
    for (var station in stations) {
      station.distance = Geolocator.distanceBetween(
        position.latitude,
        position.longitude,
        station.latitude,
        station.longitude,
      ) / 1000;  // 转换为km
    }
    
    // 按距离排序
    stations.sort((a, b) => a.distance.compareTo(b.distance));
  }
}

Q2: 如何实现充电桩状态实时更新?

A: 使用定时器或WebSocket:

import 'dart:async';

class RealtimeUpdateService {
  Timer? _timer;
  
  // 开始定时更新
  void startRealtimeUpdate(Function onUpdate) {
    _timer = Timer.periodic(const Duration(seconds: 30), (timer) {
      // 模拟状态更新
      onUpdate();
    });
  }
  
  // 停止更新
  void stopRealtimeUpdate() {
    _timer?.cancel();
    _timer = null;
  }
  
  // 使用WebSocket实时更新
  void connectWebSocket(String url) {
    // final channel = WebSocketChannel.connect(Uri.parse(url));
    // channel.stream.listen((message) {
    //   // 处理实时数据
    // });
  }
}

// 在页面中使用
class _ChargingStationListPageState extends State<ChargingStationListPage> {
  final _updateService = RealtimeUpdateService();
  
  
  void initState() {
    super.initState();
    _updateService.startRealtimeUpdate(() {
      _refreshData();
    });
  }
  
  
  void dispose() {
    _updateService.stopRealtimeUpdate();
    super.dispose();
  }
}

Q3: 如何添加支付功能?

A: 集成支付SDK:

class PaymentService {
  // 创建充电订单
  Future<String> createChargingOrder({
    required String stationId,
    required String pileId,
    required double amount,
  }) async {
    // 调用后端API创建订单
    final orderId = DateTime.now().millisecondsSinceEpoch.toString();
    return orderId;
  }
  
  // 支付宝支付
  Future<bool> payWithAlipay(String orderId, double amount) async {
    // 调用支付宝SDK
    // final result = await Alipay.pay(orderInfo);
    // return result['resultStatus'] == '9000';
    return true;
  }
  
  // 微信支付
  Future<bool> payWithWechat(String orderId, double amount) async {
    // 调用微信SDK
    // final result = await Wechat.pay(orderInfo);
    // return result['errCode'] == 0;
    return true;
  }
  
  // 显示支付选项
  void showPaymentOptions(
    BuildContext context,
    String orderId,
    double amount,
  ) {
    showModalBottomSheet(
      context: context,
      builder: (context) => Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.payment),
            title: const Text('支付宝'),
            trailing: Text(${amount.toStringAsFixed(2)}'),
            onTap: () async {
              Navigator.pop(context);
              final success = await payWithAlipay(orderId, amount);
              if (success) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('支付成功')),
                );
              }
            },
          ),
          ListTile(
            leading: const Icon(Icons.payment),
            title: const Text('微信支付'),
            trailing: Text(${amount.toStringAsFixed(2)}'),
            onTap: () async {
              Navigator.pop(context);
              final success = await payWithWechat(orderId, amount);
              if (success) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('支付成功')),
                );
              }
            },
          ),
        ],
      ),
    );
  }
}

Q4: 如何实现充电桩搜索功能?

A: 添加搜索框和筛选逻辑:

class SearchService {
  // 搜索充电站
  List<ChargingStation> searchStations(
    List<ChargingStation> stations,
    String query,
  ) {
    if (query.isEmpty) return stations;
    
    final lowerQuery = query.toLowerCase();
    return stations.where((station) {
      // 搜索名称
      if (station.name.toLowerCase().contains(lowerQuery)) {
        return true;
      }
      // 搜索地址
      if (station.address.toLowerCase().contains(lowerQuery)) {
        return true;
      }
      // 搜索运营商
      if (station.operator.toLowerCase().contains(lowerQuery)) {
        return true;
      }
      return false;
    }).toList();
  }
}

// 搜索页面
class SearchPage extends StatefulWidget {
  final List<ChargingStation> stations;
  
  const SearchPage({super.key, required this.stations});
  
  
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  final _searchController = TextEditingController();
  final _searchService = SearchService();
  List<ChargingStation> _searchResults = [];
  
  
  void initState() {
    super.initState();
    _searchResults = widget.stations;
  }
  
  void _performSearch(String query) {
    setState(() {
      _searchResults = _searchService.searchStations(widget.stations, query);
    });
  }
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TextField(
          controller: _searchController,
          decoration: const InputDecoration(
            hintText: '搜索充电站、地址、运营商...',
            border: InputBorder.none,
          ),
          onChanged: _performSearch,
        ),
      ),
      body: ListView.builder(
        itemCount: _searchResults.length,
        itemBuilder: (context, index) {
          return _buildStationCard(_searchResults[index]);
        },
      ),
    );
  }
}

Q5: 如何添加用户评价功能?

A: 实现评分和评论系统:

class Review {
  String id;
  String userId;
  String userName;
  String stationId;
  double rating;
  String comment;
  DateTime createdAt;
  List<String> photos;
  
  Review({
    required this.id,
    required this.userId,
    required this.userName,
    required this.stationId,
    required this.rating,
    required this.comment,
    required this.createdAt,
    List<String>? photos,
  }) : photos = photos ?? [];
}

class ReviewService {
  // 添加评价
  Future<void> addReview(Review review) async {
    // 保存到本地或服务器
  }
  
  // 获取充电站评价
  Future<List<Review>> getStationReviews(String stationId) async {
    // 从本地或服务器获取
    return [];
  }
  
  // 计算平均评分
  double calculateAverageRating(List<Review> reviews) {
    if (reviews.isEmpty) return 0;
    final total = reviews.fold(0.0, (sum, r) => sum + r.rating);
    return total / reviews.length;
  }
}

// 评价对话框
void showReviewDialog(
  BuildContext context,
  ChargingStation station,
) {
  double rating = 5.0;
  final commentController = TextEditingController();
  
  showDialog(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setState) => AlertDialog(
        title: const Text('评价充电站'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('评分'),
            Slider(
              value: rating,
              min: 1,
              max: 5,
              divisions: 8,
              label: rating.toStringAsFixed(1),
              onChanged: (value) {
                setState(() {
                  rating = value;
                });
              },
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: List.generate(5, (index) {
                return Icon(
                  index < rating ? Icons.star : Icons.star_border,
                  color: Colors.amber,
                  size: 32,
                );
              }),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: commentController,
              decoration: const InputDecoration(
                labelText: '评价内容',
                hintText: '分享您的充电体验...',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          FilledButton(
            onPressed: () {
              final review = Review(
                id: DateTime.now().millisecondsSinceEpoch.toString(),
                userId: 'user123',
                userName: '用户',
                stationId: station.id,
                rating: rating,
                comment: commentController.text,
                createdAt: DateTime.now(),
              );
              // 保存评价
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('评价成功')),
              );
            },
            child: const Text('提交'),
          ),
        ],
      ),
    ),
  );
}

项目总结

实现的功能

✅ 充电站列表查询
✅ 实时充电桩状态(空闲/充电中/故障)
✅ 费用查询(电费+服务费)
✅ 智能筛选(类型、可用性)
✅ 多种排序(距离、可用数、评分)
✅ 充电站详情(运营商、营业时间、电话)
✅ 充电桩详情(功率、接口、状态、费用)
✅ 配套设施展示
✅ 费用预估计算
✅ 数据实时刷新
✅ 数据本地持久化

技术亮点

  1. 实时状态更新:模拟充电桩状态变化
  2. 智能筛选排序:多维度筛选和排序
  3. 费用自动计算:电费+服务费+预估时间
  4. 状态颜色编码:绿/橙/红三色区分
  5. ModalBottomSheet:优雅的筛选界面
  6. ChoiceChip:直观的选项选择
  7. List.fold累加:高效的数据统计
  8. 数据持久化:完整的JSON序列化

应用场景

  • 🚗 电动车主日常充电
  • 🗺️ 出行前规划充电路线
  • 💰 对比充电费用
  • ⏱️ 预估充电时间
  • 📍 查找附近充电站
  • 🔋 查看充电桩实时状态
  • 📊 充电数据统计分析
  • ⭐ 充电站评价参考

数据流程图

筛选

排序

刷新

查看详情

开始充电

启动应用

加载充电站数据

显示列表

用户操作

应用筛选条件

应用排序规则

更新状态

显示充电桩列表

选择充电桩

计算费用

确认充电

充电流程图

查找充电站

查看充电桩状态

有空闲桩?

查看费用

选择其他站点

预估充电时间和费用

开始充电

插入充电枪

充电中

充电完成

结算费用

评价充电站

实时充电桩查询是一款实用的电动车充电助手应用,通过实时状态查询、智能筛选排序、费用预估计算等功能,帮助电动车主快速找到合适的充电站,让充电变得更加便捷高效。

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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐