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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user