feat: UI 系统中心化 + 趋势图重构 + 法律文档 H5 上线
- UI 系统: 新增 design_tokens / module_visuals / app_buttons / app_status_badge / app_toast / ai_content 六个共享模块; 28 个页面接入, SnackBar 全部替换为 AppToast - 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格线; X 轴日+月份分隔; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层, 选中点变实心 - 法律文档 H5: 后端 wwwroot 托管隐私政策/服务协议/关于/个人信息收集清单/第三方 SDK 清单 + 索引页; Program.cs 启用 UseDefaultFiles + UseStaticFiles - 其他: auth_provider 登录流程调整; api_client IP 适配; 主页智能体栏间距优化
This commit is contained in:
@@ -6,7 +6,7 @@ import 'local_database.dart';
|
||||
/// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。
|
||||
const String baseUrl = String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://192.168.10.139:5000',
|
||||
defaultValue: 'http://10.4.212.224:5000',
|
||||
);
|
||||
|
||||
class ApiException implements Exception {
|
||||
@@ -19,21 +19,46 @@ class ApiException implements Exception {
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
typedef AuthExpiredListener = void Function();
|
||||
|
||||
class AuthExpiredNotifier {
|
||||
final List<AuthExpiredListener> _listeners = [];
|
||||
|
||||
AuthExpiredListener addListener(AuthExpiredListener listener) {
|
||||
_listeners.add(listener);
|
||||
return () => _listeners.remove(listener);
|
||||
}
|
||||
|
||||
void notify() {
|
||||
for (final listener in List<AuthExpiredListener>.from(_listeners)) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dio HTTP 客户端封装——带 token 注入、401 自动刷新
|
||||
class ApiClient {
|
||||
final Dio _dio;
|
||||
final LocalDatabase _db;
|
||||
final AuthExpiredNotifier? _authExpiredNotifier;
|
||||
|
||||
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'},
|
||||
)) {
|
||||
ApiClient({
|
||||
required LocalDatabase db,
|
||||
AuthExpiredNotifier? authExpiredNotifier,
|
||||
}) : _db = db,
|
||||
_authExpiredNotifier = authExpiredNotifier,
|
||||
_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: false, responseBody: false));
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(requestBody: false, responseBody: false),
|
||||
);
|
||||
}
|
||||
|
||||
Dio get dio => _dio;
|
||||
@@ -51,8 +76,15 @@ class ApiClient {
|
||||
await _db.delete('refresh_token');
|
||||
}
|
||||
|
||||
void notifyAuthExpired() {
|
||||
_authExpiredNotifier?.notify();
|
||||
}
|
||||
|
||||
/// 带 token 的 GET 请求
|
||||
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
|
||||
Future<Response> get(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
return _request(_dio.get(path, queryParameters: queryParameters));
|
||||
}
|
||||
|
||||
@@ -72,9 +104,16 @@ class ApiClient {
|
||||
}
|
||||
|
||||
/// 上传文件(multipart),返回文件 URL
|
||||
Future<String?> uploadFile(String path, File file, {String fieldName = 'file'}) async {
|
||||
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),
|
||||
fieldName: await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: file.path.split('/').last,
|
||||
),
|
||||
});
|
||||
final res = await _request(_dio.post(path, data: formData));
|
||||
final data = res.data;
|
||||
@@ -118,14 +157,15 @@ class ApiClient {
|
||||
return _ensureBusinessSuccess(await request);
|
||||
} on DioException catch (error) {
|
||||
final body = error.response?.data;
|
||||
final message = body is Map && body['message']?.toString().trim().isNotEmpty == true
|
||||
final message =
|
||||
body is Map && body['message']?.toString().trim().isNotEmpty == true
|
||||
? body['message'].toString()
|
||||
: error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout
|
||||
? '请求超时,请稍后重试'
|
||||
: error.type == DioExceptionType.connectionError
|
||||
? '无法连接服务器,请检查网络或后端地址'
|
||||
: '请求失败,请稍后重试';
|
||||
error.type == DioExceptionType.receiveTimeout
|
||||
? '请求超时,请稍后重试'
|
||||
: error.type == DioExceptionType.connectionError
|
||||
? '无法连接服务器,请检查网络或后端地址'
|
||||
: '请求失败,请稍后重试';
|
||||
final rawCode = body is Map ? body['code'] : null;
|
||||
throw ApiException(
|
||||
message,
|
||||
@@ -144,7 +184,10 @@ class _AuthInterceptor extends Interceptor {
|
||||
_AuthInterceptor(this._client);
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
if (!options.path.contains('/auth/')) {
|
||||
final token = await _client.accessToken;
|
||||
if (token != null) {
|
||||
@@ -160,20 +203,26 @@ class _AuthInterceptor extends Interceptor {
|
||||
final refresh = await _client.refreshToken;
|
||||
if (refresh != null) {
|
||||
try {
|
||||
final response = await Dio(BaseOptions(baseUrl: baseUrl))
|
||||
.post('/api/auth/refresh', data: {'refreshToken': refresh});
|
||||
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);
|
||||
final retryResponse = await Dio(
|
||||
BaseOptions(baseUrl: baseUrl),
|
||||
).fetch(opts);
|
||||
return handler.resolve(retryResponse);
|
||||
}
|
||||
} catch (e) { log('[ApiClient] token刷新失败: $e'); }
|
||||
} catch (e) {
|
||||
log('[ApiClient] token刷新失败: $e');
|
||||
}
|
||||
}
|
||||
await _client.clearTokens();
|
||||
_client.notifyAuthExpired();
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user