feat(mobile): #24 Flutter三合一APP骨架搭建

- Flutter 3.x 项目初始化(pubspec.yaml + 目录结构)
- 核心依赖:dio/go_router/provider/shared_preferences/hive/geolocator
- 统一登录页 + Token管理(TokenService + AuthInterceptor)
- 底部Tab三合一导航(供水/巡检/营收)
- 供水管理Tab:监测数据列表(MonitorListPage)
- 巡检Tab:任务列表+状态筛选+进度展示(PatrolTaskListPage)
- 营收Tab:抄表页面+账单列表(MeterReadingPage/BillListPage)
- 消息推送服务(PushService)
- GPS定位服务(LocationService)
- 拍照/相册服务(CameraService)
- 离线缓存服务(CacheService)
- Android/iOS打包配置+权限声明
- GoRouter路由+Auth认证守卫
- Provider状态管理
This commit is contained in:
2026-06-15 08:52:55 +08:00
parent 37bcc78eee
commit d9d72405c6
41 changed files with 3014 additions and 0 deletions
@@ -0,0 +1,122 @@
import 'dart:convert';
/// 离线缓存服务
/// 基于 Hive 提供本地数据存储能力
class CacheService {
static CacheService? _instance;
final Map<String, dynamic> _memoryCache = {};
bool _isInitialized = false;
CacheService._internal();
factory CacheService() {
_instance ??= CacheService._internal();
return _instance!;
}
/// 初始化缓存(打开 Hive Box)
Future<void> initialize() async {
if (_isInitialized) return;
try {
// TODO: 初始化 Hive
// await Hive.initFlutter();
// await Hive.openBox(AppConstants.cacheBoxName);
// await Hive.openBox(AppConstants.userBoxName);
_isInitialized = true;
print('[CacheService] 缓存服务已初始化(模拟)');
} catch (e) {
print('[CacheService] 初始化失败: $e');
}
}
/// 写入缓存
Future<void> put(String key, dynamic value, {String box = 'default'}) async {
try {
// TODO: 使用 Hive 持久化
// final cacheBox = Hive.box(AppConstants.cacheBoxName);
// await cacheBox.put(key, value);
_memoryCache['$box:$key'] = value;
} catch (e) {
print('[CacheService] 写入缓存失败: $e');
}
}
/// 读取缓存
Future<T?> get<T>(String key, {String box = 'default'}) async {
try {
// TODO: 使用 Hive 读取
// final cacheBox = Hive.box(AppConstants.cacheBoxName);
// return cacheBox.get(key) as T?;
return _memoryCache['$box:$key'] as T?;
} catch (e) {
print('[CacheService] 读取缓存失败: $e');
return null;
}
}
/// 删除缓存
Future<void> delete(String key, {String box = 'default'}) async {
try {
_memoryCache.remove('$box:$key');
} catch (e) {
print('[CacheService] 删除缓存失败: $e');
}
}
/// 清空指定 Box 的所有缓存
Future<void> clearBox({String box = 'default'}) async {
try {
_memoryCache.removeWhere((key, _) => key.startsWith('$box:'));
} catch (e) {
print('[CacheService] 清空缓存失败: $e');
}
}
/// 清空所有缓存
Future<void> clearAll() async {
try {
_memoryCache.clear();
} catch (e) {
print('[CacheService] 清空所有缓存失败: $e');
}
}
/// 缓存 JSON 对象
Future<void> cacheJson(String key, Map<String, dynamic> data, {String box = 'default'}) async {
await put(key, json.encode(data), box: box);
}
/// 读取缓存的 JSON 对象
Future<Map<String, dynamic>?> getCachedJson(String key, {String box = 'default'}) async {
final data = await get<String>(key, box: box);
if (data == null) return null;
try {
return json.decode(data) as Map<String, dynamic>;
} catch (e) {
return null;
}
}
/// 缓存列表数据
Future<void> cacheList(String key, List<Map<String, dynamic>> items, {String box = 'default'}) async {
await put(key, json.encode(items), box: box);
}
/// 读取缓存的列表数据
Future<List<Map<String, dynamic>>?> getCachedList(String key, {String box = 'default'}) async {
final data = await get<String>(key, box: box);
if (data == null) return null;
try {
final decoded = json.decode(data) as List;
return decoded.cast<Map<String, dynamic>>();
} catch (e) {
return null;
}
}
/// 检查是否有缓存
Future<bool> has(String key, {String box = 'default'}) async {
return _memoryCache.containsKey('$box:$key');
}
}
@@ -0,0 +1,141 @@
import 'dart:io';
/// 拍照/相册服务
/// 提供拍照和从相册选择图片的功能
class CameraService {
static CameraService? _instance;
CameraService._internal();
factory CameraService() {
_instance ??= CameraService._internal();
return _instance!;
}
/// 拍照
Future<CapturedImage?> takePicture() async {
try {
// TODO: 使用 image_picker 拍照
// final picker = ImagePicker();
// final XFile? image = await picker.pickImage(
// source: ImageSource.camera,
// maxWidth: 1920,
// maxHeight: 1080,
// imageQuality: 85,
// );
// if (image != null) {
// return CapturedImage(
// path: image.path,
// name: image.name,
// size: await image.length(),
// );
// }
// 模拟拍照结果
print('[CameraService] 拍照(模拟)');
return CapturedImage(
path: '/mock/camera/photo_${DateTime.now().millisecondsSinceEpoch}.jpg',
name: 'photo_${DateTime.now().millisecondsSinceEpoch}.jpg',
size: 1024 * 1024, // 1MB
source: ImageSource.camera,
);
} catch (e) {
print('[CameraService] 拍照失败: $e');
return null;
}
}
/// 从相册选择图片
Future<CapturedImage?> pickFromGallery() async {
try {
// TODO: 使用 image_picker 从相册选择
// final picker = ImagePicker();
// final XFile? image = await picker.pickImage(
// source: ImageSource.gallery,
// maxWidth: 1920,
// maxHeight: 1080,
// imageQuality: 85,
// );
// if (image != null) {
// return CapturedImage(
// path: image.path,
// name: image.name,
// size: await image.length(),
// );
// }
// 模拟选择结果
print('[CameraService] 从相册选择(模拟)');
return CapturedImage(
path: '/mock/gallery/image_${DateTime.now().millisecondsSinceEpoch}.jpg',
name: 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
size: 2 * 1024 * 1024, // 2MB
source: ImageSource.gallery,
);
} catch (e) {
print('[CameraService] 选择图片失败: $e');
return null;
}
}
/// 选择多张图片
Future<List<CapturedImage>> pickMultipleImages() async {
try {
// TODO: 使用 image_picker 多选
// final picker = ImagePicker();
// final List<XFile> images = await picker.pickMultiImage();
// return images.map((img) => CapturedImage(...)).toList();
print('[CameraService] 多选图片(模拟)');
return [];
} catch (e) {
print('[CameraService] 多选图片失败: $e');
return [];
}
}
/// 上传图片文件
Future<String?> uploadImage(File file, {String? uploadPath}) async {
try {
// TODO: 使用 Dio 上传文件
// final formData = FormData.fromMap({
// 'file': await MultipartFile.fromFile(file.path),
// });
// final response = await DioClient().post('/upload', data: formData);
// return response.data['data']['url'];
print('[CameraService] 上传图片(模拟): ${file.path}');
return 'https://api.example.com/uploads/mock_image.jpg';
} catch (e) {
print('[CameraService] 上传图片失败: $e');
return null;
}
}
}
/// 拍摄/选择的图片模型
class CapturedImage {
final String path;
final String name;
final int size;
final ImageSource source;
CapturedImage({
required this.path,
required this.name,
required this.size,
required this.source,
});
String get sizeFormatted {
if (size < 1024) return '$size B';
if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)} KB';
return '${(size / 1024 / 1024).toStringAsFixed(1)} MB';
}
}
/// 图片来源
enum ImageSource {
camera,
gallery,
}
@@ -0,0 +1,125 @@
import 'dart:async';
/// GPS 定位服务
/// 提供获取当前位置、权限请求等功能
class LocationService {
static LocationService? _instance;
LocationService._internal();
factory LocationService() {
_instance ??= LocationService._internal();
return _instance!;
}
/// 请求位置权限
Future<bool> requestPermission() async {
// TODO: 使用 geolocator 请求权限
// final permission = await Geolocator.requestPermission();
// return permission == LocationPermission.always || permission == LocationPermission.whileInUse;
print('[LocationService] 请求位置权限(模拟)');
return true;
}
/// 检查位置权限状态
Future<bool> hasPermission() async {
// TODO: 使用 geolocator 检查权限
return true;
}
/// 检查 GPS 是否开启
Future<bool> isGpsEnabled() async {
// TODO: 使用 geolocator 检查 GPS 状态
// return await Geolocator.isLocationServiceEnabled();
return true;
}
/// 获取当前位置
Future<LocationData?> getCurrentLocation() async {
try {
final hasPermission = await requestPermission();
if (!hasPermission) return null;
// TODO: 使用 geolocator 获取位置
// final position = await Geolocator.getCurrentPosition(
// desiredAccuracy: LocationAccuracy.high,
// );
// return LocationData(
// latitude: position.latitude,
// longitude: position.longitude,
// accuracy: position.accuracy,
// );
// 模拟位置数据(杭州市中心)
return LocationData(
latitude: 30.2741,
longitude: 120.1551,
accuracy: 10.0,
altitude: 15.0,
timestamp: DateTime.now(),
);
} catch (e) {
print('[LocationService] 获取位置失败: $e');
return null;
}
}
/// 持续监听位置变化
Stream<LocationData> watchLocation() async* {
// TODO: 使用 geolocator 的位置流
// final positionStream = Geolocator.getPositionStream(
// locationSettings: LocationSettings(accuracy: LocationAccuracy.high),
// );
// await for (final position in positionStream) {
// yield LocationData.fromPosition(position);
// }
// 模拟位置流
while (true) {
await Future.delayed(const Duration(seconds: 5));
yield LocationData(
latitude: 30.2741 + (DateTime.now().millisecondsSinceEpoch % 100) / 10000,
longitude: 120.1551 + (DateTime.now().millisecondsSinceEpoch % 100) / 10000,
accuracy: 10.0,
timestamp: DateTime.now(),
);
}
}
/// 计算两点之间的距离(米)
double distanceBetween(
double startLatitude,
double startLongitude,
double endLatitude,
double endLongitude,
) {
// TODO: 使用 geolocator 计算距离
// return Geolocator.distanceBetween(startLatitude, startLongitude, endLatitude, endLongitude);
// 简单估算
const earthRadius = 6371000.0;
final dLat = (endLatitude - startLatitude) * 3.14159 / 180;
final dLon = (endLongitude - startLongitude) * 3.14159 / 180;
final a = dLat * dLat + dLon * dLon * 0.25;
return earthRadius * 2 * a * 1000;
}
}
/// 位置数据模型
class LocationData {
final double latitude;
final double longitude;
final double accuracy;
final double? altitude;
final DateTime timestamp;
LocationData({
required this.latitude,
required this.longitude,
required this.accuracy,
this.altitude,
required this.timestamp,
});
@override
String toString() => 'LocationData(lat: $latitude, lng: $longitude, accuracy: $accuracy)';
}
@@ -0,0 +1,104 @@
import 'dart:async';
/// 消息推送服务(模拟)
/// 提供推送初始化、消息接收回调等功能
class PushService {
static PushService? _instance;
final StreamController<PushMessage> _messageController = StreamController.broadcast();
/// 推送消息流
Stream<PushMessage> get messageStream => _messageController.stream;
PushService._internal();
factory PushService() {
_instance ??= PushService._internal();
return _instance!;
}
/// 初始化推送服务
Future<void> initialize() async {
// TODO: 集成 flutter_local_notifications 或 firebase_messaging
// 1. 初始化通知插件
// 2. 请求通知权限
// 3. 获取设备推送 Token
// 4. 注册推送 Token 到后端
print('[PushService] 推送服务已初始化(模拟)');
// 模拟延迟推送消息
_simulatePushMessages();
}
/// 获取设备推送 Token
Future<String?> getDeviceToken() async {
// TODO: 获取实际的推送 Token
return 'mock_device_token_${DateTime.now().millisecondsSinceEpoch}';
}
/// 注册推送 Token 到后端
Future<void> registerToken(String token) async {
// TODO: 调用后端接口注册 Token
print('[PushService] Token 已注册: $token');
}
/// 处理接收到的推送消息
void onMessageReceived(Map<String, dynamic> data) {
final message = PushMessage(
id: data['id']?.toString() ?? '',
title: data['title'] ?? '',
body: data['body'] ?? '',
type: data['type'] ?? 'general',
data: data,
timestamp: DateTime.now(),
);
_messageController.add(message);
}
/// 显示本地通知
Future<void> showLocalNotification({
required String title,
required String body,
String? payload,
}) async {
// TODO: 使用 flutter_local_notifications 显示本地通知
print('[PushService] 本地通知: $title - $body');
}
/// 模拟推送消息
void _simulatePushMessages() {
Future.delayed(const Duration(seconds: 10), () {
_messageController.add(PushMessage(
id: 'mock_1',
title: '巡检提醒',
body: '您有一条新的巡检任务待执行',
type: 'patrol',
data: {},
timestamp: DateTime.now(),
));
});
}
/// 释放资源
void dispose() {
_messageController.close();
}
}
/// 推送消息模型
class PushMessage {
final String id;
final String title;
final String body;
final String type;
final Map<String, dynamic> data;
final DateTime timestamp;
PushMessage({
required this.id,
required this.title,
required this.body,
required this.type,
required this.data,
required this.timestamp,
});
}