feat: make core pages local-first offline

This commit is contained in:
2026-08-21 12:40:01 +08:00
parent 8828851631
commit fefa2d74e6
20 changed files with 921 additions and 188 deletions
@@ -6,6 +6,7 @@ import 'package:miaoji_zhang/shared/services/transaction_events.dart';
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
import 'package:miaoji_zhang/shared/widgets/category_icon.dart';
import 'package:miaoji_zhang/shared/widgets/app_controls.dart';
import 'package:miaoji_zhang/shared/widgets/backend_status_icon.dart';
class RecycleBinPage extends StatefulWidget {
const RecycleBinPage({super.key});
@@ -17,6 +18,7 @@ class RecycleBinPage extends StatefulWidget {
class _RecycleBinPageState extends State<RecycleBinPage> {
List<TxItem> _items = const [];
bool _loading = true;
int _loadRevision = 0;
@override
void initState() {
@@ -25,15 +27,30 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Future<void> _load() async {
final revision = ++_loadRevision;
setState(() => _loading = true);
try {
await CurrentLedgerStore.instance.ensureLoaded();
final items = await TxApi.recycleBin();
if (mounted) setState(() => _items = items);
await CurrentLedgerStore.instance.loadCached();
final cached = TxApi.recycleBinLocal();
if (mounted && revision == _loadRevision) {
setState(() {
_items = cached;
_loading = false;
});
}
final remote = await TxApi.recycleBinRemote();
if (mounted && revision == _loadRevision) {
setState(() => _items = remote);
}
} catch (error) {
if (mounted) _showError(error);
if (mounted && revision == _loadRevision && _loading) {
setState(() => _loading = false);
_showError(error);
}
} finally {
if (mounted) setState(() => _loading = false);
if (mounted && revision == _loadRevision && _loading) {
setState(() => _loading = false);
}
}
}
@@ -48,6 +65,7 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Future<void> _permanentDelete(TxItem item) async {
if (ApiClient.availability.value != BackendAvailability.online) return;
final confirmed = await _confirm('永久删除', '永久删除后无法恢复,聊天中的账单卡片会显示为已删除。');
if (!confirmed) return;
try {
@@ -59,7 +77,10 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
}
Future<void> _clear() async {
if (_items.isEmpty) return;
if (_items.isEmpty ||
ApiClient.availability.value != BackendAvailability.online) {
return;
}
final confirmed = await _confirm('清空回收站', '将永久删除当前账本回收站中的全部账单,无法恢复。');
if (!confirmed) return;
try {
@@ -79,12 +100,14 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
);
Future<void> _showActions(TxItem item) async {
final online = ApiClient.availability.value == BackendAvailability.online;
final action = await showJzOptionSheet<String>(
context,
title: item.note ?? item.categoryName,
options: const [
JzOption(value: 'restore', label: '恢复账单'),
JzOption(value: 'delete', label: '永久删除', subtitle: '删除后无法恢复'),
options: [
const JzOption(value: 'restore', label: '恢复账单'),
if (online)
const JzOption(value: 'delete', label: '永久删除', subtitle: '删除后无法恢复'),
],
);
if (action == 'restore') await _restore(item);
@@ -103,9 +126,16 @@ class _RecycleBinPageState extends State<RecycleBinPage> {
appBar: AppBar(
title: Text('${CurrentLedgerStore.instance.currentName} · 回收站'),
actions: [
TextButton(
onPressed: _items.isEmpty ? null : _clear,
child: Text('清空'),
BackendStatusIcon(onRetry: _load),
ValueListenableBuilder<BackendAvailability>(
valueListenable: ApiClient.availability,
builder: (context, availability, _) => TextButton(
onPressed:
_items.isEmpty || availability != BackendAvailability.online
? null
: _clear,
child: Text('清空'),
),
),
],
),