diff --git a/lib/data/repositories/account_repository.dart b/lib/data/repositories/account_repository.dart index 7ea4923..8b19ea0 100644 --- a/lib/data/repositories/account_repository.dart +++ b/lib/data/repositories/account_repository.dart @@ -87,6 +87,18 @@ abstract class AccountRepository { /// 检查账户是否有交易记录 Future hasTransactions(int accountId); + /// 创建账户平账交易 + /// [accountId] 账户ID + /// [ledgerId] 账本ID + /// [amount] 平账金额(正数表示增加余额,负数表示减少余额) + /// [note] 备注信息 + Future createAdjustmentTransaction({ + required int accountId, + required int ledgerId, + required double amount, + String? note, + }); + /// 响应式监听账户信息变化 Stream watchAccount(int accountId); diff --git a/lib/data/repositories/cloud/cloud_account_repository.dart b/lib/data/repositories/cloud/cloud_account_repository.dart index 0a4cba3..ed395e0 100644 --- a/lib/data/repositories/cloud/cloud_account_repository.dart +++ b/lib/data/repositories/cloud/cloud_account_repository.dart @@ -263,6 +263,11 @@ class CloudAccountRepository implements AccountRepository { if (toAccountId == accountId) { balance += amount; } + } else if (type == 'adjustment') { + // 平账交易:根据金额的正负来决定增加或减少余额 + if (tx['account_id'] == accountId) { + balance += amount; + } } } @@ -298,6 +303,9 @@ class CloudAccountRepository implements AccountRepository { balance += amount; } else if (type == 'expense') { balance -= amount; + } else if (type == 'adjustment') { + // 平账交易:根据金额的正负来决定增加或减少余额 + balance += amount; } } @@ -592,4 +600,14 @@ class CloudAccountRepository implements AccountRepository { return results.map((row) => _accountFromJson(row)).toList(); } + + @override + Future createAdjustmentTransaction({ + required int accountId, + required int ledgerId, + required double amount, + String? note, + }) async { + throw UnimplementedError('云端平账交易创建暂不支持'); + } } diff --git a/lib/data/repositories/cloud/cloud_ledger_repository.dart b/lib/data/repositories/cloud/cloud_ledger_repository.dart index 9d0db72..6a394b3 100644 --- a/lib/data/repositories/cloud/cloud_ledger_repository.dart +++ b/lib/data/repositories/cloud/cloud_ledger_repository.dart @@ -182,6 +182,9 @@ class CloudLedgerRepository implements LedgerRepository { balance += amount; } else if (type == 'expense') { balance -= amount; + } else if (type == 'adjustment') { + // 平账交易:根据金额的正负来决定增加或减少余额 + balance += amount; } // transfer 不影响总余额 } diff --git a/lib/data/repositories/cloud/cloud_repository.dart b/lib/data/repositories/cloud/cloud_repository.dart index 7f9c4fb..e3db6c1 100644 --- a/lib/data/repositories/cloud/cloud_repository.dart +++ b/lib/data/repositories/cloud/cloud_repository.dart @@ -606,6 +606,20 @@ class CloudRepository extends BaseRepository { Stream> watchAccountTransactions(int accountId) => _account.watchAccountTransactions(accountId); + @override + Future createAdjustmentTransaction({ + required int accountId, + required int ledgerId, + required double amount, + String? note, + }) => + _account.createAdjustmentTransaction( + accountId: accountId, + ledgerId: ledgerId, + amount: amount, + note: note, + ); + // Statistics Repository 方法 @override Future> diff --git a/lib/data/repositories/local/local_account_repository.dart b/lib/data/repositories/local/local_account_repository.dart index 65f9c95..d9f6861 100644 --- a/lib/data/repositories/local/local_account_repository.dart +++ b/lib/data/repositories/local/local_account_repository.dart @@ -144,6 +144,9 @@ class LocalAccountRepository implements AccountRepository { } else if (t.type == 'transfer') { // 作为转出账户 balance -= t.amount; + } else if (t.type == 'adjustment') { + // 平账交易:根据金额的正负来决定增加或减少余额 + balance += t.amount; } } @@ -181,6 +184,9 @@ class LocalAccountRepository implements AccountRepository { balance -= tx.amount; } else if (tx.type == 'transfer') { balance -= tx.amount; + } else if (tx.type == 'adjustment') { + // 平账交易:根据金额的正负来决定增加或减少余额 + balance += tx.amount; } } else if (tx.toAccountId == accountId) { // 作为转入账户(转账) @@ -210,6 +216,9 @@ class LocalAccountRepository implements AccountRepository { balance -= tx.amount; } else if (tx.type == 'transfer') { balance -= tx.amount; + } else if (tx.type == 'adjustment') { + // 平账交易:根据金额的正负来决定增加或减少余额 + balance += tx.amount; } } else if (tx.toAccountId == accountId) { // 作为转入账户(转账) @@ -463,4 +472,34 @@ class LocalAccountRepository implements AccountRepository { ..where((a) => a.id.isIn(accountIds))) .get(); } + + @override + Future createAdjustmentTransaction({ + required int accountId, + required int ledgerId, + required double amount, + String? note, + }) async { + logger.info('AccountAdjustment', + '📝 创建平账交易: accountId=$accountId, ledgerId=$ledgerId, amount=$amount, note=$note'); + + try { + final transactionId = + await db.into(db.transactions).insert(TransactionsCompanion.insert( + ledgerId: ledgerId, + type: 'adjustment', + amount: amount.abs(), + accountId: d.Value(accountId), + categoryId: const d.Value(null), + happenedAt: d.Value(DateTime.now()), + note: d.Value(note), + )); + + logger.info('AccountAdjustment', '✅ 平账交易创建成功!ID=$transactionId'); + return transactionId; + } catch (e, stack) { + logger.error('AccountAdjustment', '❌ 创建平账交易失败', e, stack); + rethrow; + } + } } diff --git a/lib/data/repositories/local/local_ledger_repository.dart b/lib/data/repositories/local/local_ledger_repository.dart index dc5391f..f29fa4a 100644 --- a/lib/data/repositories/local/local_ledger_repository.dart +++ b/lib/data/repositories/local/local_ledger_repository.dart @@ -118,6 +118,9 @@ class LocalLedgerRepository implements LedgerRepository { balance += t.amount; } else if (t.type == 'expense') { balance -= t.amount; + } else if (t.type == 'adjustment') { + // 平账交易:根据金额的正负来决定增加或减少余额 + balance += t.amount; } // transfer 不影响总余额 } diff --git a/lib/data/repositories/local/local_repository.dart b/lib/data/repositories/local/local_repository.dart index 6093e39..4daf1cb 100644 --- a/lib/data/repositories/local/local_repository.dart +++ b/lib/data/repositories/local/local_repository.dart @@ -635,6 +635,20 @@ class LocalRepository extends BaseRepository { Stream> watchAccountTransactions(int accountId) => _accountRepo.watchAccountTransactions(accountId); + @override + Future createAdjustmentTransaction({ + required int accountId, + required int ledgerId, + required double amount, + String? note, + }) => + _accountRepo.createAdjustmentTransaction( + accountId: accountId, + ledgerId: ledgerId, + amount: amount, + note: note, + ); + @override Future batchInsertAccounts(List accounts) => _accountRepo.batchInsertAccounts(accounts); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 21be982..e02aaf2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1499,6 +1499,9 @@ "accountTypeOther": "Other", "accountInitialBalance": "Initial Balance", "accountInitialBalanceHint": "Enter initial balance (optional)", + "accountBalanceCurrent": "Current", + "accountBalanceTarget": "Enter target balance", + "accountAdjustmentHint": "Enter actual balance, system will auto-create adjustment record", "accountDeleteWarningTitle": "Confirm Delete", "accountDeleteWarningMessage": "This account has {count} related transactions. After deletion, account information in transaction records will be cleared. Confirm deletion?", "@accountDeleteWarningMessage": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bfd8046..a8ab3cf 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -6570,6 +6570,24 @@ abstract class AppLocalizations { /// **'Enter initial balance (optional)'** String get accountInitialBalanceHint; + /// No description provided for @accountBalanceCurrent. + /// + /// In en, this message translates to: + /// **'Current'** + String get accountBalanceCurrent; + + /// No description provided for @accountBalanceTarget. + /// + /// In en, this message translates to: + /// **'Enter target balance'** + String get accountBalanceTarget; + + /// No description provided for @accountAdjustmentHint. + /// + /// In en, this message translates to: + /// **'Enter actual balance, system will auto-create adjustment record'** + String get accountAdjustmentHint; + /// No description provided for @accountDeleteWarningTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f966e63..660fc2b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3459,6 +3459,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get accountInitialBalanceHint => 'Enter initial balance (optional)'; + @override + String get accountBalanceCurrent => 'Current'; + + @override + String get accountBalanceTarget => 'Enter target balance'; + + @override + String get accountAdjustmentHint => + 'Enter actual balance, system will auto-create adjustment record'; + @override String get accountDeleteWarningTitle => 'Confirm Delete'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index c431e9a..d1dab99 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -3459,6 +3459,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get accountInitialBalanceHint => '请输入初始资金(可选)'; + @override + String get accountBalanceCurrent => '当前'; + + @override + String get accountBalanceTarget => '输入目标余额'; + + @override + String get accountAdjustmentHint => '输入实际余额,系统将自动创建平账记录'; + @override String get accountDeleteWarningTitle => '确认删除'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f7c6dfc..aa324d9 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1395,6 +1395,9 @@ "accountTypeOther": "其他", "accountInitialBalance": "初始资金", "accountInitialBalanceHint": "请输入初始资金(可选)", + "accountBalanceCurrent": "当前", + "accountBalanceTarget": "输入目标余额", + "accountAdjustmentHint": "输入实际余额,系统将自动创建平账记录", "accountDeleteWarningTitle": "确认删除", "accountDeleteWarningMessage": "该账户有 {count} 笔关联交易,删除后交易记录中的账户信息将被清空。确认删除吗?", "@accountDeleteWarningMessage": { diff --git a/lib/pages/account/account_edit_page.dart b/lib/pages/account/account_edit_page.dart index 5ddbd1d..8bc3bf0 100644 --- a/lib/pages/account/account_edit_page.dart +++ b/lib/pages/account/account_edit_page.dart @@ -34,6 +34,7 @@ class _AccountEditPageState extends ConsumerState { bool _saving = false; bool _isNameDuplicate = false; String? _nameErrorText; + double? _currentBalance; // 当前账户余额 // 预设账户类型 static const List accountTypes = [ @@ -49,14 +50,31 @@ class _AccountEditPageState extends ConsumerState { void initState() { super.initState(); _nameController = TextEditingController(text: widget.account?.name ?? ''); - _initialBalanceController = TextEditingController( - text: widget.account?.initialBalance != null && - widget.account!.initialBalance != 0.0 - ? widget.account!.initialBalance.toStringAsFixed(2) - : '', - ); _selectedType = widget.account?.type ?? 'cash'; _selectedCurrency = widget.account?.currency ?? 'CNY'; + + _initialBalanceController = TextEditingController(); + + // 编辑模式:加载当前余额 + if (isEditing) { + _currentBalance = null; + _loadCurrentBalance(); + } else { + _currentBalance = null; + } + } + + Future _loadCurrentBalance() async { + if (!isEditing) return; + + final repo = ref.read(repositoryProvider); + final balance = await repo.getAccountBalance(widget.account!.id); + if (mounted) { + setState(() { + _currentBalance = balance; + _initialBalanceController.text = balance.toStringAsFixed(2); + }); + } } @override @@ -307,7 +325,7 @@ class _AccountEditPageState extends ConsumerState { SizedBox(height: 8.0.scaled(context, ref)), - // 初始资金 + // 账户余额 SectionCard( margin: EdgeInsets.zero, child: Padding( @@ -315,19 +333,36 @@ class _AccountEditPageState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.accountInitialBalance, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: BeeTokens.textPrimary(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isEditing + ? l10n.accountBalance + : l10n.accountInitialBalance, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: BeeTokens.textPrimary(context), + ), + ), + if (isEditing && _currentBalance != null) + Text( + '${l10n.accountBalanceCurrent}: ¥${_currentBalance!.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], ), SizedBox(height: 12.0.scaled(context, ref)), TextFormField( controller: _initialBalanceController, decoration: InputDecoration( - hintText: l10n.accountInitialBalanceHint, + hintText: isEditing + ? l10n.accountBalanceTarget + : l10n.accountInitialBalanceHint, hintStyle: TextStyle(color: Colors.grey[400]), prefixText: '¥ ', prefixStyle: TextStyle( @@ -363,6 +398,16 @@ class _AccountEditPageState extends ConsumerState { return null; }, ), + SizedBox(height: 8.0.scaled(context, ref)), + // 平账提示 + if (isEditing) + Text( + l10n.accountAdjustmentHint, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), ], ), ), @@ -451,6 +496,20 @@ class _AccountEditPageState extends ConsumerState { initialBalanceText.isEmpty ? 0.0 : double.parse(initialBalanceText); if (isEditing) { + // v1.15.0: 平账功能实现 + // 计算当前账户余额 + final currentBalance = await repo.getAccountBalance(widget.account!.id); + final difference = initialBalance - currentBalance; + + // 如果有差额,创建平账交易 + if (difference.abs() > 0.01) { + await repo.createAdjustmentTransaction( + accountId: widget.account!.id, + ledgerId: widget.ledgerId, + amount: difference, + ); + } + // 检查币种是否变化 String? currencyToUpdate; if (_selectedCurrency != widget.account!.currency) { @@ -471,12 +530,13 @@ class _AccountEditPageState extends ConsumerState { currencyToUpdate = _selectedCurrency; } + // 更新账户信息(不更新 initialBalance) await repo.updateAccount( widget.account!.id, name: name, type: _selectedType, currency: currencyToUpdate, - initialBalance: initialBalance, + // initialBalance 不再更新,保持原值 ); } else { await repo.createAccount( diff --git a/lib/widgets/biz/transaction_list.dart b/lib/widgets/biz/transaction_list.dart index 189ff5e..9ea17bf 100644 --- a/lib/widgets/biz/transaction_list.dart +++ b/lib/widgets/biz/transaction_list.dart @@ -353,7 +353,7 @@ class TransactionListState extends ConsumerState { final list = item.$3 as List<({Transaction t, Category? category})>; double dayIncome = 0, dayExpense = 0; for (final it in list) { - // 转账不计入收支统计 + // 转账和平账不计入收支统计 if (it.t.type == 'income') { dayIncome += it.t.amount; }