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
+151
View File
@@ -0,0 +1,151 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/bill_api.dart';
import '../core/network/api_exception.dart';
import '../models/bill_request.dart';
import '../utils/date_util.dart';
import 'bill_refresh.dart';
/// 账单编辑页的表单状态
class BillEditState {
final bool loading; // 详情加载中
final String? loadError; // 详情加载失败提示
final int type; // 1=支出 2=收入
final int? categoryId;
final double? amount;
final String billDate; // yyyy-MM-dd
final String description;
final bool submitting;
final int revision; // 服务端数据回填时 +1,供页面同步非受控输入框
const BillEditState({
this.loading = false,
this.loadError,
this.type = 1,
this.categoryId,
this.amount,
required this.billDate,
this.description = '',
this.submitting = false,
this.revision = 0,
});
BillEditState copyWith({
bool? loading,
Object? loadError = _sentinel,
int? type,
int? categoryId,
Object? amount = _sentinel,
String? billDate,
String? description,
bool? submitting,
int? revision,
}) {
return BillEditState(
loading: loading ?? this.loading,
loadError: loadError == _sentinel ? this.loadError : loadError as String?,
type: type ?? this.type,
categoryId: categoryId ?? this.categoryId,
amount: amount == _sentinel ? this.amount : amount as double?,
billDate: billDate ?? this.billDate,
description: description ?? this.description,
submitting: submitting ?? this.submitting,
revision: revision ?? this.revision,
);
}
static const _sentinel = Object();
}
class BillEditNotifier extends Notifier<BillEditState> {
BillEditNotifier(this.billId);
final int billId;
@override
BillEditState build() {
_load();
return BillEditState(billDate: formatDate(DateTime.now()), loading: true);
}
Future<void> _load() async {
try {
final bill = await ref.read(billApiProvider).getBill(billId);
if (!ref.mounted) return;
state = BillEditState(
type: bill.type,
categoryId: bill.categoryId,
amount: bill.absAmount,
billDate: bill.billDate,
description: bill.description,
revision: state.revision + 1,
);
} on ApiException catch (e) {
if (!ref.mounted) return;
state = state.copyWith(loading: false, loadError: e.message);
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(loading: false, loadError: '账单加载失败');
}
}
/// 重新加载(错误重试用)
void reload() {
state = state.copyWith(loading: true, loadError: null);
_load();
}
void setType(int type) {
if (type == state.type) return;
// 切换收支后分类需重选(与记账页行为一致)
state = state.copyWith(type: type, categoryId: null);
}
void setCategory(int id) => state = state.copyWith(categoryId: id);
void setAmount(double? value) => state = state.copyWith(amount: value);
void setDate(String date) => state = state.copyWith(billDate: date);
void setDescription(String desc) => state = state.copyWith(description: desc);
/// 保存:必须回传全部字段,否则后端会把 description 覆盖成 null
Future<void> save() async {
final s = state;
if (s.categoryId == null) throw ApiException(-1, '请选择分类');
if (s.amount == null || s.amount! <= 0) throw ApiException(-1, '请输入有效金额');
state = state.copyWith(submitting: true);
try {
await ref.read(billApiProvider).updateBill(
billId,
BillRequest(
categoryId: s.categoryId!,
amount: s.amount!,
description: s.description,
billDate: s.billDate,
type: s.type,
),
);
if (!ref.mounted) return;
invalidateBillData(ref);
} finally {
if (ref.mounted) state = state.copyWith(submitting: false);
}
}
/// 删除该账单
Future<void> remove() async {
state = state.copyWith(submitting: true);
try {
await ref.read(billApiProvider).deleteBill(billId);
if (!ref.mounted) return;
invalidateBillData(ref);
} finally {
if (ref.mounted) state = state.copyWith(submitting: false);
}
}
}
final billEditProvider = NotifierProvider.autoDispose
.family<BillEditNotifier, BillEditState, int>(BillEditNotifier.new);