Files
xianyan/lib/core/network/api_client.dart
Developer a5d8483797 chore: 完成多平台兼容性优化与资源更新
本次提交包含多项改进:
1. 新增Android启动页资源与配色配置,完善多版本主题适配
2. 全量替换Platform.pathSeparator为硬编码斜杠,修复Web平台路径兼容问题
3. 为大量文件系统操作添加kIsWeb守卫,优化Web端表现
4. 替换硬编码平台判断为platform_utils封装,统一平台检测逻辑
5. 移除冗余代码与默认参数,优化小部件性能
6. 新增Web端适配逻辑,处理不支持的原生功能
7. 更新鸿蒙兼容性工具,完善平台识别与路径处理
8. 优化设备注册错误捕获,避免非致命崩溃
9. 添加启动页图标与背景配置,优化首屏体验
2026-06-05 02:31:34 +08:00

305 lines
9.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — API 客户端
/// 创建时间: 2026-04-20
/// 更新时间: 2026-06-05
/// 作用: Dio 网络请求封装,统一拦截器与错误处理
/// 上次更新: 修复Web平台兼容性createFormData添加kIsWeb守卫
/// ============================================================
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:logger/logger.dart';
import 'api_interceptor.dart';
import 'api_exception.dart';
import 'cache_config.dart';
/// API 客户端单例
///
/// 全局唯一的 Dio 实例,配置 baseUrl / 超时 / 拦截器。
/// 通过 [ApiClient.instance] 访问。
/// 缓存拦截器通过 [initCache] 异步初始化。
class ApiClient {
ApiClient._() {
_dio = Dio(_baseOptions);
_dio.interceptors.addAll([
LogInterceptor(logPrint: _logPrint),
ApiInterceptor(),
]);
}
static final ApiClient _instance = ApiClient._();
/// 全局唯一实例
static ApiClient get instance => _instance;
late final Dio _dio;
/// 缓存是否已初始化
bool _cacheInitialized = false;
/// 日志打印器
static final Logger _logger = Logger(printer: PrettyPrinter(methodCount: 0));
static void _logPrint(Object obj) => _logger.d(obj);
/// 基础配置
static final BaseOptions _baseOptions = BaseOptions(
baseUrl: 'https://tools.wktyl.com',
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
sendTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
);
/// 初始化缓存拦截器需在App启动时调用一次
Future<void> initCache() async {
if (_cacheInitialized) return;
try {
final cacheOptions = CacheConfig.buildOptions();
_dio.interceptors.insert(0, DioCacheInterceptor(options: cacheOptions));
_cacheInitialized = true;
_logger.i('ApiClient: 缓存拦截器初始化成功');
} catch (e) {
_logger.e('ApiClient: 缓存拦截器初始化失败', error: e);
}
}
/// 获取原始 Dio 实例 (高级用法)
Dio get dio => _dio;
// ============================================================
// GET
// ============================================================
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
bool useCache = true,
bool forceRefresh = false,
}) async {
try {
final effectiveOptions = await _buildCacheOptions(
path: path,
options: options,
useCache: useCache,
forceRefresh: forceRefresh,
);
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: effectiveOptions,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// ============================================================
// POST
// ============================================================
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// ============================================================
// PUT
// ============================================================
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// ============================================================
// DELETE
// ============================================================
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// ============================================================
// FormData
// ============================================================
Future<FormData> createFormData(Map<String, dynamic> fields) async {
if (kIsWeb) throw UnsupportedError('Web端不支持文件上传');
final formFields = <MapEntry<String, dynamic>>[];
for (final entry in fields.entries) {
if (entry.value is File) {
final file = entry.value as File;
formFields.add(
MapEntry(
entry.key,
await MultipartFile.fromFile(
file.path,
filename: file.path.split('/').last,
),
),
);
} else {
formFields.add(MapEntry(entry.key, entry.value));
}
}
return FormData.fromMap(Map.fromEntries(formFields));
}
// ============================================================
// 缓存选项构建
// ============================================================
/// 根据 path 和参数构建带缓存策略的 Options
Future<Options?> _buildCacheOptions({
required String path,
Options? options,
bool useCache = true,
bool forceRefresh = false,
}) async {
if (!useCache || !_cacheInitialized) return options;
final baseCacheOptions = CacheConfig.buildOptions();
if (forceRefresh) {
final refreshOpts = CacheConfig.forceRefreshOptions(
baseOptions: baseCacheOptions,
);
return _mergeOptions(options, refreshOpts);
}
final pathCacheOptions = CacheConfig.optionsForPath(
path,
baseOptions: baseCacheOptions,
);
if (pathCacheOptions == null) return options;
return _mergeOptions(options, pathCacheOptions);
}
/// 合并用户 Options 与缓存 Options
Options? _mergeOptions(Options? userOptions, CacheOptions cacheOptions) {
if (userOptions == null) {
return cacheOptions.toOptions();
}
final mergedExtra = <String, dynamic>{
...?userOptions.extra,
...cacheOptions.toOptions().extra ?? {},
};
return userOptions.copyWith(extra: mergedExtra);
}
// ============================================================
// 错误处理
// ============================================================
/// 将 DioException 转换为自定义 ApiException
static ApiException _handleError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return const ApiException(code: -1, message: '连接超时,请检查网络');
case DioExceptionType.connectionError:
return const ApiException(code: -2, message: '网络连接失败');
case DioExceptionType.badResponse:
return _handleBadResponse(e.response);
case DioExceptionType.cancel:
return const ApiException(code: -3, message: '请求已取消');
case DioExceptionType.badCertificate:
return const ApiException(code: -4, message: '证书验证失败');
default:
return const ApiException(code: -5, message: '未知网络错误');
}
}
/// 处理 HTTP 错误码
static ApiException _handleBadResponse(Response<dynamic>? response) {
if (response == null) {
return const ApiException(code: -6, message: '服务器无响应');
}
switch (response.statusCode) {
case 400:
return const ApiException(code: 400, message: '请求参数错误');
case 401:
return const ApiException(code: 401, message: '未授权,请登录');
case 403:
return const ApiException(code: 403, message: '拒绝访问');
case 404:
return const ApiException(code: 404, message: '资源不存在');
case 429:
return const ApiException(code: 429, message: '请求过于频繁');
case 500:
return const ApiException(code: 500, message: '服务器内部错误');
case 502:
return const ApiException(code: 502, message: '网关错误');
case 503:
return const ApiException(code: 503, message: '服务不可用');
default:
return ApiException(
code: response.statusCode ?? -7,
message: '请求失败 (${response.statusCode})',
);
}
}
}