本次提交包含多项改进: 1. 新增Android启动页资源与配色配置,完善多版本主题适配 2. 全量替换Platform.pathSeparator为硬编码斜杠,修复Web平台路径兼容问题 3. 为大量文件系统操作添加kIsWeb守卫,优化Web端表现 4. 替换硬编码平台判断为platform_utils封装,统一平台检测逻辑 5. 移除冗余代码与默认参数,优化小部件性能 6. 新增Web端适配逻辑,处理不支持的原生功能 7. 更新鸿蒙兼容性工具,完善平台识别与路径处理 8. 优化设备注册错误捕获,避免非致命崩溃 9. 添加启动页图标与背景配置,优化首屏体验
305 lines
9.0 KiB
Dart
305 lines
9.0 KiB
Dart
/// ============================================================
|
||
/// 闲言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})',
|
||
);
|
||
}
|
||
}
|
||
}
|