feat: make core pages local-first offline
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:miaoji_zhang/shared/api/api_client.dart';
|
||||
import 'package:miaoji_zhang/shared/api/business_api.dart';
|
||||
@@ -6,6 +8,7 @@ import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
|
||||
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/app_icons.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/async_error_view.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/backend_status_icon.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/category_icon.dart';
|
||||
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
|
||||
import 'package:miaoji_zhang/shared/services/session_store.dart';
|
||||
@@ -25,13 +28,14 @@ class _ReportPageState extends State<ReportPage> {
|
||||
String? _error;
|
||||
DateTime _anchor = ShanghaiTime.now;
|
||||
_ReportKind _kind = _ReportKind.monthly;
|
||||
int _loadRevision = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
PublicConfigApi.companionNotifier.addListener(_refreshCompanion);
|
||||
CurrentLedgerStore.instance.addListener(_load);
|
||||
_load();
|
||||
unawaited(_load());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -46,24 +50,33 @@ class _ReportPageState extends State<ReportPage> {
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final revision = ++_loadRevision;
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final report = switch (_kind) {
|
||||
_ReportKind.weekly => await ReportApi.weekly(_anchor),
|
||||
_ReportKind.monthly => await ReportApi.monthlyPeriod(
|
||||
_anchor.year,
|
||||
_anchor.month,
|
||||
),
|
||||
_ReportKind.yearly => await ReportApi.yearly(_anchor.year),
|
||||
final period = switch (_kind) {
|
||||
_ReportKind.weekly => 'week',
|
||||
_ReportKind.monthly => 'month',
|
||||
_ReportKind.yearly => 'year',
|
||||
};
|
||||
if (mounted) setState(() => _report = report);
|
||||
final local = ReportApi.local(period, _anchor);
|
||||
if (mounted && revision == _loadRevision) {
|
||||
setState(() => _report = local);
|
||||
}
|
||||
final remote = await ReportApi.remote(period, _anchor);
|
||||
if (mounted && revision == _loadRevision) {
|
||||
setState(() => _report = remote);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) setState(() => _error = apiErrorMessage(error));
|
||||
if (mounted && revision == _loadRevision && _report == null) {
|
||||
setState(() => _error = apiErrorMessage(error));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
if (mounted && revision == _loadRevision) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +121,7 @@ class _ReportPageState extends State<ReportPage> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(SessionStore.instance.aiEnabled ? 'AI 报告' : '报告'),
|
||||
actions: [BackendStatusIcon(onRetry: _load)],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:async';
|
||||
import 'package:miaoji_zhang/shared/services/shanghai_time.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -8,6 +9,7 @@ import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
|
||||
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/app_controls.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/async_error_view.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/backend_status_icon.dart';
|
||||
import 'package:miaoji_zhang/shared/widgets/category_icon.dart';
|
||||
|
||||
class StatsPage extends StatefulWidget {
|
||||
@@ -23,12 +25,13 @@ class _StatsPageState extends State<StatsPage> {
|
||||
PeriodStats? _stats;
|
||||
String? _error;
|
||||
bool _loading = true;
|
||||
int _loadRevision = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
CurrentLedgerStore.instance.addListener(_load);
|
||||
_load();
|
||||
unawaited(_load());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -38,6 +41,7 @@ class _StatsPageState extends State<StatsPage> {
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final revision = ++_loadRevision;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
@@ -45,12 +49,25 @@ class _StatsPageState extends State<StatsPage> {
|
||||
});
|
||||
}
|
||||
try {
|
||||
final stats = await TxApi.periodStats(_period, _anchor);
|
||||
if (mounted) setState(() => _stats = stats);
|
||||
final local = TxApi.periodStatsLocal(_period, _anchor);
|
||||
if (mounted && revision == _loadRevision) {
|
||||
setState(() => _stats = local);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) setState(() => _error = apiErrorMessage(error));
|
||||
if (mounted && revision == _loadRevision) {
|
||||
setState(() => _error = apiErrorMessage(error));
|
||||
}
|
||||
}
|
||||
try {
|
||||
final remote = await TxApi.periodStatsRemote(_period, _anchor);
|
||||
if (mounted && revision == _loadRevision) {
|
||||
setState(() => _stats = remote);
|
||||
}
|
||||
} catch (_) {
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
if (mounted && revision == _loadRevision) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +110,10 @@ class _StatsPageState extends State<StatsPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final stats = _stats;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('统计')),
|
||||
appBar: AppBar(
|
||||
title: Text('统计'),
|
||||
actions: [BackendStatusIcon(onRetry: _load)],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
@@ -105,7 +125,7 @@ class _StatsPageState extends State<StatsPage> {
|
||||
JzOption(value: 'month', label: '月'),
|
||||
JzOption(value: 'year', label: '年'),
|
||||
],
|
||||
onChanged: _loading ? null : _selectPeriod,
|
||||
onChanged: _selectPeriod,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -171,7 +191,7 @@ class _StatsPageState extends State<StatsPage> {
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: '上一个周期',
|
||||
onPressed: _loading ? null : () => _shift(-1),
|
||||
onPressed: () => _shift(-1),
|
||||
icon: Icon(Icons.chevron_left_rounded),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -184,9 +204,7 @@ class _StatsPageState extends State<StatsPage> {
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '下一个周期',
|
||||
onPressed: _loading || _isAfterCurrentPeriod(nextAnchor)
|
||||
? null
|
||||
: () => _shift(1),
|
||||
onPressed: _isAfterCurrentPeriod(nextAnchor) ? null : () => _shift(1),
|
||||
icon: Icon(Icons.chevron_right_rounded),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user