Files
nj/app/lib/data/remote/api_client.dart
iven 33dc5e19e4 fix(app): Web 平台兼容性修复 + 字体资源 + API base URL
- 添加 Web/Windows 平台支持 (flutter create --platforms)
- 下载字体资源 (NotoSansSC/Caveat Regular+Bold)
- Isar 3.x Web 不兼容:添加 kIsWeb 守卫,Web 上跳过 Isar 初始化
- IsarJournalRepository: instance 返回 nullable,Web 上使用 RemoteJournalRepository
- SyncEngine: persistPendingQueue/restorePendingQueue Web 安全
- SettingsBloc: 从 RepositoryProvider 改为 ListenableProvider
- ApiClient base URL: 8080 → 3000 匹配后端端口
- Isar .g.dart: 64 位 ID 替换为 JS 安全范围值
2026-06-01 17:30:27 +08:00

150 lines
3.9 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.
// API 客户端 — Dio 封装 + JWT 注入 + 离线感知
//
// 核心职责:
// - 封装 Dio HTTP 客户端,统一配置超时和头信息
// - JWT token 自动注入(请求拦截器)
// - 离线状态感知(网络不可用时抛出明确异常)
// - 为 SyncEngine 提供远程操作能力
import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
/// 网络离线异常 — 网络不可用时由 ApiClient 抛出
class OfflineException implements Exception {
final String message;
const OfflineException([this.message = '网络不可用,请检查网络连接']);
@override
String toString() => 'OfflineException: $message';
}
/// API 客户端 — 所有远程请求的统一入口
class ApiClient {
late final Dio _dio;
String? _token;
/// 基础 URL默认指向本地开发服务器
final String baseUrl;
ApiClient({this.baseUrl = 'http://localhost:3000/api/v1'}) {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// 请求拦截器:注入 JWT token
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
if (_token != null) {
options.headers['Authorization'] = 'Bearer $_token';
}
handler.next(options);
},
));
// 响应拦截器:统一错误处理
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
// 401 时自动清除 token需要重新登录
if (error.response?.statusCode == 401) {
_token = null;
}
handler.next(error);
},
));
}
/// 设置 JWT token登录成功后调用
void setToken(String token) => _token = token;
/// 清除 JWT token退出登录时调用
void clearToken() => _token = null;
/// 当前是否已登录
bool get isAuthenticated => _token != null;
/// 检查网络是否可用
Future<bool> _isOnline() async {
final result = await Connectivity().checkConnectivity();
// connectivity_plus 返回 List<ConnectivityResult>
return result.any((r) => r != ConnectivityResult.none);
}
/// 确保网络可用,否则抛出 OfflineException
Future<void> _ensureOnline() async {
final online = await _isOnline();
if (!online) {
throw const OfflineException();
}
}
// ===== CRUD 方法 =====
/// GET 请求
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParams,
}) async {
await _ensureOnline();
return _dio.get<T>(path, queryParameters: queryParams);
}
/// POST 请求(创建资源)
Future<Response<T>> post<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.post<T>(path, data: data);
}
/// PUT 请求(更新资源)
Future<Response<T>> put<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.put<T>(path, data: data);
}
/// DELETE 请求
Future<Response<T>> delete<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.delete<T>(path, data: data);
}
/// PATCH 请求(部分更新)
Future<Response<T>> patch<T>(
String path, {
dynamic data,
}) async {
await _ensureOnline();
return _dio.patch<T>(path, data: data);
}
/// 文件上传multipart/form-data
Future<Response<T>> upload<T>(
String path, {
required String filePath,
required String fileName,
String fieldName = 'file',
Map<String, dynamic>? extraFields,
}) async {
await _ensureOnline();
final formData = FormData.fromMap({
fieldName: await MultipartFile.fromFile(filePath, filename: fileName),
...?extraFields,
});
return _dio.post<T>(path, data: formData);
}
}