feat: make core pages local-first offline

This commit is contained in:
2026-08-21 12:40:01 +08:00
parent 8828851631
commit fefa2d74e6
20 changed files with 921 additions and 188 deletions
+28 -1
View File
@@ -4,6 +4,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:miaoji_zhang/shared/api/backend_identity.dart';
import 'package:miaoji_zhang/shared/services/session_store.dart';
enum BackendAvailability { unknown, online, offline }
class ApiClient {
ApiClient._();
static final ApiClient instance = ApiClient._();
@@ -12,6 +14,9 @@ class ApiClient {
static const _legacyTokenKey = 'auth_token';
static final _tokenKey = 'auth_token_${BackendIdentity.scope}';
static final sessionExpired = ValueNotifier<int>(0);
static final availability = ValueNotifier<BackendAvailability>(
BackendAvailability.unknown,
);
static bool _handlingUnauthorized = false;
static const String baseUrl = BackendIdentity.baseUrl;
@@ -31,6 +36,10 @@ class ApiClient {
)
..interceptors.add(
InterceptorsWrapper(
onResponse: (response, handler) {
availability.value = BackendAvailability.online;
handler.next(response);
},
onRequest: (options, handler) async {
final token = await _storage.read(key: _tokenKey);
if (token != null) {
@@ -39,6 +48,11 @@ class ApiClient {
handler.next(options);
},
onError: (error, handler) async {
if (isConnectivityError(error)) {
availability.value = BackendAvailability.offline;
} else if (error.response != null) {
availability.value = BackendAvailability.online;
}
final unauthorized = error.response?.statusCode == 401;
final data = error.response?.data;
final aiDenied =
@@ -74,6 +88,18 @@ class ApiClient {
}
Future<String?> readToken() => _storage.read(key: _tokenKey);
Future<void> probe() async {
try {
await dio.get<void>(
'/api/public/brand',
options: Options(receiveTimeout: const Duration(seconds: 10)),
);
} catch (_) {
// The interceptor owns the reachability state transition.
}
}
Future<void> clearToken() async {
await _storage.delete(key: _tokenKey);
await _storage.delete(key: _legacyTokenKey);
@@ -91,8 +117,9 @@ String apiErrorMessage(Object e) {
if (e is StateError) return e.message;
if (e is DioException) {
final data = e.response?.data;
if (data is Map && data['message'] != null)
if (data is Map && data['message'] != null) {
return data['message'] as String;
}
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.connectionError ||
e.type == DioExceptionType.receiveTimeout) {