import 'package:flutter/material.dart'; import 'package:miaoji_zhang/shared/services/transaction_events.dart'; import 'package:miaoji_zhang/shared/api/api_client.dart'; import 'package:miaoji_zhang/shared/api/business_api.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; import 'package:miaoji_zhang/shared/widgets/category_icon.dart'; Future showParseSheet( BuildContext context, { required String source, String? initialText, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _ParseSheet(source: source, initialText: initialText), ); } class _ParseSheet extends StatefulWidget { final String source; final String? initialText; const _ParseSheet({required this.source, this.initialText}); @override State<_ParseSheet> createState() => _ParseSheetState(); } class _ParseSheetState extends State<_ParseSheet> { late final TextEditingController _input; ParsedDraft? _draft; bool _parsing = false; bool _saving = false; bool get isVoice => widget.source == 'voice'; @override void initState() { super.initState(); _input = TextEditingController(text: widget.initialText ?? ''); if ((widget.initialText ?? '').trim().isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) => _parse()); } } @override void dispose() { _input.dispose(); super.dispose(); } Future _parse() async { final text = _input.text.trim(); if (text.isEmpty) return; setState(() => _parsing = true); try { final d = await ParseApi.parse(text, source: widget.source); if (mounted) setState(() => _draft = d); } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(apiErrorMessage(e)))); } } finally { if (mounted) setState(() => _parsing = false); } } Future _confirm() async { final d = _draft; if (d == null || d.amount <= 0) return; setState(() => _saving = true); try { await ParseApi.confirm(d, widget.source, _input.text.trim()); TransactionEvents.notifyChanged(); if (mounted) Navigator.pop(context, true); } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(apiErrorMessage(e)))); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final d = _draft; return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.vertical(top: Radius.circular(22)), ), child: Padding( padding: const EdgeInsets.fromLTRB(22, 22, 22, 30), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Column( children: [ Text( isVoice ? '语音记账' : '文字识别', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700), ), SizedBox(height: 4), Text( isVoice ? '识别到的语音会自动填到这里,你也可以手动补充或修改。' : '把识别到的文字贴进来,AI 会先帮你解析成账单草稿。', style: TextStyle(fontSize: 10.5, color: context.jz.text3), textAlign: TextAlign.center, ), ], ), ), SizedBox(height: 14), TextField( controller: _input, maxLines: 2, decoration: InputDecoration( hintText: isVoice ? '例如:昨天晚上和朋友吃烧烤 88 支付宝付的' : '例如:永辉超市 合计 128.60 微信支付', ), onChanged: (_) { if (_draft != null) setState(() => _draft = null); }, ), SizedBox(height: 12), if (d == null) SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.ai), onPressed: _parsing ? null : _parse, child: _parsing ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text('开始识别'), ), ) else ...[ Container( padding: const EdgeInsets.all(13), decoration: BoxDecoration( color: context.jz.aiBackground, borderRadius: BorderRadius.circular(13), ), child: Row( children: [ CategoryIconBox(iconKey: d.categoryIcon, size: 38), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '¥${d.amount.toStringAsFixed(2)}', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: d.isIncome ? AppTheme.primary : AppTheme.red, ), ), SizedBox(height: 3), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: (d.isIncome ? AppTheme.primary : AppTheme.red) .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(5), ), child: Text( d.typeLabel, style: TextStyle( fontSize: 9.5, fontWeight: FontWeight.w700, color: d.isIncome ? AppTheme.primary : AppTheme.red, ), ), ), SizedBox(width: 6), Flexible( child: Text( d.categoryName + (d.paymentMethod == null ? '' : ' · ' + d.paymentMethod!), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, color: context.jz.text2, ), ), ), ], ), if (!d.matched) Text( '没有识别出有效金额,请修改后重新识别。', style: TextStyle( fontSize: 10.5, color: AppTheme.red, ), ), ], ), ), ], ), ), SizedBox(height: 12), Row( children: [ Expanded( child: OutlinedButton( onPressed: () => setState(() => _draft = null), child: Text( '重新识别', style: TextStyle(color: context.jz.text2), ), ), ), SizedBox(width: 10), Expanded( child: ElevatedButton( onPressed: (!d.matched || _saving) ? null : _confirm, child: _saving ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text('确认入账'), ), ), ], ), ], ], ), ), ); } }