Flutter 框架跨平台鸿蒙开发 - 全国公厕查询:智能定位附近公厕
fill:#333;important;important;fill:none;color:#333;color:#333;important;fill:none;fill:#333;height:1em;筛选排序刷新查看详情写评价导航启动应用选择城市生成公厕数据显示公厕列表用户操作应用筛选条件应用排序规则更新数据显示详情页用户操作提交评价打开导航。
Flutter全国公厕查询:智能定位附近公厕
项目简介
全国公厕查询是一款专为市民打造的便民服务Flutter应用,帮助用户快速找到附近的公共厕所,查看无障碍设施信息和用户评分。通过智能筛选和评价系统,让如厕变得更加便捷舒适。
运行效果图




核心功能
- 城市选择:支持8个主要城市切换
- 公厕查询:显示附近公共厕所列表
- 类型分类:公园、商场、地铁、医院、景区、街道
- 无障碍设施:查看无障碍通道、扶手等设施
- 设施信息:母婴室、WiFi、空调等配套设施
- 开放时间:24小时或限时开放信息
- 费用信息:免费或收费标识
- 评分系统:用户评分和评价查看
- 智能筛选:按类型、距离、评分、设施筛选
- 多种排序:按距离、评分、评价数排序
- 评价功能:用户可以发表评价和打分
- 详情查看:公厕详细信息和评价列表
技术特点
- Material Design 3设计风格
- 多城市数据模拟
- 智能筛选排序
- 评分可视化
- 响应式卡片布局
- ModalBottomSheet交互
- 评价系统
- 标签功能
核心代码实现
1. 城市数据模型
class City {
String name; // 城市名称
String code; // 城市代码
double latitude; // 纬度
double longitude; // 经度
City({
required this.name,
required this.code,
required this.latitude,
required this.longitude,
});
}
模型字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| name | String | 城市名称 |
| code | String | 城市代码(如BJ、SH) |
| latitude | double | 纬度坐标 |
| longitude | double | 经度坐标 |
支持城市列表:
- 北京(BJ):39.9042, 116.4074
- 上海(SH):31.2304, 121.4737
- 广州(GZ):23.1291, 113.2644
- 深圳(SZ):22.5431, 114.0579
- 杭州(HZ):30.2741, 120.1551
- 成都(CD):30.5728, 104.0668
- 武汉(WH):30.5928, 114.3055
- 西安(XA):34.3416, 108.9398
2. 公厕数据模型
class PublicToilet {
String id; // 公厕编号
String name; // 名称
String address; // 地址
double latitude; // 纬度
double longitude; // 经度
double distance; // 距离
String type; // 类型
bool hasDisabledAccess; // 无障碍设施
bool hasBabyChanging; // 母婴室
bool hasWifi; // WiFi
bool hasAirConditioning; // 空调
bool isOpen24Hours; // 24小时开放
String openTime; // 开放时间
double rating; // 评分
int reviewCount; // 评价数
bool isFree; // 是否免费
String managedBy; // 管理单位
String phone; // 联系电话
// 类型图标
IconData get typeIcon {
switch (type) {
case '公园公厕':
return Icons.park;
case '商场公厕':
return Icons.shopping_bag;
case '地铁公厕':
return Icons.subway;
case '医院公厕':
return Icons.local_hospital;
case '景区公厕':
return Icons.landscape;
case '街道公厕':
return Icons.signpost;
default:
return Icons.wc;
}
}
// 类型颜色
Color get typeColor {
switch (type) {
case '公园公厕':
return Colors.green;
case '商场公厕':
return Colors.purple;
case '地铁公厕':
return Colors.blue;
case '医院公厕':
return Colors.red;
case '景区公厕':
return Colors.orange;
case '街道公厕':
return Colors.teal;
default:
return Colors.grey;
}
}
// 评分文本
String get ratingText {
if (rating >= 4.5) return '优秀';
if (rating >= 4.0) return '良好';
if (rating >= 3.5) return '一般';
if (rating >= 3.0) return '较差';
return '很差';
}
// 评分颜色
Color get ratingColor {
if (rating >= 4.5) return Colors.green;
if (rating >= 4.0) return Colors.lightGreen;
if (rating >= 3.5) return Colors.orange;
if (rating >= 3.0) return Colors.deepOrange;
return Colors.red;
}
}
模型字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 唯一标识符 |
| name | String | 公厕名称 |
| address | String | 详细地址 |
| latitude | double | 纬度坐标 |
| longitude | double | 经度坐标 |
| distance | double | 距离(km) |
| type | String | 公厕类型 |
| hasDisabledAccess | bool | 是否有无障碍设施 |
| hasBabyChanging | bool | 是否有母婴室 |
| hasWifi | bool | 是否有WiFi |
| hasAirConditioning | bool | 是否有空调 |
| isOpen24Hours | bool | 是否24小时开放 |
| openTime | String | 开放时间 |
| rating | double | 评分(0-5) |
| reviewCount | int | 评价数量 |
| isFree | bool | 是否免费 |
| managedBy | String | 管理单位 |
| phone | String | 联系电话 |
计算属性:
typeIcon:根据类型返回对应图标typeColor:根据类型返回对应颜色ratingText:返回评分等级文本ratingColor:返回评分对应颜色
公厕类型与图标映射:
| 类型 | 图标 | 颜色 | 说明 |
|---|---|---|---|
| 公园公厕 | park | 绿色 | 公园内公厕 |
| 商场公厕 | shopping_bag | 紫色 | 商场内公厕 |
| 地铁公厕 | subway | 蓝色 | 地铁站公厕 |
| 医院公厕 | local_hospital | 红色 | 医院内公厕 |
| 景区公厕 | landscape | 橙色 | 景区内公厕 |
| 街道公厕 | signpost | 青色 | 街道公厕 |
评分等级划分:
| 评分范围 | 等级 | 颜色 |
|---|---|---|
| 4.5-5.0 | 优秀 | 绿色 |
| 4.0-4.5 | 良好 | 浅绿色 |
| 3.5-4.0 | 一般 | 橙色 |
| 3.0-3.5 | 较差 | 深橙色 |
| 0-3.0 | 很差 | 红色 |
3. 评价数据模型
class ToiletReview {
String id; // 评价ID
String toiletId; // 公厕ID
String userName; // 用户名
double rating; // 评分
String comment; // 评价内容
DateTime createTime; // 创建时间
List<String> tags; // 标签列表
ToiletReview({
required this.id,
required this.toiletId,
required this.userName,
required this.rating,
required this.comment,
required this.createTime,
required this.tags,
});
}
模型字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 评价唯一标识 |
| toiletId | String | 关联的公厕ID |
| userName | String | 评价用户名 |
| rating | double | 评分(1-5星) |
| comment | String | 评价文字内容 |
| createTime | DateTime | 评价时间 |
| tags | List | 标签列表 |
常用标签:
- 干净、整洁
- 方便、好找
- 设施齐全、贴心
- 24小时、便利
- 卫生好、管理好
- 需维护、老旧
- 人多、排队
- 推荐、不错
4. 公厕数据生成
void _generateToilets() {
if (_selectedCity == null) return;
final random = Random();
final types = ['公园公厕', '商场公厕', '地铁公厕', '医院公厕', '景区公厕', '街道公厕'];
final managedByList = ['市政管理', '物业管理', '商场管理', '景区管理'];
_toilets = List.generate(60, (index) {
final type = types[random.nextInt(types.length)];
final lat = _selectedCity!.latitude + (random.nextDouble() - 0.5) * 0.08;
final lng = _selectedCity!.longitude + (random.nextDouble() - 0.5) * 0.08;
final distance = random.nextDouble() * 4;
final rating = 3.0 + random.nextDouble() * 2;
final hasDisabled = random.nextDouble() > 0.4;
final isFree = random.nextDouble() > 0.2;
final is24Hours = random.nextDouble() > 0.6;
return PublicToilet(
id: 'WC${(index + 1).toString().padLeft(4, '0')}',
name: '$type${index + 1}号',
address: '${_selectedCity!.name}市某某区某某路${index + 1}号',
latitude: lat,
longitude: lng,
distance: distance,
type: type,
hasDisabledAccess: hasDisabled,
hasBabyChanging: random.nextDouble() > 0.5,
hasWifi: random.nextDouble() > 0.7,
hasAirConditioning: random.nextDouble() > 0.6,
isOpen24Hours: is24Hours,
openTime: is24Hours ? '24小时' : '06:00-22:00',
rating: rating,
reviewCount: random.nextInt(200) + 10,
isFree: isFree,
managedBy: managedByList[random.nextInt(managedByList.length)],
phone: '010-${random.nextInt(90000000) + 10000000}',
);
});
_applyFilters();
}
数据生成逻辑:
- 每个城市生成60个公厕
- 类型随机分配(6种类型)
- 坐标在城市中心±0.08度范围内
- 距离在0-4公里范围内
- 评分在3.0-5.0范围内
- 60%概率有无障碍设施
- 80%概率免费使用
- 40%概率24小时开放
- 50%概率有母婴室
- 30%概率有WiFi
- 40%概率有空调
- 评价数10-210条
- 公厕编号格式:WC+4位数字
5. 智能筛选功能
void _applyFilters() {
setState(() {
_filteredToilets = _toilets.where((toilet) {
// 类型筛选
if (_filterType != '全部' && toilet.type != _filterType) {
return false;
}
// 无障碍设施筛选
if (_onlyDisabledAccess && !toilet.hasDisabledAccess) {
return false;
}
// 24小时开放筛选
if (_onlyOpen24Hours && !toilet.isOpen24Hours) {
return false;
}
// 免费筛选
if (_onlyFree && !toilet.isFree) {
return false;
}
// 距离筛选
if (toilet.distance > _maxDistance) {
return false;
}
// 评分筛选
if (toilet.rating < _minRating) {
return false;
}
return true;
}).toList();
// 排序
switch (_sortBy) {
case 'distance':
_filteredToilets.sort((a, b) => a.distance.compareTo(b.distance));
break;
case 'rating':
_filteredToilets.sort((a, b) => b.rating.compareTo(a.rating));
break;
case 'reviewCount':
_filteredToilets.sort((a, b) => b.reviewCount.compareTo(a.reviewCount));
break;
}
});
}
筛选条件:
| 筛选项 | 选项 | 说明 |
|---|---|---|
| 类型 | 全部/6种类型 | 按公厕类型筛选 |
| 距离 | 0.5-5.0km | 滑动条选择最大距离 |
| 评分 | 0-5.0 | 滑动条选择最低评分 |
| 无障碍 | 开关 | 仅显示有无障碍设施 |
| 24小时 | 开关 | 仅显示24小时开放 |
| 免费 | 开关 | 仅显示免费公厕 |
排序方式:
| 排序 | 说明 |
|---|---|
| 按距离 | 从近到远排序 |
| 按评分 | 从高到低排序 |
| 按评价数 | 从多到少排序 |
筛选流程:
- 检查类型匹配
- 检查无障碍设施
- 检查24小时开放
- 检查免费使用
- 检查距离范围
- 检查评分范围
- 按选定方式排序
6. 城市选择对话框
void _selectCity() {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选择城市',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: _cities.map((city) {
return ChoiceChip(
label: Text(city.name),
selected: _selectedCity?.code == city.code,
onSelected: (selected) {
setState(() {
_selectedCity = city;
_generateToilets();
});
Navigator.pop(context);
},
);
}).toList(),
),
],
),
),
);
}
交互设计:
- 使用ModalBottomSheet展示城市列表
- ChoiceChip实现单选效果
- 选中城市后自动关闭对话框
- 重新生成该城市的公厕数据
7. 筛选对话框
void _showFilterDialog() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => StatefulBuilder(
builder: (context, setModalState) => Container(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('筛选条件', style: TextStyle(fontSize: 18)),
// 类型筛选
Wrap(
spacing: 8,
children: ['全部', '公园公厕', '商场公厕', '地铁公厕',
'医院公厕', '景区公厕', '街道公厕']
.map((type) {
return ChoiceChip(
label: Text(type),
selected: _filterType == type,
onSelected: (selected) {
setModalState(() => _filterType = type);
setState(() => _filterType = type);
_applyFilters();
},
);
}).toList(),
),
// 距离滑块
Slider(
value: _maxDistance,
min: 0.5,
max: 5.0,
divisions: 9,
label: '${_maxDistance.toStringAsFixed(1)}km',
onChanged: (value) {
setModalState(() => _maxDistance = value);
setState(() => _maxDistance = value);
_applyFilters();
},
),
// 评分滑块
Slider(
value: _minRating,
min: 0.0,
max: 5.0,
divisions: 10,
label: _minRating.toStringAsFixed(1),
onChanged: (value) {
setModalState(() => _minRating = value);
setState(() => _minRating = value);
_applyFilters();
},
),
// 开关选项
SwitchListTile(
title: const Text('仅显示无障碍设施'),
value: _onlyDisabledAccess,
onChanged: (value) {
setModalState(() => _onlyDisabledAccess = value);
setState(() => _onlyDisabledAccess = value);
_applyFilters();
},
),
SwitchListTile(
title: const Text('仅显示24小时开放'),
value: _onlyOpen24Hours,
onChanged: (value) {
setModalState(() => _onlyOpen24Hours = value);
setState(() => _onlyOpen24Hours = value);
_applyFilters();
},
),
SwitchListTile(
title: const Text('仅显示免费公厕'),
value: _onlyFree,
onChanged: (value) {
setModalState(() => _onlyFree = value);
setState(() => _onlyFree = value);
_applyFilters();
},
),
],
),
),
),
),
);
}
StatefulBuilder的使用:
- 在ModalBottomSheet中实现局部状态更新
- setModalState更新对话框内部UI
- setState更新主页面数据
- 实时应用筛选条件
isScrollControlled属性:
- 允许对话框内容滚动
- 适合内容较多的筛选条件
- 提升用户体验
8. 排序对话框
void _showSortDialog() {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'排序方式',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
RadioListTile<String>(
title: const Text('按距离排序'),
value: 'distance',
groupValue: _sortBy,
onChanged: (value) {
setState(() {
_sortBy = value!;
_applyFilters();
});
Navigator.pop(context);
},
),
RadioListTile<String>(
title: const Text('按评分排序'),
value: 'rating',
groupValue: _sortBy,
onChanged: (value) {
setState(() {
_sortBy = value!;
_applyFilters();
});
Navigator.pop(context);
},
),
RadioListTile<String>(
title: const Text('按评价数排序'),
value: 'reviewCount',
groupValue: _sortBy,
onChanged: (value) {
setState(() {
_sortBy = value!;
_applyFilters();
});
Navigator.pop(context);
},
),
],
),
),
);
}
RadioListTile的使用:
- 实现单选效果
- groupValue指定当前选中值
- onChanged回调处理选择变化
- 选择后自动关闭对话框
9. 公厕卡片UI
Widget _buildToiletCard(PublicToilet toilet) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ToiletDetailPage(toilet: toilet),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部:图标、名称、类型、评分
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: toilet.typeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
toilet.typeIcon,
color: toilet.typeColor,
size: 32,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
toilet.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: toilet.typeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
toilet.type,
style: TextStyle(
fontSize: 12,
color: toilet.typeColor,
),
),
),
if (toilet.isFree) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'免费',
style: TextStyle(
fontSize: 12,
color: Colors.green,
),
),
),
],
],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
children: [
Icon(Icons.star, size: 16, color: toilet.ratingColor),
const SizedBox(width: 4),
Text(
toilet.rating.toStringAsFixed(1),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: toilet.ratingColor,
),
),
],
),
Text(
'${toilet.reviewCount}条评价',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
],
),
const SizedBox(height: 12),
// 距离和开放时间
Row(
children: [
const Icon(Icons.navigation, size: 16, color: Colors.blue),
const SizedBox(width: 4),
Text(
'${(toilet.distance * 1000).toStringAsFixed(0)}米',
style: const TextStyle(color: Colors.blue),
),
const SizedBox(width: 16),
const Icon(Icons.access_time, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Text(
toilet.openTime,
style: const TextStyle(color: Colors.grey),
),
],
),
const SizedBox(height: 8),
// 地址
Row(
children: [
const Icon(Icons.location_on, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
toilet.address,
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
),
],
),
const SizedBox(height: 12),
// 设施标签
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (toilet.hasDisabledAccess)
_buildFacilityChip(Icons.accessible, '无障碍', Colors.blue),
if (toilet.hasBabyChanging)
_buildFacilityChip(Icons.baby_changing_station, '母婴室', Colors.pink),
if (toilet.hasWifi)
_buildFacilityChip(Icons.wifi, 'WiFi', Colors.purple),
if (toilet.hasAirConditioning)
_buildFacilityChip(Icons.ac_unit, '空调', Colors.cyan),
if (toilet.isOpen24Hours)
_buildFacilityChip(Icons.access_time, '24小时', Colors.green),
],
),
],
),
),
),
);
}
Widget _buildFacilityChip(IconData icon, String label, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(fontSize: 12, color: color),
),
],
),
);
}
卡片布局结构:
- 顶部:类型图标、名称、类型标签、免费标签、评分
- 中部:距离、开放时间、地址信息
- 底部:设施标签(无障碍、母婴室、WiFi、空调、24小时)
视觉设计要点:
- 类型颜色作为主题色
- 评分颜色根据分数变化
- 免费标签使用绿色突出显示
- 设施标签使用不同颜色区分
10. 汇总卡片
Widget _buildSummaryCard(int disabledAccessCount, int open24HoursCount) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildSummaryItem(
Icons.wc,
'附近公厕',
'${_filteredToilets.length}个',
),
_buildSummaryItem(
Icons.accessible,
'无障碍',
'$disabledAccessCount个',
color: Colors.blue,
),
_buildSummaryItem(
Icons.access_time,
'24小时',
'$open24HoursCount个',
color: Colors.green,
),
],
),
),
);
}
Widget _buildSummaryItem(IconData icon, String label, String value,
{Color? color}) {
return Column(
children: [
Icon(icon, size: 32, color: color ?? Colors.grey),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
汇总信息:
- 附近公厕:筛选后的总数量
- 无障碍设施:有无障碍设施的数量
- 24小时开放:24小时开放的数量
11. 公厕详情页
class ToiletDetailPage extends StatefulWidget {
final PublicToilet toilet;
const ToiletDetailPage({super.key, required this.toilet});
State<ToiletDetailPage> createState() => _ToiletDetailPageState();
}
class _ToiletDetailPageState extends State<ToiletDetailPage> {
List<ToiletReview> _reviews = [];
void initState() {
super.initState();
_generateReviews();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.toilet.name),
actions: [
IconButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('导航功能')),
);
},
icon: const Icon(Icons.navigation),
tooltip: '导航',
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBasicInfo(),
_buildFacilities(),
_buildRatingSection(),
_buildReviewsList(),
],
),
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: FilledButton.icon(
onPressed: _showReviewDialog,
icon: const Icon(Icons.rate_review),
label: const Text('写评价'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
),
);
}
}
详情页结构:
- 基本信息卡片:名称、类型、距离、开放时间、地址、管理单位、电话
- 设施信息卡片:无障碍、母婴室、WiFi、空调、24小时、免费
- 评分统计卡片:总评分、星级分布、评价数量
- 评价列表卡片:用户评价详情
- 底部按钮:写评价
12. 评价对话框
void _showReviewDialog() {
double rating = 5.0;
String comment = '';
final selectedTags = <String>[];
final availableTags = ['干净', '整洁', '方便', '好找',
'设施齐全', '贴心', '24小时', '便利'];
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: const Text('评价公厕'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('评分'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return IconButton(
icon: Icon(
index < rating ? Icons.star : Icons.star_border,
color: Colors.amber,
size: 32,
),
onPressed: () {
setDialogState(() => rating = index + 1.0);
},
);
}),
),
const Text('标签'),
Wrap(
spacing: 8,
children: availableTags.map((tag) {
return FilterChip(
label: Text(tag),
selected: selectedTags.contains(tag),
onSelected: (selected) {
setDialogState(() {
if (selected) {
selectedTags.add(tag);
} else {
selectedTags.remove(tag);
}
});
},
);
}).toList(),
),
TextField(
decoration: const InputDecoration(
labelText: '评价内容',
hintText: '分享您的使用体验',
border: OutlineInputBorder(),
),
maxLines: 3,
onChanged: (value) => comment = value,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
FilledButton(
onPressed: () {
if (comment.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入评价内容')),
);
return;
}
setState(() {
_reviews.insert(
0,
ToiletReview(
id: 'R${_reviews.length + 1}',
toiletId: widget.toilet.id,
userName: '当前用户',
rating: rating,
comment: comment,
createTime: DateTime.now(),
tags: selectedTags,
),
);
});
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('评价成功')),
);
},
child: const Text('提交'),
),
],
),
),
);
}
评价功能:
- 星级评分(1-5星)
- 标签选择(多选)
- 文字评价(必填)
- 提交验证
- 实时更新列表
FilterChip的使用:
- 实现多选标签效果
- selected属性控制选中状态
- onSelected回调处理选择变化
13. 评分统计可视化
Widget _buildRatingSection() {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.star, color: Colors.amber),
SizedBox(width: 8),
Text(
'用户评价',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Column(
children: [
Text(
widget.toilet.rating.toStringAsFixed(1),
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: widget.toilet.ratingColor,
),
),
Row(
children: List.generate(5, (index) {
return Icon(
index < widget.toilet.rating
? Icons.star
: Icons.star_border,
color: Colors.amber,
size: 20,
);
}),
),
Text(
'${widget.toilet.reviewCount}条评价',
style: const TextStyle(color: Colors.grey),
),
],
),
const SizedBox(width: 32),
Expanded(
child: Column(
children: [
_buildRatingBar(5, 0.6),
_buildRatingBar(4, 0.25),
_buildRatingBar(3, 0.1),
_buildRatingBar(2, 0.03),
_buildRatingBar(1, 0.02),
],
),
),
],
),
],
),
),
);
}
Widget _buildRatingBar(int stars, double percentage) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Text('$stars', style: const TextStyle(fontSize: 12)),
const SizedBox(width: 4),
const Icon(Icons.star, size: 12, color: Colors.amber),
const SizedBox(width: 8),
Expanded(
child: LinearProgressIndicator(
value: percentage,
backgroundColor: Colors.grey[200],
color: Colors.amber,
),
),
const SizedBox(width: 8),
Text(
'${(percentage * 100).toStringAsFixed(0)}%',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
);
}
评分可视化:
- 左侧:总评分、星级显示、评价数量
- 右侧:5个星级的分布百分比
- 使用LinearProgressIndicator显示比例
星级分布示例:
- 5星:60%
- 4星:25%
- 3星:10%
- 2星:3%
- 1星:2%
技术要点详解
1. ModalBottomSheet的高级用法
ModalBottomSheet是Flutter中常用的底部弹出对话框组件,本项目中大量使用。
基本用法:
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 内容
],
),
),
);
isScrollControlled属性:
showModalBottomSheet(
context: context,
isScrollControlled: true, // 允许内容滚动
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
// 可滚动的内容
),
),
),
);
关键属性:
context:上下文对象builder:构建器函数isScrollControlled:是否可滚动shape:形状定义backgroundColor:背景颜色isDismissible:是否可通过点击外部关闭
使用场景:
- 城市选择
- 筛选条件设置
- 排序方式选择
- 快速操作菜单
2. StatefulBuilder的使用
StatefulBuilder允许在对话框内部维护独立的状态。
基本用法:
showModalBottomSheet(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setModalState) => Container(
child: Column(
children: [
Slider(
value: _value,
onChanged: (value) {
setModalState(() => _value = value); // 更新对话框内部状态
setState(() => _value = value); // 更新主页面状态
},
),
],
),
),
),
);
关键点:
setModalState:更新对话框内部UIsetState:更新主页面数据- 两者配合实现实时预览效果
使用场景:
- 筛选条件实时预览
- 滑块值实时显示
- 开关状态实时更新
3. ChoiceChip和FilterChip
ChoiceChip用于单选,FilterChip用于多选。
ChoiceChip(单选):
Wrap(
spacing: 8,
children: ['选项1', '选项2', '选项3'].map((option) {
return ChoiceChip(
label: Text(option),
selected: _selectedOption == option,
onSelected: (selected) {
setState(() => _selectedOption = option);
},
);
}).toList(),
)
FilterChip(多选):
Wrap(
spacing: 8,
children: ['标签1', '标签2', '标签3'].map((tag) {
return FilterChip(
label: Text(tag),
selected: _selectedTags.contains(tag),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedTags.add(tag);
} else {
_selectedTags.remove(tag);
}
});
},
);
}).toList(),
)
关键属性:
label:标签文本selected:是否选中onSelected:选择回调selectedColor:选中颜色backgroundColor:背景颜色
使用场景:
- 城市选择(单选)
- 类型筛选(单选)
- 评价标签(多选)
4. RadioListTile的使用
RadioListTile用于实现单选列表项。
基本用法:
RadioListTile<String>(
title: const Text('选项1'),
value: 'option1',
groupValue: _selectedValue,
onChanged: (value) {
setState(() => _selectedValue = value!);
},
)
关键属性:
title:标题文本value:选项值groupValue:当前选中值onChanged:选择回调subtitle:副标题secondary:前置图标
使用场景:
- 排序方式选择
- 单选设置项
- 选项列表
5. Slider滑块组件
Slider用于在范围内选择数值。
基本用法:
Slider(
value: _currentValue,
min: 0.0,
max: 5.0,
divisions: 10,
label: _currentValue.toStringAsFixed(1),
onChanged: (value) {
setState(() => _currentValue = value);
},
)
关键属性:
value:当前值min:最小值max:最大值divisions:分段数label:标签文本onChanged:值变化回调
使用场景:
- 距离筛选(0.5-5.0km)
- 评分筛选(0-5.0)
- 范围选择
6. LinearProgressIndicator进度条
LinearProgressIndicator用于显示线性进度。
基本用法:
LinearProgressIndicator(
value: 0.75,
backgroundColor: Colors.grey[200],
color: Colors.amber,
minHeight: 8,
)
关键属性:
value:进度值(0-1)backgroundColor:背景颜色color:进度颜色minHeight:最小高度
使用场景:
- 评分分布显示
- 进度展示
- 百分比可视化
7. 计算属性的使用
计算属性(getter)可以根据对象状态动态返回值。
示例:
class PublicToilet {
String type;
double rating;
IconData get typeIcon {
switch (type) {
case '公园公厕':
return Icons.park;
case '商场公厕':
return Icons.shopping_bag;
default:
return Icons.wc;
}
}
Color get typeColor {
switch (type) {
case '公园公厕':
return Colors.green;
case '商场公厕':
return Colors.purple;
default:
return Colors.grey;
}
}
String get ratingText {
if (rating >= 4.5) return '优秀';
if (rating >= 4.0) return '良好';
if (rating >= 3.5) return '一般';
return '较差';
}
}
优势:
- 减少数据冗余
- 保持数据一致性
- 简化代码逻辑
- 便于维护
8. 列表筛选与排序
使用Dart的集合操作实现高效的数据筛选和排序。
筛选:
_filteredToilets = _toilets.where((toilet) {
if (_filterType != '全部' && toilet.type != _filterType) {
return false;
}
if (_onlyDisabledAccess && !toilet.hasDisabledAccess) {
return false;
}
if (toilet.distance > _maxDistance) {
return false;
}
if (toilet.rating < _minRating) {
return false;
}
return true;
}).toList();
排序:
switch (_sortBy) {
case 'distance':
_filteredToilets.sort((a, b) => a.distance.compareTo(b.distance));
break;
case 'rating':
_filteredToilets.sort((a, b) => b.rating.compareTo(a.rating));
break;
case 'reviewCount':
_filteredToilets.sort((a, b) => b.reviewCount.compareTo(a.reviewCount));
break;
}
性能优化:
- 使用where进行链式筛选
- 避免多次遍历列表
- 在setState中更新状态
9. 评价系统实现
实现完整的评价功能,包括评分、标签、评论。
评价数据生成:
void _generateReviews() {
final random = Random();
final userNames = ['张三', '李四', '王五', '赵六'];
final comments = [
'环境很干净,设施齐全',
'位置方便,很容易找到',
'有无障碍设施,很贴心',
];
final tagsList = [
['干净', '整洁'],
['方便', '好找'],
['设施齐全', '贴心'],
];
_reviews = List.generate(
min(widget.toilet.reviewCount, 20),
(index) {
final rating = 3.0 + random.nextDouble() * 2;
final commentIndex = random.nextInt(comments.length);
return ToiletReview(
id: 'R${index + 1}',
toiletId: widget.toilet.id,
userName: userNames[random.nextInt(userNames.length)],
rating: rating,
comment: comments[commentIndex],
createTime: DateTime.now().subtract(Duration(days: random.nextInt(90))),
tags: tagsList[commentIndex],
);
},
);
_reviews.sort((a, b) => b.createTime.compareTo(a.createTime));
}
评价提交:
void _submitReview(double rating, String comment, List<String> tags) {
setState(() {
_reviews.insert(
0,
ToiletReview(
id: 'R${_reviews.length + 1}',
toiletId: widget.toilet.id,
userName: '当前用户',
rating: rating,
comment: comment,
createTime: DateTime.now(),
tags: tags,
),
);
});
}
评价显示:
- 按时间倒序排列
- 显示用户名、评分、评论、标签
- 限制显示数量(前5条)
- 提供查看全部按钮
10. 空状态处理
当筛选结果为空时,显示友好的空状态提示。
_filteredToilets.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.wc, size: 80, color: Colors.grey[300]),
const SizedBox(height: 16),
Text('附近暂无公厕',
style: TextStyle(color: Colors.grey[600])),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: () {
setState(() {
_filterType = '全部';
_onlyDisabledAccess = false;
_onlyOpen24Hours = false;
_onlyFree = false;
_maxDistance = 5.0;
_minRating = 0.0;
});
_applyFilters();
},
icon: const Icon(Icons.refresh),
label: const Text('重置筛选'),
),
],
),
)
: ListView.builder(...)
空状态设计要点:
- 使用大图标吸引注意
- 提供清晰的说明文字
- 给出解决方案(重置筛选)
- 保持视觉一致性
功能扩展方向
1. 地图集成
集成高德地图或百度地图SDK,在地图上显示公厕位置。
实现思路:
- 集成地图SDK(amap_flutter_map)
- 在地图上标注公厕位置
- 使用不同颜色标记不同类型
- 点击标记显示公厕信息
- 支持地图缩放和拖动
- 显示用户当前位置
- 规划步行路线
代码示例:
AMapWidget(
markers: _toilets.map((toilet) => Marker(
position: LatLng(toilet.latitude, toilet.longitude),
icon: BitmapDescriptor.fromAssetImage(
'assets/toilet_${toilet.type}.png',
),
infoWindow: InfoWindow(
title: toilet.name,
snippet: '评分: ${toilet.rating.toStringAsFixed(1)}',
),
)).toSet(),
onMapCreated: (controller) {
_mapController = controller;
},
)
2. 导航功能
提供从当前位置到公厕的导航功能。
实现思路:
- 获取用户当前位置
- 调用地图导航API
- 支持步行导航
- 显示预计到达时间
- 实时更新导航路线
- 语音播报导航信息
技术方案:
- 使用geolocator获取位置
- 使用url_launcher打开地图导航
- 或集成地图SDK的导航功能
3. 收藏功能
收藏常用的公厕,快速查找。
实现思路:
- 添加收藏按钮
- 保存收藏列表
- 显示收藏页面
- 快速导航到收藏公厕
- 支持备注信息
- 收藏排序
数据模型:
class FavoriteToilet {
String toiletId;
String name;
String address;
DateTime addTime;
String note;
}
4. 图片上传
用户可以上传公厕照片,帮助其他用户了解实际情况。
实现思路:
- 集成image_picker选择照片
- 压缩图片减少存储
- 上传到服务器
- 在详情页展示照片
- 支持照片浏览
- 照片审核机制
代码示例:
Future<void> _pickImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
// 压缩图片
final compressedImage = await FlutterImageCompress.compressWithFile(
image.path,
quality: 70,
);
// 上传图片
await _uploadImage(compressedImage);
}
}
5. 举报功能
用户可以举报信息不准确或已关闭的公厕。
实现思路:
- 添加举报按钮
- 选择举报原因
- 填写举报说明
- 提交举报信息
- 管理员审核
- 更新公厕状态
举报类型:
- 信息不准确
- 已关闭
- 卫生状况差
- 设施损坏
- 其他问题
6. 无障碍设施详情
提供更详细的无障碍设施信息。
实现思路:
- 无障碍通道宽度
- 扶手位置和高度
- 坐便器高度
- 紧急呼叫按钮
- 轮椅回转空间
- 照片展示
数据模型:
class DisabledAccessInfo {
bool hasRamp; // 坡道
double rampWidth; // 坡道宽度
bool hasHandrail; // 扶手
double handrailHeight; // 扶手高度
bool hasEmergencyButton; // 紧急按钮
double doorWidth; // 门宽
bool hasWheelchairSpace; // 轮椅空间
List<String> photos; // 照片
}
7. 实时状态
显示公厕的实时使用状态。
实现思路:
- 传感器检测使用状态
- WebSocket实时推送
- 显示空闲/使用中
- 预计等待时间
- 排队人数
- 使用时长统计
状态类型:
- 空闲:绿色
- 使用中:橙色
- 维护中:红色
- 已关闭:灰色
8. 数据统计
统计公厕使用数据,生成可视化报表。
实现思路:
- 使用次数统计
- 高峰时段分析
- 用户评分趋势
- 设施使用率
- 区域分布图
- 月度报告生成
图表展示:
// 使用fl_chart绘制图表
LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
spots: _usageData.map((d) => FlSpot(d.hour, d.count)).toList(),
isCurved: true,
colors: [Colors.blue],
),
],
),
)
常见问题解答
1. 如何实现实时位置更新?
问题:公厕位置需要基于用户当前位置计算距离,如何实现?
解答:
在实际应用中,需要集成定位SDK:
// 使用geolocator获取位置
import 'package:geolocator/geolocator.dart';
Future<Position> _getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('位置服务未开启');
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('位置权限被拒绝');
}
}
return await Geolocator.getCurrentPosition();
}
// 计算距离
double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
return Geolocator.distanceBetween(lat1, lon1, lat2, lon2) / 1000; // 转换为公里
}
// 更新所有公厕距离
void _updateToiletDistances(Position position) {
for (var toilet in _toilets) {
toilet.distance = calculateDistance(
position.latitude,
position.longitude,
toilet.latitude,
toilet.longitude,
);
}
_applyFilters();
}
// 监听位置变化
StreamSubscription<Position> _positionStream =
Geolocator.getPositionStream(
locationSettings: LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 100, // 移动100米更新一次
),
).listen((Position position) {
_updateToiletDistances(position);
});
注意事项:
- 需要在AndroidManifest.xml和Info.plist中添加权限
- 考虑电量消耗,合理设置更新频率
- 处理权限被拒绝的情况
2. 如何处理公厕数据的持久化?
问题:如何保存用户的筛选条件和收藏列表?
解答:
使用SharedPreferences或本地数据库:
// 使用SharedPreferences
import 'package:shared_preferences/shared_preferences.dart';
// 保存筛选条件
Future<void> _saveFilterSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('filterType', _filterType);
await prefs.setBool('onlyDisabledAccess', _onlyDisabledAccess);
await prefs.setBool('onlyOpen24Hours', _onlyOpen24Hours);
await prefs.setBool('onlyFree', _onlyFree);
await prefs.setDouble('maxDistance', _maxDistance);
await prefs.setDouble('minRating', _minRating);
}
// 加载筛选条件
Future<void> _loadFilterSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_filterType = prefs.getString('filterType') ?? '全部';
_onlyDisabledAccess = prefs.getBool('onlyDisabledAccess') ?? false;
_onlyOpen24Hours = prefs.getBool('onlyOpen24Hours') ?? false;
_onlyFree = prefs.getBool('onlyFree') ?? false;
_maxDistance = prefs.getDouble('maxDistance') ?? 3.0;
_minRating = prefs.getDouble('minRating') ?? 0.0;
});
}
// 保存收藏列表
Future<void> _saveFavorites(List<String> favoriteIds) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList('favorites', favoriteIds);
}
// 加载收藏列表
Future<List<String>> _loadFavorites() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getStringList('favorites') ?? [];
}
使用SQLite数据库:
import 'package:sqflite/sqflite.dart';
class DatabaseHelper {
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final path = await getDatabasesPath();
return await openDatabase(
'$path/toilets.db',
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE favorites (
id TEXT PRIMARY KEY,
toilet_id TEXT,
name TEXT,
address TEXT,
add_time TEXT,
note TEXT
)
''');
},
);
}
Future<void> addFavorite(FavoriteToilet favorite) async {
final db = await database;
await db.insert('favorites', favorite.toMap());
}
Future<List<FavoriteToilet>> getFavorites() async {
final db = await database;
final maps = await db.query('favorites', orderBy: 'add_time DESC');
return maps.map((map) => FavoriteToilet.fromMap(map)).toList();
}
}
3. 如何实现评价的审核机制?
问题:如何防止恶意评价和垃圾信息?
解答:
实现多层审核机制:
class ReviewModerationService {
// 敏感词过滤
bool _containsSensitiveWords(String text) {
final sensitiveWords = ['敏感词1', '敏感词2', '敏感词3'];
for (var word in sensitiveWords) {
if (text.contains(word)) {
return true;
}
}
return false;
}
// 评价验证
Future<ReviewValidationResult> validateReview(ToiletReview review) async {
// 1. 检查评价内容长度
if (review.comment.length < 5) {
return ReviewValidationResult(
isValid: false,
message: '评价内容至少5个字',
);
}
// 2. 检查敏感词
if (_containsSensitiveWords(review.comment)) {
return ReviewValidationResult(
isValid: false,
message: '评价包含敏感词',
);
}
// 3. 检查评分合理性
if (review.rating < 1 || review.rating > 5) {
return ReviewValidationResult(
isValid: false,
message: '评分必须在1-5之间',
);
}
// 4. 检查用户评价频率
final recentReviews = await _getUserRecentReviews(review.userName);
if (recentReviews.length > 10) {
return ReviewValidationResult(
isValid: false,
message: '评价过于频繁,请稍后再试',
);
}
return ReviewValidationResult(isValid: true);
}
// 获取用户最近评价
Future<List<ToiletReview>> _getUserRecentReviews(String userName) async {
// 从数据库查询用户最近1小时的评价
return [];
}
}
class ReviewValidationResult {
final bool isValid;
final String? message;
ReviewValidationResult({required this.isValid, this.message});
}
审核流程:
- 客户端基本验证(长度、格式)
- 敏感词过滤
- 频率限制
- 服务器端审核
- 人工复审(可选)
4. 如何优化大量数据的加载性能?
问题:当公厕数量很多时,如何优化列表性能?
解答:
使用分页加载和虚拟滚动:
class ToiletListPage extends StatefulWidget {
State<ToiletListPage> createState() => _ToiletListPageState();
}
class _ToiletListPageState extends State<ToiletListPage> {
final ScrollController _scrollController = ScrollController();
List<PublicToilet> _displayedToilets = [];
int _currentPage = 0;
final int _pageSize = 20;
bool _isLoading = false;
void initState() {
super.initState();
_loadMoreToilets();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent * 0.8) {
_loadMoreToilets();
}
}
Future<void> _loadMoreToilets() async {
if (_isLoading) return;
setState(() => _isLoading = true);
// 模拟网络请求
await Future.delayed(Duration(milliseconds: 500));
final startIndex = _currentPage * _pageSize;
final endIndex = min(startIndex + _pageSize, _filteredToilets.length);
if (startIndex < _filteredToilets.length) {
setState(() {
_displayedToilets.addAll(
_filteredToilets.sublist(startIndex, endIndex),
);
_currentPage++;
_isLoading = false;
});
} else {
setState(() => _isLoading = false);
}
}
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: _displayedToilets.length + (_isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index == _displayedToilets.length) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
);
}
return _buildToiletCard(_displayedToilets[index]);
},
);
}
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
性能优化技巧:
- 使用ListView.builder实现懒加载
- 分页加载数据
- 缓存已加载数据
- 图片懒加载
- 避免在build方法中进行复杂计算
5. 如何实现离线功能?
问题:在没有网络的情况下,如何使用应用?
解答:
实现本地缓存和离线数据:
import 'package:connectivity_plus/connectivity_plus.dart';
class OfflineService {
// 检查网络连接
Future<bool> isOnline() async {
final connectivityResult = await Connectivity().checkConnectivity();
return connectivityResult != ConnectivityResult.none;
}
// 缓存公厕数据
Future<void> cacheToiletData(List<PublicToilet> toilets) async {
final prefs = await SharedPreferences.getInstance();
final jsonData = toilets.map((t) => t.toJson()).toList();
await prefs.setString('cached_toilets', jsonEncode(jsonData));
await prefs.setString('cache_time', DateTime.now().toIso8601String());
}
// 加载缓存数据
Future<List<PublicToilet>> loadCachedToilets() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString('cached_toilets');
if (jsonString == null) return [];
final jsonData = jsonDecode(jsonString) as List;
return jsonData.map((json) => PublicToilet.fromJson(json)).toList();
}
// 检查缓存是否过期
Future<bool> isCacheExpired() async {
final prefs = await SharedPreferences.getInstance();
final cacheTimeString = prefs.getString('cache_time');
if (cacheTimeString == null) return true;
final cacheTime = DateTime.parse(cacheTimeString);
final now = DateTime.now();
final difference = now.difference(cacheTime);
return difference.inHours > 24; // 缓存24小时
}
// 获取公厕数据(在线/离线)
Future<List<PublicToilet>> getToilets() async {
final online = await isOnline();
if (online) {
// 在线:从服务器获取
final toilets = await _fetchFromServer();
await cacheToiletData(toilets);
return toilets;
} else {
// 离线:从缓存加载
return await loadCachedToilets();
}
}
Future<List<PublicToilet>> _fetchFromServer() async {
// 从服务器获取数据
return [];
}
}
离线功能:
- 缓存公厕列表
- 缓存地图数据
- 离线评价(待同步)
- 收藏列表本地存储
- 网络恢复后自动同步
项目总结
核心功能流程
数据流转
技术架构
项目特色
- 多城市支持:覆盖8个主要城市,数据独立管理
- 智能筛选:类型、距离、评分、设施多维度筛选
- 无障碍关注:重点展示无障碍设施信息
- 评价系统:完整的评分、标签、评论功能
- 用户体验:Material Design 3设计,交互流畅
- 数据可视化:评分分布、设施标签等可视化元素
- 响应式布局:适配不同屏幕尺寸
- 空状态处理:友好的空状态提示和引导
学习收获
通过本项目,你将掌握:
- Flutter基础:Widget组合、状态管理、导航路由
- Material Design:卡片、芯片、进度条、对话框等组件
- 数据处理:列表筛选、排序、计算属性
- 交互设计:ModalBottomSheet、StatefulBuilder、手势操作
- UI设计:颜色搭配、布局设计、视觉层次
- 业务逻辑:评价系统、筛选逻辑、数据验证
- 代码组织:模型设计、方法封装、代码复用
性能优化建议
- 列表优化:使用ListView.builder实现懒加载
- 状态管理:合理使用setState,避免不必要的重建
- 图片优化:使用Icon代替图片,减少资源占用
- 数据缓存:缓存筛选结果,避免重复计算
- 异步操作:使用Future和async/await处理耗时操作
- 内存管理:及时释放不用的资源和监听器
- 分页加载:大量数据时使用分页加载
后续优化方向
- 真实数据:接入后端API,获取真实公厕数据
- 地图显示:集成地图SDK,可视化公厕位置
- 导航功能:提供到公厕的导航路线
- 图片功能:支持上传和查看公厕照片
- 用户系统:登录注册、个人中心、使用记录
- 推送通知:附近公厕提醒、维护通知
- 数据统计:使用数据分析、图表展示
- 社交功能:分享公厕、好友推荐
本项目提供了一个完整的公厕查询应用框架,你可以在此基础上继续扩展功能,打造更加完善的便民服务应用。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)