Flutter 框架跨平台鸿蒙开发 - 实时充电桩查询:智能定位附近充电站
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('提交'),
),
],
),
),
);
}
项目总结
实现的功能
✅ 充电站列表查询
✅ 实时充电桩状态(空闲/充电中/故障)
✅ 费用查询(电费+服务费)
✅ 智能筛选(类型、可用性)
✅ 多种排序(距离、可用数、评分)
✅ 充电站详情(运营商、营业时间、电话)
✅ 充电桩详情(功率、接口、状态、费用)
✅ 配套设施展示
✅ 费用预估计算
✅ 数据实时刷新
✅ 数据本地持久化
技术亮点
- 实时状态更新:模拟充电桩状态变化
- 智能筛选排序:多维度筛选和排序
- 费用自动计算:电费+服务费+预估时间
- 状态颜色编码:绿/橙/红三色区分
- ModalBottomSheet:优雅的筛选界面
- ChoiceChip:直观的选项选择
- List.fold累加:高效的数据统计
- 数据持久化:完整的JSON序列化
应用场景
- 🚗 电动车主日常充电
- 🗺️ 出行前规划充电路线
- 💰 对比充电费用
- ⏱️ 预估充电时间
- 📍 查找附近充电站
- 🔋 查看充电桩实时状态
- 📊 充电数据统计分析
- ⭐ 充电站评价参考
数据流程图
充电流程图
实时充电桩查询是一款实用的电动车充电助手应用,通过实时状态查询、智能筛选排序、费用预估计算等功能,帮助电动车主快速找到合适的充电站,让充电变得更加便捷高效。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)