Files
AI-Health/health_app/lib/core/api_client.dart
MingNian 7baf0d513f fix: 登录页白底+渐变边框/胶囊加大/底部提示/今日健康优化/发送按钮蓝紫渐变
- 登录页:输入框白底+渐变紫蓝外框,去掉focus内框;登录按钮白底紫字;协议勾选白底
- 首页胶囊:适老化加大(内边距14x8/字号15/图标17/圆角18)
- 欢迎卡片底部提示:各智能体专属引导文案,字号加大颜色加深
- 今日健康卡片:健康指标改一行总结(异常提醒/正常显示);用药显示具体药品名+状态
- 发送按钮:蓝→紫渐变
- 设置页菜单:白色卡片+圆角16+轻阴影
- IP改localhost:adb reverse解决IP变动问题
- 服药打卡快捷按钮:改为跳转打卡页,不再一键全打
2026-06-11 11:17:13 +08:00

114 lines
3.5 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.

import 'dart:io';
import 'package:dio/dio.dart';
import 'local_database.dart';
/// API 基础地址
const String baseUrl = 'http://localhost:5000';
/// Dio HTTP 客户端封装——带 token 注入、401 自动刷新
class ApiClient {
final Dio _dio;
final LocalDatabase _db;
ApiClient({required LocalDatabase db})
: _db = db,
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 60),
headers: {'Content-Type': 'application/json'},
)) {
_dio.interceptors.add(_AuthInterceptor(this));
_dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: false));
}
Dio get dio => _dio;
Future<String?> get accessToken => _db.read('access_token');
Future<String?> get refreshToken => _db.read('refresh_token');
Future<void> saveTokens(String access, String refresh) async {
await _db.write('access_token', access);
await _db.write('refresh_token', refresh);
}
Future<void> clearTokens() async {
await _db.deleteAll();
}
/// 带 token 的 GET 请求
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
return _dio.get(path, queryParameters: queryParameters);
}
/// 带 token 的 POST 请求
Future<Response> post(String path, {dynamic data}) async {
return _dio.post(path, data: data);
}
/// 带 token 的 PUT 请求
Future<Response> put(String path, {dynamic data}) async {
return _dio.put(path, data: data);
}
/// 带 token 的 DELETE 请求
Future<Response> delete(String path) async {
return _dio.delete(path);
}
/// 上传文件multipart返回文件 URL
Future<String?> uploadFile(String path, File file, {String fieldName = 'file'}) async {
final formData = FormData.fromMap({
fieldName: await MultipartFile.fromFile(file.path, filename: file.path.split('/').last),
});
final res = await _dio.post(path, data: formData);
final data = res.data;
if (data is Map) {
return data['url']?.toString() ?? data['data']?['url']?.toString();
}
return null;
}
}
/// 认证拦截器:自动注入 token + 401 刷新
class _AuthInterceptor extends Interceptor {
final ApiClient _client;
_AuthInterceptor(this._client);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
if (!options.path.contains('/auth/')) {
final token = await _client.accessToken;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
final refresh = await _client.refreshToken;
if (refresh != null) {
try {
final response = await Dio(BaseOptions(baseUrl: baseUrl))
.post('/api/auth/refresh', data: {'refreshToken': refresh});
final data = response.data['data'];
if (data != null) {
await _client.saveTokens(data['accessToken'], data['refreshToken']);
final opts = err.requestOptions;
final token = data['accessToken'];
opts.headers['Authorization'] = 'Bearer $token';
final retryResponse = await Dio(BaseOptions(baseUrl: baseUrl)).fetch(opts);
return handler.resolve(retryResponse);
}
} catch (e) { print('[ApiClient] token刷新失败: $e'); }
}
await _client.clearTokens();
}
handler.next(err);
}
}