2026-07-30 21:47:41 +08:00
|
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:http/http.dart' as http;
|
2026-08-02 01:30:07 +08:00
|
|
|
|
import 'package:image_picker/image_picker.dart';
|
2026-07-30 21:47:41 +08:00
|
|
|
|
|
|
|
|
|
|
class ApiException implements Exception {
|
|
|
|
|
|
const ApiException(this.code, this.message);
|
|
|
|
|
|
|
|
|
|
|
|
final int code;
|
|
|
|
|
|
final String message;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
String toString() => message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ApiClient {
|
|
|
|
|
|
ApiClient(this._tokenProvider, {http.Client? client, String? baseUrl})
|
|
|
|
|
|
: _client = client ?? http.Client(),
|
|
|
|
|
|
baseUrl =
|
|
|
|
|
|
baseUrl ??
|
|
|
|
|
|
const String.fromEnvironment(
|
|
|
|
|
|
'API_BASE_URL',
|
|
|
|
|
|
defaultValue: 'http://10.0.2.2:12426',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
final String baseUrl;
|
|
|
|
|
|
final String Function() _tokenProvider;
|
|
|
|
|
|
final http.Client _client;
|
|
|
|
|
|
|
|
|
|
|
|
Future<Object?> get(String path, {bool authenticated = true}) =>
|
|
|
|
|
|
_send('GET', path, authenticated: authenticated);
|
|
|
|
|
|
|
|
|
|
|
|
Future<Object?> post(
|
|
|
|
|
|
String path, {
|
|
|
|
|
|
Map<String, Object?>? body,
|
|
|
|
|
|
bool authenticated = true,
|
|
|
|
|
|
}) => _send('POST', path, body: body, authenticated: authenticated);
|
|
|
|
|
|
|
|
|
|
|
|
Future<Object?> put(String path, {Map<String, Object?>? body}) => _send('PUT', path, body: body);
|
|
|
|
|
|
|
2026-08-02 01:30:07 +08:00
|
|
|
|
/// 上传跨平台文件对象;浏览器端不能依赖本地文件路径。
|
|
|
|
|
|
Future<String> upload(XFile file, {String contentType = 'image/jpeg'}) async {
|
|
|
|
|
|
final request = http.MultipartRequest(
|
|
|
|
|
|
'POST',
|
|
|
|
|
|
Uri.parse('$baseUrl/upload/file'),
|
|
|
|
|
|
);
|
|
|
|
|
|
request.headers['authorization'] = _tokenProvider();
|
2026-07-30 21:47:41 +08:00
|
|
|
|
request.fields['declared_content_type'] = contentType;
|
2026-08-02 01:30:07 +08:00
|
|
|
|
request.files.add(
|
|
|
|
|
|
http.MultipartFile.fromBytes(
|
|
|
|
|
|
'file',
|
|
|
|
|
|
await file.readAsBytes(),
|
|
|
|
|
|
filename: file.name,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-07-30 21:47:41 +08:00
|
|
|
|
final response = await http.Response.fromStream(await request.send());
|
|
|
|
|
|
final details = _decode(response);
|
|
|
|
|
|
return jsonMap(details)['uri'] as String? ?? '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<Object?> _send(
|
|
|
|
|
|
String method,
|
|
|
|
|
|
String path, {
|
|
|
|
|
|
Map<String, Object?>? body,
|
|
|
|
|
|
bool authenticated = true,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
final request = http.Request(method, Uri.parse('$baseUrl$path'));
|
2026-08-02 01:30:07 +08:00
|
|
|
|
request.headers['accept'] = 'application/json';
|
2026-07-30 21:47:41 +08:00
|
|
|
|
if (authenticated && _tokenProvider().isNotEmpty) {
|
2026-08-02 01:30:07 +08:00
|
|
|
|
request.headers['authorization'] = _tokenProvider();
|
2026-07-30 21:47:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (body != null) {
|
2026-08-02 01:30:07 +08:00
|
|
|
|
request.headers['content-type'] = 'application/json; charset=UTF-8';
|
2026-07-30 21:47:41 +08:00
|
|
|
|
request.body = jsonEncode(body);
|
|
|
|
|
|
}
|
2026-08-02 01:30:07 +08:00
|
|
|
|
final response = await http.Response.fromStream(
|
|
|
|
|
|
await _client.send(request),
|
|
|
|
|
|
);
|
2026-07-30 21:47:41 +08:00
|
|
|
|
return _decode(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Object? _decode(http.Response response) {
|
|
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
|
|
|
|
throw ApiException(response.statusCode, '网络请求失败(${response.statusCode})');
|
|
|
|
|
|
}
|
|
|
|
|
|
final decoded = jsonDecode(response.body);
|
|
|
|
|
|
if (decoded is! Map<String, Object?>) {
|
|
|
|
|
|
throw const ApiException(500, '服务端响应格式错误');
|
|
|
|
|
|
}
|
|
|
|
|
|
final code = (decoded['code'] as num?)?.toInt() ?? 500;
|
|
|
|
|
|
if (code != 0) throw ApiException(code, decoded['message'] as String? ?? '操作失败');
|
|
|
|
|
|
return decoded['details'];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, Object?> jsonMap(Object? value) {
|
|
|
|
|
|
if (value is Map<String, Object?>) return value;
|
|
|
|
|
|
if (value is Map) return value.map((key, item) => MapEntry(key.toString(), item));
|
|
|
|
|
|
throw const ApiException(500, '服务端数据格式错误');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<Map<String, Object?>> jsonList(Object? value) {
|
|
|
|
|
|
if (value is! List) return const [];
|
|
|
|
|
|
return value.map<Map<String, Object?>>(jsonMap).toList(growable: false);
|
|
|
|
|
|
}
|