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 '../providers/document_provider.dart'; import '../models/document.dart'; import '../utils/constants.dart'; import '../theme/app_colors.dart'; /// 首页/仪表盘 class DashboardPage extends StatefulWidget { const DashboardPage({Key? key}) : super(key: key); @override State createState() => _DashboardPageState(); } class _DashboardPageState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _fadeAnimation; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, ), ); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AppLayout( child: FadeTransition( opacity: _fadeAnimation, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 24), // 欢迎区域 - 带渐变背景 _buildWelcomeSection(context), const SizedBox(height: 40), // 统计卡片 - 更现代的设计 _buildStatsSection(context), const SizedBox(height: 40), // 快速操作 - 网格布局 _buildQuickActions(context), const SizedBox(height: 40), // 最近文档 - 改进的列表设计 _buildRecentDocuments(context), const SizedBox(height: 40), ], ), ), ), ); } Widget _buildWelcomeSection(BuildContext context) { return Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppColors.primary.withOpacity(0.1), AppColors.primary.withOpacity(0.05), ], ), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.primary.withOpacity(0.2), width: 1, ), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( gradient: LinearGradient( colors: [AppColors.primary, AppColors.primaryDark], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppColors.primary.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: const Icon( LucideIcons.sparkles, color: Colors.white, size: 28, ), ), const SizedBox(width: 16), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '欢迎使用灵越智报', style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: AppColors.textPrimary, letterSpacing: -0.5, ), ), SizedBox(height: 8), Text( '智能文档处理平台 · AI驱动的文档理解与处理', style: TextStyle( fontSize: 16, color: AppColors.textSecondary, height: 1.5, ), ), ], ), ), ], ), const SizedBox(height: 24), Row( children: [ AppButton( text: '立即上传', type: ButtonType.primary, icon: LucideIcons.upload, onPressed: () => context.push(AppRoutes.upload), ), const SizedBox(width: 12), AppButton( text: '查看文档', type: ButtonType.secondary, icon: LucideIcons.folder_open, onPressed: () {}, ), ], ), ], ), ), ], ), ); } Widget _buildStatsSection(BuildContext context) { return Consumer( builder: (context, provider, child) { final total = provider.documents.length; final completed = provider.documents .where((d) => d.status == DocumentStatus.completed) .length; final processing = provider.documents .where( (d) => d.status == DocumentStatus.processing || d.status == DocumentStatus.parsing, ) .length; final pending = provider.documents .where((d) => d.status == DocumentStatus.pending) .length; return LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth > 600; return isWide ? Row( children: [ Expanded( child: _buildStatCard( context, title: '总文档数', value: total.toString(), icon: LucideIcons.file_text, color: AppColors.primary, gradient: [AppColors.primary, AppColors.primaryLight], )), const SizedBox(width: 16), Expanded( child: _buildStatCard( context, title: '处理中', value: processing.toString(), icon: LucideIcons.loader, color: AppColors.warning, gradient: [AppColors.warning, Colors.orange.shade300], )), const SizedBox(width: 16), Expanded( child: _buildStatCard( context, title: '已完成', value: completed.toString(), icon: LucideIcons.circle_check, color: AppColors.success, gradient: [AppColors.success, Colors.green.shade300], )), const SizedBox(width: 16), Expanded( child: _buildStatCard( context, title: '待处理', value: pending.toString(), icon: LucideIcons.clock, color: AppColors.info, gradient: [AppColors.info, Colors.blue.shade300], )), ], ) : Column( children: [ Row( children: [ Expanded( child: _buildStatCard( context, title: '总文档数', value: total.toString(), icon: LucideIcons.file_text, color: AppColors.primary, gradient: [ AppColors.primary, AppColors.primaryLight ], )), const SizedBox(width: 12), Expanded( child: _buildStatCard( context, title: '处理中', value: processing.toString(), icon: LucideIcons.loader, color: AppColors.warning, gradient: [ AppColors.warning, Colors.orange.shade300 ], )), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildStatCard( context, title: '已完成', value: completed.toString(), icon: LucideIcons.circle_check, color: AppColors.success, gradient: [ AppColors.success, Colors.green.shade300 ], )), const SizedBox(width: 12), Expanded( child: _buildStatCard( context, title: '待处理', value: pending.toString(), icon: LucideIcons.clock, color: AppColors.info, gradient: [AppColors.info, Colors.blue.shade300], )), ], ), ], ); }, ); }, ); } Widget _buildStatCard( BuildContext context, { required String title, required String value, required IconData icon, required Color color, required List gradient, }) { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ ...gradient.map((c) => c.withOpacity(0.1)), Colors.white, ], ), borderRadius: BorderRadius.circular(16), border: Border.all( color: color.withOpacity(0.2), width: 1, ), boxShadow: [ BoxShadow( color: color.withOpacity(0.1), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( gradient: LinearGradient(colors: gradient), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: color.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Icon(icon, color: Colors.white, size: 24), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( '+12%', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: color, ), ), ), ], ), const SizedBox(height: 16), Text( value, style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: color, height: 1.2, ), ), const SizedBox(height: 4), Text( title, style: TextStyle( fontSize: 14, color: AppColors.textSecondary, fontWeight: FontWeight.w500, ), ), ], ), ), ); } Widget _buildQuickActions(BuildContext context) { final isWide = MediaQuery.of(context).size.width > 600; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '快速操作', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), TextButton.icon( onPressed: () {}, icon: const Icon(LucideIcons.arrow_right, size: 16), label: const Text('查看全部'), style: TextButton.styleFrom( foregroundColor: AppColors.primary, ), ), ], ), const SizedBox(height: 20), isWide ? Row( children: [ Expanded( child: _buildActionCard( context, icon: LucideIcons.upload, title: '上传文档', subtitle: '支持PDF、Word、图片等多种格式', color: AppColors.primary, gradient: [AppColors.primary, AppColors.primaryLight], onTap: () => context.push(AppRoutes.upload), )), const SizedBox(width: 16), Expanded( child: _buildActionCard( context, icon: LucideIcons.folder_open, title: '文档管理', subtitle: '查看、搜索和管理所有文档', color: AppColors.info, gradient: [AppColors.info, Colors.blue.shade300], onTap: () {}, )), const SizedBox(width: 16), Expanded( child: _buildActionCard( context, icon: LucideIcons.sparkles, title: 'AI处理', subtitle: '智能提取、润色和批注', color: Colors.purple, gradient: [Colors.purple, Colors.purple.shade300], onTap: () {}, )), ], ) : Column( children: [ _buildActionCard( context, icon: LucideIcons.upload, title: '上传文档', subtitle: '支持PDF、Word、图片等多种格式', color: AppColors.primary, gradient: [AppColors.primary, AppColors.primaryLight], onTap: () => context.push(AppRoutes.upload), ), const SizedBox(height: 12), _buildActionCard( context, icon: LucideIcons.folder_open, title: '文档管理', subtitle: '查看、搜索和管理所有文档', color: AppColors.info, gradient: [AppColors.info, Colors.blue.shade300], onTap: () {}, ), const SizedBox(height: 12), _buildActionCard( context, icon: LucideIcons.sparkles, title: 'AI处理', subtitle: '智能提取、润色和批注', color: Colors.purple, gradient: [Colors.purple, Colors.purple.shade300], onTap: () {}, ), ], ), ], ); } Widget _buildActionCard( BuildContext context, { required IconData icon, required String title, required String subtitle, required Color color, required List gradient, required VoidCallback onTap, }) { return Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ ...gradient.map((c) => c.withOpacity(0.1)), Colors.white, ], ), borderRadius: BorderRadius.circular(16), border: Border.all( color: color.withOpacity(0.2), width: 1, ), boxShadow: [ BoxShadow( color: color.withOpacity(0.1), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( gradient: LinearGradient(colors: gradient), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: color.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Icon(icon, color: Colors.white, size: 24), ), const SizedBox(height: 16), Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const SizedBox(height: 6), Text( subtitle, style: TextStyle( fontSize: 13, color: AppColors.textSecondary, height: 1.4, ), ), const SizedBox(height: 12), Row( children: [ Text( '立即使用', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: color, ), ), const SizedBox(width: 4), Icon( LucideIcons.arrow_right, size: 14, color: color, ), ], ), ], ), ), ), ); } Widget _buildRecentDocuments(BuildContext context) { return Consumer( builder: (context, provider, child) { final recentDocs = provider.documents.take(6).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '最近文档', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), TextButton.icon( onPressed: () {}, icon: const Icon(LucideIcons.arrow_right, size: 16), label: const Text('查看全部'), style: TextButton.styleFrom( foregroundColor: AppColors.primary, ), ), ], ), const SizedBox(height: 20), recentDocs.isEmpty ? Container( padding: const EdgeInsets.all(48), decoration: BoxDecoration( color: AppColors.backgroundLight, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), ), child: Column( children: [ Icon( LucideIcons.file_x, size: 64, color: AppColors.textSecondary.withOpacity(0.5), ), const SizedBox(height: 16), Text( '暂无文档', style: TextStyle( fontSize: 16, color: AppColors.textSecondary, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( '上传您的第一个文档开始使用', style: TextStyle( fontSize: 14, color: AppColors.textSecondary.withOpacity(0.7), ), ), const SizedBox(height: 24), AppButton( text: '上传文档', type: ButtonType.primary, icon: LucideIcons.upload, onPressed: () => context.push(AppRoutes.upload), ), ], ), ) : LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; final crossAxisCount = width > 1200 ? 4 : width > 900 ? 3 : width > 600 ? 2 : 1; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, crossAxisSpacing: 16, mainAxisSpacing: 16, childAspectRatio: crossAxisCount == 1 ? 2.4 : 1.6, ), itemCount: recentDocs.length, itemBuilder: (context, index) { final doc = recentDocs[index]; return _buildDocumentCard(context, doc); }, ); }, ), ], ); }, ); } Widget _buildDocumentCard(BuildContext context, Document doc) { return Material( color: Colors.transparent, child: InkWell( onTap: () { if (doc.status == DocumentStatus.completed) { context.push('${AppRoutes.parse}/${doc.id}'); } }, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: _getStatusColor(doc.status).withOpacity(0.2), width: 1, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _getStatusColor(doc.status).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( _getDocumentIcon(doc.type), color: _getStatusColor(doc.status), size: 20, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: _getStatusColor(doc.status).withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Text( doc.status.label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _getStatusColor(doc.status), ), ), ), ], ), Expanded(child: Container(),), Text( doc.name, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary, height: 1.3, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Row( children: [ Icon( LucideIcons.file_text, size: 12, color: AppColors.textSecondary, ), const SizedBox(width: 4), Text( doc.formattedFileSize, style: TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), const SizedBox(width: 12), Icon( LucideIcons.clock, size: 12, color: AppColors.textSecondary, ), const SizedBox(width: 4), Text( _formatDate(doc.createdAt), style: TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ], ), ), ), ); } 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 if (diff.inMinutes > 0) { return '${diff.inMinutes}分钟前'; } else { return '刚刚'; } } IconData _getDocumentIcon(DocumentType type) { switch (type) { case DocumentType.pdf: return LucideIcons.file_text; case DocumentType.word: return LucideIcons.file_text; case DocumentType.image: return LucideIcons.image; default: return LucideIcons.file; } } Color _getStatusColor(DocumentStatus status) { switch (status) { case DocumentStatus.completed: return AppColors.success; case DocumentStatus.processing: case DocumentStatus.parsing: return AppColors.warning; case DocumentStatus.failed: return AppColors.error; default: return AppColors.textSecondary; } } }