| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- 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<ParseComparePage> createState() => _ParseComparePageState();
- }
- class _ParseComparePageState extends State<ParseComparePage> {
- 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<DocumentProvider>(
- 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<TextSpan> _buildFormattedText(String text) {
- final lines = text.split('\n');
- final spans = <TextSpan>[];
- 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"幻觉"导致的错判风险,从而提升财务数据的可靠性与可审计性。''';
- }
- }
|