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,39 @@
/// 统一 API 响应模型
class ApiResponse<T> {
final int code;
final String message;
final T? data;
ApiResponse({
required this.code,
required this.message,
this.data,
});
bool get isSuccess => code == 200 || code == 0;
factory ApiResponse.fromJson(Map<String, dynamic> json, T Function(dynamic)? fromJsonT) {
return ApiResponse(
code: json['code'] ?? 0,
message: json['message'] ?? '',
data: json['data'] != null && fromJsonT != null ? fromJsonT(json['data']) : json['data'] as T?,
);
}
}
/// 分页响应模型
class PaginatedResponse<T> {
final List<T> list;
final int total;
final int page;
final int pageSize;
PaginatedResponse({
required this.list,
required this.total,
required this.page,
required this.pageSize,
});
bool get hasMore => page * pageSize < total;
}
@@ -0,0 +1,41 @@
import 'package:dio/dio.dart';
import '../../features/auth/services/token_service.dart';
import '../constants/app_constants.dart';
/// Token 认证拦截器
/// 自动在请求头中附加 Token,处理 Token 刷新
class AuthInterceptor extends Interceptor {
final TokenService _tokenService = TokenService();
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _tokenService.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
// Token 过期,尝试刷新
try {
final refreshed = await _tokenService.refreshToken();
if (refreshed) {
// 使用新 Token 重试请求
final token = await _tokenService.getToken();
err.requestOptions.headers['Authorization'] = 'Bearer $token';
final response = await Dio().fetch(err.requestOptions);
handler.resolve(response);
return;
}
} catch (_) {
// 刷新失败,清除 Token 并跳转登录
await _tokenService.clearTokens();
}
}
handler.next(err);
}
}
@@ -0,0 +1,79 @@
import 'package:dio/dio.dart';
import '../constants/app_constants.dart';
import 'auth_interceptor.dart';
/// Dio HTTP 客户端配置
class DioClient {
static DioClient? _instance;
late Dio _dio;
Dio get dio => _dio;
DioClient._internal() {
_dio = Dio(
BaseOptions(
baseUrl: AppConstants.baseUrl + AppConstants.apiVersion,
connectTimeout: const Duration(milliseconds: AppConstants.connectTimeout),
receiveTimeout: const Duration(milliseconds: AppConstants.receiveTimeout),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
// 添加拦截器
_dio.interceptors.addAll([
AuthInterceptor(),
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (obj) => print('[DioLog] $obj'),
),
]);
}
factory DioClient() {
_instance ??= DioClient._internal();
return _instance!;
}
/// GET 请求
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
return _dio.get<T>(path, queryParameters: queryParameters, options: options);
}
/// POST 请求
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
return _dio.post<T>(path, data: data, queryParameters: queryParameters, options: options);
}
/// PUT 请求
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
return _dio.put<T>(path, data: data, queryParameters: queryParameters, options: options);
}
/// DELETE 请求
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
return _dio.delete<T>(path, data: data, queryParameters: queryParameters, options: options);
}
}