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 createState() => _SyncConflictsPageState(); } class _SyncConflictsPageState extends State { List> _items = const []; int? _resolvingId; @override void initState() { super.initState(); _load(); } void _load() { setState(() => _items = LocalDatabase.instance.conflicts()); } Future _resolve( Map 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 item) { final local = item['local'] as Map; final remote = item['remote'] as Map; 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 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' => '同一账单在云端有新修改,是否仍恢复?', _ => '本地与云端都修改了同一账单', }; }