Initial project import

This commit is contained in:
2026-07-24 23:11:20 +08:00
commit 6396eabb87
372 changed files with 49682 additions and 0 deletions
@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:miaoji_zhang/shared/api/api_client.dart';
import 'package:miaoji_zhang/shared/services/local_database.dart';
import 'package:miaoji_zhang/shared/services/sync_service.dart';
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
import 'package:miaoji_zhang/shared/widgets/app_controls.dart';
class SyncConflictsPage extends StatefulWidget {
const SyncConflictsPage({super.key});
@override
State<SyncConflictsPage> createState() => _SyncConflictsPageState();
}
class _SyncConflictsPageState extends State<SyncConflictsPage> {
List<Map<String, dynamic>> _items = const [];
int? _resolvingId;
@override
void initState() {
super.initState();
_load();
}
void _load() {
setState(() => _items = LocalDatabase.instance.conflicts());
}
Future<void> _resolve(
Map<String, dynamic> item, {
required bool keepLocal,
}) async {
setState(() => _resolvingId = item['id'] as int);
try {
await SyncService.instance.resolveConflict(item, keepLocal: keepLocal);
if (mounted) _load();
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(apiErrorMessage(error))));
}
} finally {
if (mounted) setState(() => _resolvingId = null);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('同步冲突')),
body: _items.isEmpty
? Center(
child: Text(
'没有需要处理的同步冲突',
style: TextStyle(color: context.jz.text3),
),
)
: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _items.length,
separatorBuilder: (_, __) => SizedBox(height: 10),
itemBuilder: (context, index) => _conflictCard(_items[index]),
),
);
}
Widget _conflictCard(Map<String, dynamic> item) {
final local = item['local'] as Map<String, dynamic>;
final remote = item['remote'] as Map<String, dynamic>;
final resolving = _resolvingId == item['id'];
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.sync_problem_rounded, color: AppTheme.orange),
SizedBox(width: 8),
Expanded(
child: Text(
_operationLabel(item['operation'] as String),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800),
),
),
],
),
SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _version('本地版本', local, context.jz.primaryBackground),
),
SizedBox(width: 9),
Expanded(
child: _version('云端版本', remote, context.jz.aiBackground),
),
],
),
SizedBox(height: 12),
Row(
children: [
Expanded(
child: JzActionButton(
label: '使用云端',
secondary: true,
loading: resolving,
onPressed: resolving
? null
: () => _resolve(item, keepLocal: false),
),
),
SizedBox(width: 9),
Expanded(
child: JzActionButton(
label: '保留本地',
loading: resolving,
onPressed: resolving
? null
: () => _resolve(item, keepLocal: true),
),
),
],
),
],
),
),
);
}
Widget _version(String title, Map<String, dynamic> value, Color background) {
final amount = value['amount'];
return Container(
constraints: const BoxConstraints(minHeight: 92),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(fontSize: 10.5, color: context.jz.text3),
),
SizedBox(height: 6),
if (amount != null)
Text(
'¥${(amount as num).toStringAsFixed(2)}',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800),
),
SizedBox(height: 3),
Text(
(value['note'] ?? value['categoryName'] ?? '无可展示内容').toString(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 10.5, color: context.jz.text2),
),
],
),
);
}
String _operationLabel(String operation) => switch (operation) {
'delete' => '同一账单在云端有新修改,是否仍删除?',
'restore' => '同一账单在云端有新修改,是否仍恢复?',
_ => '本地与云端都修改了同一账单',
};
}