Каркас приложения: авторизация, зоны/устройства/правила, управление устройствами

HTTP-клиент к /api/v1 с Sanctum-токеном (хранится в secure storage),
модели зеркалят Laravel API Resources. Экраны: логин, список устройств
с живым статусом, детальная страница устройства (история телеметрии,
turn-on/turn-off/set-level с проверкой capability), списки зон и правил
автоматизации (пока только чтение — формы создания/редактирования впереди).
This commit is contained in:
2026-08-12 01:12:39 +05:00
parent 6a00826194
commit 45b8733d22
25 changed files with 1531 additions and 129 deletions
+87
View File
@@ -0,0 +1,87 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'api_config.dart';
/// Thrown for any non-2xx response. `errors` is populated for Laravel's
/// standard 422 validation shape (`{"message": ..., "errors": {field: [...]}}`).
class ApiException implements Exception {
ApiException(this.statusCode, this.message, {this.errors});
final int statusCode;
final String message;
final Map<String, List<String>>? errors;
bool get isUnauthorized => statusCode == 401;
@override
String toString() => message;
}
/// Thin wrapper over `package:http` for the `/api/v1` backend: builds the
/// URL, attaches the bearer token (set by AuthService after login), decodes
/// JSON, and turns non-2xx responses into [ApiException].
class ApiClient {
String? _token;
void setToken(String? token) => _token = token;
Uri _uri(String path) => Uri.parse('${ApiConfig.apiV1Url}$path');
Map<String, String> get _headers => {
'Accept': 'application/json',
'Content-Type': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
Future<dynamic> get(String path) async {
return _handle(await http.get(_uri(path), headers: _headers));
}
Future<dynamic> post(String path, [Map<String, dynamic>? body]) async {
return _handle(await http.post(
_uri(path),
headers: _headers,
body: body == null ? null : jsonEncode(body),
));
}
Future<dynamic> put(String path, [Map<String, dynamic>? body]) async {
return _handle(await http.put(
_uri(path),
headers: _headers,
body: body == null ? null : jsonEncode(body),
));
}
Future<dynamic> delete(String path) async {
return _handle(await http.delete(_uri(path), headers: _headers));
}
dynamic _handle(http.Response response) {
final ok = response.statusCode >= 200 && response.statusCode < 300;
if (response.body.isEmpty) {
if (ok) return null;
throw ApiException(response.statusCode, 'Ошибка запроса (${response.statusCode})');
}
final decoded = jsonDecode(utf8.decode(response.bodyBytes));
if (ok) return decoded;
final message = decoded is Map && decoded['message'] is String
? decoded['message'] as String
: 'Ошибка запроса (${response.statusCode})';
Map<String, List<String>>? errors;
if (decoded is Map && decoded['errors'] is Map) {
errors = (decoded['errors'] as Map).map(
(key, value) => MapEntry(key as String, List<String>.from(value as List)),
);
}
throw ApiException(response.statusCode, message, errors: errors);
}
}