import 'package:dio/dio.dart'; import '../config/app_config.dart'; import '../storage/token_storage.dart'; import 'api_exception.dart'; /// 统一网络客户端:JWT 拦截器 + 统一响应解包(code != 0 抛 ApiException) class ApiClient { final TokenStorage _tokenStorage; late final Dio _dio; /// 401 时回调(登出) void Function()? onUnauthorized; // ignore: prefer_initializing_formals ApiClient({required TokenStorage tokenStorage, Dio? dio}) : _tokenStorage = tokenStorage { _dio = dio ?? Dio(BaseOptions( baseUrl: AppConfig.baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 60), )); _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { final token = _tokenStorage.token; if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (e, handler) { if (e.response?.statusCode == 401) onUnauthorized?.call(); handler.next(e); }, )); } Future post(String path, Map body, {T Function(dynamic data)? parse}) async { final res = await _dio.post(path, data: body); return _unwrap(res, parse); } Future get(String path, {Map? query, T Function(dynamic data)? parse}) async { final res = await _dio.get(path, queryParameters: query); return _unwrap(res, parse); } /// multipart 上传:fields 为文本字段,fileField 为文件字段名 Future upload(String path, Map fields, String fileField, String filePath, {T Function(dynamic data)? parse}) async { final form = FormData.fromMap({ ...fields, fileField: await MultipartFile.fromFile(filePath), }); final res = await _dio.post(path, data: form); return _unwrap(res, parse); } T _unwrap(Response res, T Function(dynamic data)? parse) { final data = res.data; if (data is! Map) { throw ApiException(-1, '响应格式错误'); } final code = data['code'] as int? ?? -1; final message = data['message'] as String? ?? ''; if (code != 0) { throw ApiException(code, message); } final payload = data['data']; if (parse != null) return parse(payload); return payload as T; } }