first commit

This commit is contained in:
2026-09-21 17:37:53 +08:00
commit 67d4dd5b7c
141 changed files with 8558 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
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, '请求失败');
}
}