import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import '../widgets/layout/app_layout.dart'; import '../widgets/common/app_button.dart'; import '../widgets/common/app_card.dart'; import '../providers/document_provider.dart'; import '../utils/constants.dart'; import '../theme/app_colors.dart'; /// 解析对比页 class ParseComparePage extends StatefulWidget { final String documentId; const ParseComparePage({Key? key, required this.documentId}) : super(key: key); @override State createState() => _ParseComparePageState(); } class _ParseComparePageState extends State { final ScrollController _leftScrollController = ScrollController(); final ScrollController _rightScrollController = ScrollController(); bool _syncScroll = true; double _leftZoom = 1.0; double _rightZoom = 1.0; @override void initState() { super.initState(); _leftScrollController.addListener(_onLeftScroll); _rightScrollController.addListener(_onRightScroll); } @override void dispose() { _leftScrollController.dispose(); _rightScrollController.dispose(); super.dispose(); } void _onLeftScroll() { if (_syncScroll && _leftScrollController.hasClients) { final ratio = _rightScrollController.position.maxScrollExtent / _leftScrollController.position.maxScrollExtent; if (_rightScrollController.hasClients) { _rightScrollController.jumpTo( _leftScrollController.offset * ratio, ); } } } void _onRightScroll() { if (_syncScroll && _rightScrollController.hasClients) { final ratio = _leftScrollController.position.maxScrollExtent / _rightScrollController.position.maxScrollExtent; if (_leftScrollController.hasClients) { _leftScrollController.jumpTo( _rightScrollController.offset * ratio, ); } } } @override Widget build(BuildContext context) { return Consumer( builder: (context, provider, child) { final document = provider.getDocumentById(widget.documentId); final parsedText = document?.parsedText ?? _getMockParsedText(); return AppLayout( maxContentWidth: 1600, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 24), // 标题栏 _buildHeader(context, document), const SizedBox(height: 32), // 工具栏 _buildToolbar(context), const SizedBox(height: 24), // 对比视图 _buildComparisonView(context, parsedText), const SizedBox(height: 32), // 操作按钮 _buildActionButtons(context), ], ), ), ); }, ); } Widget _buildHeader(BuildContext context, document) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.pop(), style: IconButton.styleFrom( backgroundColor: AppColors.backgroundLight, padding: const EdgeInsets.all(12), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( document?.name ?? '文档解析结果', style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const SizedBox(height: 4), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: AppColors.success.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( LucideIcons.circle_check, size: 12, color: AppColors.success, ), const SizedBox(width: 4), Text( '解析完成', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.success, ), ), ], ), ), const SizedBox(width: 12), Text( '${document?.formattedFileSize ?? "2.0 MB"} · ${_formatDate(document?.createdAt ?? DateTime.now())}', style: TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ], ), ), AppButton( text: '导出', type: ButtonType.secondary, size: ButtonSize.small, icon: LucideIcons.download, onPressed: () {}, ), ], ), ); } Widget _buildToolbar(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Row( children: [ IconButton( icon: Icon( _syncScroll ? LucideIcons.link : LucideIcons.unlink, size: 18, ), tooltip: _syncScroll ? '取消同步滚动' : '同步滚动', onPressed: () { setState(() { _syncScroll = !_syncScroll; }); }, style: IconButton.styleFrom( backgroundColor: _syncScroll ? AppColors.primary.withOpacity(0.1) : Colors.transparent, ), ), const SizedBox(width: 8), const VerticalDivider(width: 1), const SizedBox(width: 8), IconButton( icon: const Icon(LucideIcons.zoom_in, size: 18), tooltip: '放大', onPressed: () { setState(() { _leftZoom = (_leftZoom + 0.1).clamp(0.5, 2.0); _rightZoom = (_rightZoom + 0.1).clamp(0.5, 2.0); }); }, ), IconButton( icon: const Icon(LucideIcons.zoom_out, size: 18), tooltip: '缩小', onPressed: () { setState(() { _leftZoom = (_leftZoom - 0.1).clamp(0.5, 2.0); _rightZoom = (_rightZoom - 0.1).clamp(0.5, 2.0); }); }, ), IconButton( icon: const Icon(LucideIcons.rotate_cw, size: 18), tooltip: '重置', onPressed: () { setState(() { _leftZoom = 1.0; _rightZoom = 1.0; }); }, ), const Spacer(), Text( '${(_leftZoom * 100).toInt()}%', style: TextStyle( fontSize: 12, color: AppColors.textSecondary, fontWeight: FontWeight.w500, ), ), ], ), ); } Widget _buildComparisonView(BuildContext context, String parsedText) { final isWide = MediaQuery.of(context).size.width > AppConstants.tabletBreakpoint; if (isWide) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _buildOriginalDocument(context), ), const SizedBox(width: 20), Expanded( child: _buildParsedText(context, parsedText), ), ], ); } else { return Column( children: [ _buildOriginalDocument(context), const SizedBox(height: 16), _buildParsedText(context, parsedText), ], ); } } Widget _buildOriginalDocument(BuildContext context) { return AppCard( title: Row( children: [ const Icon(LucideIcons.file_text, size: 18, color: AppColors.textPrimary), const SizedBox(width: 8), const Text('原始文档'), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppColors.background, borderRadius: BorderRadius.circular(6), ), child: Text( 'PDF', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ), ], ), child: Container( height: 700, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.grey.shade50, Colors.grey.shade100, ], ), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Transform.scale( scale: _leftZoom, child: SingleChildScrollView( controller: _leftScrollController, child: Container( padding: const EdgeInsets.all(40), child: Column( children: [ // 模拟PDF页面 _buildMockPDFPage(), const SizedBox(height: 20), _buildMockPDFPage(), ], ), ), ), ), ), ), ); } Widget _buildMockPDFPage() { return Container( width: double.infinity, padding: const EdgeInsets.all(40), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 模拟PDF文本行 ...List.generate(20, (index) { return Container( margin: const EdgeInsets.only(bottom: 8), height: 16, width: double.infinity, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(4), ), ); }), ], ), ); } Widget _buildParsedText(BuildContext context, String parsedText) { return AppCard( title: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( gradient: LinearGradient( colors: [AppColors.primary, AppColors.primaryLight], ), borderRadius: BorderRadius.circular(6), ), child: const Icon( LucideIcons.sparkles, size: 14, color: Colors.white, ), ), const SizedBox(width: 8), const Text('智能解析结果'), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppColors.success.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( LucideIcons.circle_check, size: 12, color: AppColors.success, ), const SizedBox(width: 4), Text( '可编辑', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.success, ), ), ], ), ), ], ), child: Container( height: 700, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Transform.scale( scale: _rightZoom, child: SingleChildScrollView( controller: _rightScrollController, padding: const EdgeInsets.all(24), child: SelectableText.rich( TextSpan( children: _buildFormattedText(parsedText), style: const TextStyle( fontSize: 14, height: 1.8, color: AppColors.textPrimary, ), ), ), ), ), ), ); } List _buildFormattedText(String text) { final lines = text.split('\n'); final spans = []; for (int i = 0; i < lines.length; i++) { final line = lines[i]; if (line.trim().isEmpty) { spans.add(const TextSpan(text: '\n')); continue; } // 检测标题(以数字开头或包含"一、二、三"等) if (RegExp(r'^[一二三四五六七八九十]+、').hasMatch(line) || RegExp(r'^\d+[\.、]').hasMatch(line)) { spans.add( TextSpan( text: line, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), ); } else { spans.add(TextSpan(text: line)); } if (i < lines.length - 1) { spans.add(const TextSpan(text: '\n')); } } return spans; } Widget _buildActionButtons(BuildContext context) { return Row( children: [ AppButton( text: '继续AI处理', type: ButtonType.primary, icon: LucideIcons.sparkles, onPressed: () { context.push('${AppRoutes.process}/${widget.documentId}'); }, ), const SizedBox(width: 12), AppButton( text: '重新解析', type: ButtonType.secondary, icon: LucideIcons.refresh_cw, onPressed: () {}, ), const SizedBox(width: 12), AppButton( text: '导出结果', type: ButtonType.secondary, icon: LucideIcons.download, onPressed: () {}, ), ], ); } String _formatDate(DateTime date) { final now = DateTime.now(); final diff = now.difference(date); if (diff.inDays > 7) { return '${date.month}/${date.day}'; } else if (diff.inDays > 0) { return '${diff.inDays}天前'; } else if (diff.inHours > 0) { return '${diff.inHours}小时前'; } else { return '刚刚'; } } String _getMockParsedText() { return '''一、项目概述 本项目由腾讯科技有限公司(以下简称"腾讯")投资建设,旨在打造一套面向大型企业财务共享中心的"智能票据处理系统"。系统通过OCR识别、版面理解、要素抽取与人机协同核验,提升发票、合同、报销单据等票据类文档的处理效率与准确率。 二、投资与资金安排 项目总投资金额为人民币3,700万元,其中已支付1,000万元用于样机研制、数据治理与前期试点。剩余资金将依据阶段性里程碑进行分批拨付,涵盖核心算法优化、系统平台化改造、以及与客户生态的对接集成。 三、技术与产品路线 系统采用多模态文档理解技术,将版面结构、语义上下文与业务词典结合,实现对复杂票据的鲁棒解析。产品形态包含:上传解析端、人工牵引标注端、规则与关系构建端,以及结果交付端。 四、收益预期 预计上线后,票据处理效率提升60%以上,人工核验工作量下降40%,并显著降低AI"幻觉"导致的错判风险,从而提升财务数据的可靠性与可审计性。'''; } }