105 lines
3.1 KiB
Dart
105 lines
3.1 KiB
Dart
import 'package:dio/dio.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../core/network/api_exception.dart';
|
||
import '../core/network/dio_client.dart';
|
||
import '../models/bill.dart';
|
||
import '../models/bill_request.dart';
|
||
|
||
final billApiProvider = Provider<BillApi>((ref) => BillApi(ref.watch(dioProvider)));
|
||
|
||
class BillApi {
|
||
final Dio _dio;
|
||
|
||
BillApi(this._dio);
|
||
|
||
/// 账单列表,可选日期区间(yyyy-MM-dd)。
|
||
/// 后端返回裸数组,无分页。
|
||
Future<List<Bill>> getBills({String? startDate, String? endDate}) async {
|
||
try {
|
||
final params = <String, dynamic>{};
|
||
if (startDate != null) params['startDate'] = startDate;
|
||
if (endDate != null) params['endDate'] = endDate;
|
||
final res = await _dio.get('/bills', queryParameters: params);
|
||
return _parseList(res.data).map(Bill.fromJson).toList();
|
||
} on DioException catch (e) {
|
||
throw _unwrap(e);
|
||
}
|
||
}
|
||
|
||
/// 账单详情
|
||
Future<Bill> getBill(int id) async {
|
||
try {
|
||
final res = await _dio.get('/bills/$id');
|
||
return Bill.fromJson(_parseObject(res.data));
|
||
} on DioException catch (e) {
|
||
throw _unwrap(e);
|
||
}
|
||
}
|
||
|
||
/// 创建账单
|
||
Future<void> createBill(BillRequest bill) async {
|
||
try {
|
||
await _dio.post('/bills', data: bill.toJson());
|
||
} on DioException catch (e) {
|
||
throw _unwrap(e);
|
||
}
|
||
}
|
||
|
||
/// 批量创建账单(OCR 直接存储)
|
||
Future<void> createBillsBatch(List<BillRequest> bills) async {
|
||
try {
|
||
await _dio.post('/bills/batch', data: {
|
||
'bills': bills.map((b) => b.toJson()).toList(),
|
||
});
|
||
} on DioException catch (e) {
|
||
throw _unwrap(e);
|
||
}
|
||
}
|
||
|
||
/// 更新账单
|
||
///
|
||
/// 注意:后端对 description 为 null 会覆盖成 null,所以必须回传全部字段
|
||
/// (BillRequest.toJson 不含 imageUrl,后端按「null 保留原值」处理,正好保留原图)。
|
||
Future<Bill> updateBill(int id, BillRequest bill) async {
|
||
try {
|
||
final res = await _dio.put('/bills/$id', data: bill.toJson());
|
||
return Bill.fromJson(_parseObject(res.data));
|
||
} on DioException catch (e) {
|
||
throw _unwrap(e);
|
||
}
|
||
}
|
||
|
||
/// 删除账单(后端为物理删除,不可恢复)
|
||
Future<void> deleteBill(int id) async {
|
||
try {
|
||
await _dio.delete('/bills/$id');
|
||
} on DioException catch (e) {
|
||
throw _unwrap(e);
|
||
}
|
||
}
|
||
|
||
/// 兼容裸数组与 {code,data:[...]} 两种形态
|
||
List<Map<String, dynamic>> _parseList(Object? data) {
|
||
if (data is List) {
|
||
return data.whereType<Map<String, dynamic>>().toList();
|
||
}
|
||
if (data is Map && data['data'] is List) {
|
||
return (data['data'] as List).whereType<Map<String, dynamic>>().toList();
|
||
}
|
||
throw ApiException(-1, '响应格式异常');
|
||
}
|
||
|
||
Map<String, dynamic> _parseObject(Object? data) {
|
||
if (data is Map<String, dynamic>) return data;
|
||
if (data is Map && data['data'] is Map) return Map<String, dynamic>.from(data['data'] as Map);
|
||
throw ApiException(-1, '响应格式异常');
|
||
}
|
||
|
||
Never _unwrap(DioException e) {
|
||
final err = e.error;
|
||
if (err is ApiException) throw err;
|
||
throw ApiException(-1, '请求失败');
|
||
}
|
||
}
|