b34697660d
- 新增 AuthSessionInvalidated 事件处理 token 失效场景 - ApiInterceptor 新增 authFailureCallback 单飞机制 - AuthBloc 区分 manual logout 与 auto expiry 语义 - 新增 startup recovery fallback 防止启动卡死 feat: 重构 Calendar DayWeek 视图事件布局引擎 - 新增 DayEventLayoutEngine 解耦事件计算与渲染 - 新增 DayTimelineMetrics 统一时间轴常量 - 新增 DayViewScale 支持捏合缩放 feat: 新增 Settings 页面共享 UI 组件 - 新增 BackTitlePageHeader 统一页面 header - 新增 DetailHeaderActionMenu 统一操作菜单 - 新增 DestructiveActionSheet 统一删除确认 - 新增 AppToggleSwitch 统一开关组件 feat: Chat UI Schema 支持导航操作 - 支持 navigation 类型 action 触发内部路由跳转 - 新增路径验证与参数处理 chore: 更新相关测试覆盖 auth 失效路径
137 lines
3.3 KiB
Dart
137 lines
3.3 KiB
Dart
import 'dart:convert';
|
|
import 'package:dio/dio.dart';
|
|
import 'api_exception.dart';
|
|
import 'api_interceptor.dart';
|
|
import 'i_api_client.dart';
|
|
import '../storage/token_storage.dart';
|
|
|
|
class ApiClient implements IApiClient {
|
|
final Dio _dio;
|
|
final TokenStorage _tokenStorage;
|
|
final ApiInterceptor _interceptor;
|
|
|
|
factory ApiClient({
|
|
required String baseUrl,
|
|
required TokenStorage tokenStorage,
|
|
Dio? dio,
|
|
}) {
|
|
final effectiveDio =
|
|
dio ??
|
|
Dio(
|
|
BaseOptions(
|
|
baseUrl: baseUrl,
|
|
connectTimeout: const Duration(seconds: 10),
|
|
receiveTimeout: const Duration(seconds: 20),
|
|
sendTimeout: const Duration(seconds: 20),
|
|
),
|
|
);
|
|
final interceptor = ApiInterceptor(
|
|
tokenStorage: tokenStorage,
|
|
dio: effectiveDio,
|
|
);
|
|
effectiveDio.interceptors.add(interceptor);
|
|
return ApiClient._(
|
|
dio: effectiveDio,
|
|
tokenStorage: tokenStorage,
|
|
interceptor: interceptor,
|
|
);
|
|
}
|
|
|
|
ApiClient._({
|
|
required Dio dio,
|
|
required TokenStorage tokenStorage,
|
|
required ApiInterceptor interceptor,
|
|
}) : _dio = dio,
|
|
_tokenStorage = tokenStorage,
|
|
_interceptor = interceptor;
|
|
|
|
Dio get dio => _dio;
|
|
|
|
void resetInterceptor() {
|
|
_interceptor.reset();
|
|
}
|
|
|
|
void setRefreshCallback(Future<bool> Function(String) refresh) {
|
|
_interceptor.onTokenRefresh = () async {
|
|
final token = await _tokenStorage.getRefreshToken();
|
|
if (token == null) return false;
|
|
return refresh(token);
|
|
};
|
|
}
|
|
|
|
void setAuthFailureCallback(Future<void> Function() onAuthFailure) {
|
|
_interceptor.onAuthFailure = onAuthFailure;
|
|
}
|
|
|
|
@override
|
|
Future<Response<T>> get<T>(String path, {Options? options}) async {
|
|
try {
|
|
return await _dio.get<T>(path, options: options);
|
|
} on DioException catch (e) {
|
|
throw ApiException.fromDioError(e);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Response<T>> post<T>(
|
|
String path, {
|
|
dynamic data,
|
|
Options? options,
|
|
}) async {
|
|
try {
|
|
return await _dio.post<T>(path, data: data, options: options);
|
|
} on DioException catch (e) {
|
|
throw ApiException.fromDioError(e);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Response<T>> patch<T>(
|
|
String path, {
|
|
dynamic data,
|
|
Options? options,
|
|
}) async {
|
|
try {
|
|
return await _dio.patch<T>(path, data: data, options: options);
|
|
} on DioException catch (e) {
|
|
throw ApiException.fromDioError(e);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Response<T>> delete<T>(
|
|
String path, {
|
|
dynamic data,
|
|
Options? options,
|
|
}) async {
|
|
try {
|
|
return await _dio.delete<T>(path, data: data, options: options);
|
|
} on DioException catch (e) {
|
|
throw ApiException.fromDioError(e);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Stream<String>> getSseLines(
|
|
String path, {
|
|
Map<String, String>? headers,
|
|
}) async {
|
|
try {
|
|
final response = await _dio.get<ResponseBody>(
|
|
path,
|
|
options: Options(responseType: ResponseType.stream, headers: headers),
|
|
);
|
|
final responseBody = response.data;
|
|
if (responseBody == null) {
|
|
return const Stream<String>.empty();
|
|
}
|
|
return responseBody.stream
|
|
.cast<List<int>>()
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter());
|
|
} on DioException catch (e) {
|
|
throw ApiException.fromDioError(e);
|
|
}
|
|
}
|
|
}
|