dashboard_page.dart 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:provider/provider.dart';
  4. import 'package:flutter_lucide/flutter_lucide.dart';
  5. import '../widgets/layout/app_layout.dart';
  6. import '../widgets/common/app_button.dart';
  7. import '../providers/document_provider.dart';
  8. import '../models/document.dart';
  9. import '../utils/constants.dart';
  10. import '../theme/app_colors.dart';
  11. /// 首页/仪表盘
  12. class DashboardPage extends StatefulWidget {
  13. const DashboardPage({Key? key}) : super(key: key);
  14. @override
  15. State<DashboardPage> createState() => _DashboardPageState();
  16. }
  17. class _DashboardPageState extends State<DashboardPage>
  18. with SingleTickerProviderStateMixin {
  19. late AnimationController _animationController;
  20. late Animation<double> _fadeAnimation;
  21. @override
  22. void initState() {
  23. super.initState();
  24. _animationController = AnimationController(
  25. vsync: this,
  26. duration: const Duration(milliseconds: 800),
  27. );
  28. _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
  29. CurvedAnimation(
  30. parent: _animationController,
  31. curve: Curves.easeInOut,
  32. ),
  33. );
  34. _animationController.forward();
  35. }
  36. @override
  37. void dispose() {
  38. _animationController.dispose();
  39. super.dispose();
  40. }
  41. @override
  42. Widget build(BuildContext context) {
  43. return AppLayout(
  44. child: FadeTransition(
  45. opacity: _fadeAnimation,
  46. child: SingleChildScrollView(
  47. child: Column(
  48. mainAxisSize: MainAxisSize.min,
  49. crossAxisAlignment: CrossAxisAlignment.start,
  50. children: [
  51. const SizedBox(height: 24),
  52. // 欢迎区域 - 带渐变背景
  53. _buildWelcomeSection(context),
  54. const SizedBox(height: 40),
  55. // 统计卡片 - 更现代的设计
  56. _buildStatsSection(context),
  57. const SizedBox(height: 40),
  58. // 快速操作 - 网格布局
  59. _buildQuickActions(context),
  60. const SizedBox(height: 40),
  61. // 最近文档 - 改进的列表设计
  62. _buildRecentDocuments(context),
  63. const SizedBox(height: 40),
  64. ],
  65. ),
  66. ),
  67. ),
  68. );
  69. }
  70. Widget _buildWelcomeSection(BuildContext context) {
  71. return Container(
  72. padding: const EdgeInsets.all(32),
  73. decoration: BoxDecoration(
  74. gradient: LinearGradient(
  75. begin: Alignment.topLeft,
  76. end: Alignment.bottomRight,
  77. colors: [
  78. AppColors.primary.withOpacity(0.1),
  79. AppColors.primary.withOpacity(0.05),
  80. ],
  81. ),
  82. borderRadius: BorderRadius.circular(16),
  83. border: Border.all(
  84. color: AppColors.primary.withOpacity(0.2),
  85. width: 1,
  86. ),
  87. ),
  88. child: Row(
  89. children: [
  90. Expanded(
  91. child: Column(
  92. crossAxisAlignment: CrossAxisAlignment.start,
  93. children: [
  94. Row(
  95. children: [
  96. Container(
  97. padding: const EdgeInsets.all(12),
  98. decoration: BoxDecoration(
  99. gradient: LinearGradient(
  100. colors: [AppColors.primary, AppColors.primaryDark],
  101. ),
  102. borderRadius: BorderRadius.circular(12),
  103. boxShadow: [
  104. BoxShadow(
  105. color: AppColors.primary.withOpacity(0.3),
  106. blurRadius: 8,
  107. offset: const Offset(0, 4),
  108. ),
  109. ],
  110. ),
  111. child: const Icon(
  112. LucideIcons.sparkles,
  113. color: Colors.white,
  114. size: 28,
  115. ),
  116. ),
  117. const SizedBox(width: 16),
  118. const Expanded(
  119. child: Column(
  120. crossAxisAlignment: CrossAxisAlignment.start,
  121. children: [
  122. Text(
  123. '欢迎使用灵越智报',
  124. style: TextStyle(
  125. fontSize: 32,
  126. fontWeight: FontWeight.bold,
  127. color: AppColors.textPrimary,
  128. letterSpacing: -0.5,
  129. ),
  130. ),
  131. SizedBox(height: 8),
  132. Text(
  133. '智能文档处理平台 · AI驱动的文档理解与处理',
  134. style: TextStyle(
  135. fontSize: 16,
  136. color: AppColors.textSecondary,
  137. height: 1.5,
  138. ),
  139. ),
  140. ],
  141. ),
  142. ),
  143. ],
  144. ),
  145. const SizedBox(height: 24),
  146. Row(
  147. children: [
  148. AppButton(
  149. text: '立即上传',
  150. type: ButtonType.primary,
  151. icon: LucideIcons.upload,
  152. onPressed: () => context.push(AppRoutes.upload),
  153. ),
  154. const SizedBox(width: 12),
  155. AppButton(
  156. text: '查看文档',
  157. type: ButtonType.secondary,
  158. icon: LucideIcons.folder_open,
  159. onPressed: () {},
  160. ),
  161. ],
  162. ),
  163. ],
  164. ),
  165. ),
  166. ],
  167. ),
  168. );
  169. }
  170. Widget _buildStatsSection(BuildContext context) {
  171. return Consumer<DocumentProvider>(
  172. builder: (context, provider, child) {
  173. final total = provider.documents.length;
  174. final completed = provider.documents
  175. .where((d) => d.status == DocumentStatus.completed)
  176. .length;
  177. final processing = provider.documents
  178. .where(
  179. (d) =>
  180. d.status == DocumentStatus.processing ||
  181. d.status == DocumentStatus.parsing,
  182. )
  183. .length;
  184. final pending = provider.documents
  185. .where((d) => d.status == DocumentStatus.pending)
  186. .length;
  187. return LayoutBuilder(
  188. builder: (context, constraints) {
  189. final isWide = constraints.maxWidth > 600;
  190. return isWide
  191. ? Row(
  192. children: [
  193. Expanded(
  194. child: _buildStatCard(
  195. context,
  196. title: '总文档数',
  197. value: total.toString(),
  198. icon: LucideIcons.file_text,
  199. color: AppColors.primary,
  200. gradient: [AppColors.primary, AppColors.primaryLight],
  201. )),
  202. const SizedBox(width: 16),
  203. Expanded(
  204. child: _buildStatCard(
  205. context,
  206. title: '处理中',
  207. value: processing.toString(),
  208. icon: LucideIcons.loader,
  209. color: AppColors.warning,
  210. gradient: [AppColors.warning, Colors.orange.shade300],
  211. )),
  212. const SizedBox(width: 16),
  213. Expanded(
  214. child: _buildStatCard(
  215. context,
  216. title: '已完成',
  217. value: completed.toString(),
  218. icon: LucideIcons.circle_check,
  219. color: AppColors.success,
  220. gradient: [AppColors.success, Colors.green.shade300],
  221. )),
  222. const SizedBox(width: 16),
  223. Expanded(
  224. child: _buildStatCard(
  225. context,
  226. title: '待处理',
  227. value: pending.toString(),
  228. icon: LucideIcons.clock,
  229. color: AppColors.info,
  230. gradient: [AppColors.info, Colors.blue.shade300],
  231. )),
  232. ],
  233. )
  234. : Column(
  235. children: [
  236. Row(
  237. children: [
  238. Expanded(
  239. child: _buildStatCard(
  240. context,
  241. title: '总文档数',
  242. value: total.toString(),
  243. icon: LucideIcons.file_text,
  244. color: AppColors.primary,
  245. gradient: [
  246. AppColors.primary,
  247. AppColors.primaryLight
  248. ],
  249. )),
  250. const SizedBox(width: 12),
  251. Expanded(
  252. child: _buildStatCard(
  253. context,
  254. title: '处理中',
  255. value: processing.toString(),
  256. icon: LucideIcons.loader,
  257. color: AppColors.warning,
  258. gradient: [
  259. AppColors.warning,
  260. Colors.orange.shade300
  261. ],
  262. )),
  263. ],
  264. ),
  265. const SizedBox(height: 12),
  266. Row(
  267. children: [
  268. Expanded(
  269. child: _buildStatCard(
  270. context,
  271. title: '已完成',
  272. value: completed.toString(),
  273. icon: LucideIcons.circle_check,
  274. color: AppColors.success,
  275. gradient: [
  276. AppColors.success,
  277. Colors.green.shade300
  278. ],
  279. )),
  280. const SizedBox(width: 12),
  281. Expanded(
  282. child: _buildStatCard(
  283. context,
  284. title: '待处理',
  285. value: pending.toString(),
  286. icon: LucideIcons.clock,
  287. color: AppColors.info,
  288. gradient: [AppColors.info, Colors.blue.shade300],
  289. )),
  290. ],
  291. ),
  292. ],
  293. );
  294. },
  295. );
  296. },
  297. );
  298. }
  299. Widget _buildStatCard(
  300. BuildContext context, {
  301. required String title,
  302. required String value,
  303. required IconData icon,
  304. required Color color,
  305. required List<Color> gradient,
  306. }) {
  307. return Container(
  308. decoration: BoxDecoration(
  309. gradient: LinearGradient(
  310. begin: Alignment.topLeft,
  311. end: Alignment.bottomRight,
  312. colors: [
  313. ...gradient.map((c) => c.withOpacity(0.1)),
  314. Colors.white,
  315. ],
  316. ),
  317. borderRadius: BorderRadius.circular(16),
  318. border: Border.all(
  319. color: color.withOpacity(0.2),
  320. width: 1,
  321. ),
  322. boxShadow: [
  323. BoxShadow(
  324. color: color.withOpacity(0.1),
  325. blurRadius: 12,
  326. offset: const Offset(0, 4),
  327. ),
  328. ],
  329. ),
  330. child: Padding(
  331. padding: const EdgeInsets.all(20),
  332. child: Column(
  333. crossAxisAlignment: CrossAxisAlignment.start,
  334. children: [
  335. Row(
  336. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  337. children: [
  338. Container(
  339. padding: const EdgeInsets.all(10),
  340. decoration: BoxDecoration(
  341. gradient: LinearGradient(colors: gradient),
  342. borderRadius: BorderRadius.circular(12),
  343. boxShadow: [
  344. BoxShadow(
  345. color: color.withOpacity(0.3),
  346. blurRadius: 8,
  347. offset: const Offset(0, 2),
  348. ),
  349. ],
  350. ),
  351. child: Icon(icon, color: Colors.white, size: 24),
  352. ),
  353. Container(
  354. padding:
  355. const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
  356. decoration: BoxDecoration(
  357. color: color.withOpacity(0.1),
  358. borderRadius: BorderRadius.circular(12),
  359. ),
  360. child: Text(
  361. '+12%',
  362. style: TextStyle(
  363. fontSize: 12,
  364. fontWeight: FontWeight.w600,
  365. color: color,
  366. ),
  367. ),
  368. ),
  369. ],
  370. ),
  371. const SizedBox(height: 16),
  372. Text(
  373. value,
  374. style: TextStyle(
  375. fontSize: 32,
  376. fontWeight: FontWeight.bold,
  377. color: color,
  378. height: 1.2,
  379. ),
  380. ),
  381. const SizedBox(height: 4),
  382. Text(
  383. title,
  384. style: TextStyle(
  385. fontSize: 14,
  386. color: AppColors.textSecondary,
  387. fontWeight: FontWeight.w500,
  388. ),
  389. ),
  390. ],
  391. ),
  392. ),
  393. );
  394. }
  395. Widget _buildQuickActions(BuildContext context) {
  396. final isWide = MediaQuery.of(context).size.width > 600;
  397. return Column(
  398. crossAxisAlignment: CrossAxisAlignment.start,
  399. children: [
  400. Row(
  401. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  402. children: [
  403. const Text(
  404. '快速操作',
  405. style: TextStyle(
  406. fontSize: 24,
  407. fontWeight: FontWeight.bold,
  408. color: AppColors.textPrimary,
  409. ),
  410. ),
  411. TextButton.icon(
  412. onPressed: () {},
  413. icon: const Icon(LucideIcons.arrow_right, size: 16),
  414. label: const Text('查看全部'),
  415. style: TextButton.styleFrom(
  416. foregroundColor: AppColors.primary,
  417. ),
  418. ),
  419. ],
  420. ),
  421. const SizedBox(height: 20),
  422. isWide
  423. ? Row(
  424. children: [
  425. Expanded(
  426. child: _buildActionCard(
  427. context,
  428. icon: LucideIcons.upload,
  429. title: '上传文档',
  430. subtitle: '支持PDF、Word、图片等多种格式',
  431. color: AppColors.primary,
  432. gradient: [AppColors.primary, AppColors.primaryLight],
  433. onTap: () => context.push(AppRoutes.upload),
  434. )),
  435. const SizedBox(width: 16),
  436. Expanded(
  437. child: _buildActionCard(
  438. context,
  439. icon: LucideIcons.folder_open,
  440. title: '文档管理',
  441. subtitle: '查看、搜索和管理所有文档',
  442. color: AppColors.info,
  443. gradient: [AppColors.info, Colors.blue.shade300],
  444. onTap: () {},
  445. )),
  446. const SizedBox(width: 16),
  447. Expanded(
  448. child: _buildActionCard(
  449. context,
  450. icon: LucideIcons.sparkles,
  451. title: 'AI处理',
  452. subtitle: '智能提取、润色和批注',
  453. color: Colors.purple,
  454. gradient: [Colors.purple, Colors.purple.shade300],
  455. onTap: () {},
  456. )),
  457. ],
  458. )
  459. : Column(
  460. children: [
  461. _buildActionCard(
  462. context,
  463. icon: LucideIcons.upload,
  464. title: '上传文档',
  465. subtitle: '支持PDF、Word、图片等多种格式',
  466. color: AppColors.primary,
  467. gradient: [AppColors.primary, AppColors.primaryLight],
  468. onTap: () => context.push(AppRoutes.upload),
  469. ),
  470. const SizedBox(height: 12),
  471. _buildActionCard(
  472. context,
  473. icon: LucideIcons.folder_open,
  474. title: '文档管理',
  475. subtitle: '查看、搜索和管理所有文档',
  476. color: AppColors.info,
  477. gradient: [AppColors.info, Colors.blue.shade300],
  478. onTap: () {},
  479. ),
  480. const SizedBox(height: 12),
  481. _buildActionCard(
  482. context,
  483. icon: LucideIcons.sparkles,
  484. title: 'AI处理',
  485. subtitle: '智能提取、润色和批注',
  486. color: Colors.purple,
  487. gradient: [Colors.purple, Colors.purple.shade300],
  488. onTap: () {},
  489. ),
  490. ],
  491. ),
  492. ],
  493. );
  494. }
  495. Widget _buildActionCard(
  496. BuildContext context, {
  497. required IconData icon,
  498. required String title,
  499. required String subtitle,
  500. required Color color,
  501. required List<Color> gradient,
  502. required VoidCallback onTap,
  503. }) {
  504. return Material(
  505. color: Colors.transparent,
  506. child: InkWell(
  507. onTap: onTap,
  508. borderRadius: BorderRadius.circular(16),
  509. child: Container(
  510. padding: const EdgeInsets.all(24),
  511. decoration: BoxDecoration(
  512. gradient: LinearGradient(
  513. begin: Alignment.topLeft,
  514. end: Alignment.bottomRight,
  515. colors: [
  516. ...gradient.map((c) => c.withOpacity(0.1)),
  517. Colors.white,
  518. ],
  519. ),
  520. borderRadius: BorderRadius.circular(16),
  521. border: Border.all(
  522. color: color.withOpacity(0.2),
  523. width: 1,
  524. ),
  525. boxShadow: [
  526. BoxShadow(
  527. color: color.withOpacity(0.1),
  528. blurRadius: 12,
  529. offset: const Offset(0, 4),
  530. ),
  531. ],
  532. ),
  533. child: Column(
  534. crossAxisAlignment: CrossAxisAlignment.start,
  535. mainAxisSize: MainAxisSize.min,
  536. children: [
  537. Container(
  538. padding: const EdgeInsets.all(12),
  539. decoration: BoxDecoration(
  540. gradient: LinearGradient(colors: gradient),
  541. borderRadius: BorderRadius.circular(12),
  542. boxShadow: [
  543. BoxShadow(
  544. color: color.withOpacity(0.3),
  545. blurRadius: 8,
  546. offset: const Offset(0, 2),
  547. ),
  548. ],
  549. ),
  550. child: Icon(icon, color: Colors.white, size: 24),
  551. ),
  552. const SizedBox(height: 16),
  553. Text(
  554. title,
  555. style: const TextStyle(
  556. fontSize: 18,
  557. fontWeight: FontWeight.bold,
  558. color: AppColors.textPrimary,
  559. ),
  560. ),
  561. const SizedBox(height: 6),
  562. Text(
  563. subtitle,
  564. style: TextStyle(
  565. fontSize: 13,
  566. color: AppColors.textSecondary,
  567. height: 1.4,
  568. ),
  569. ),
  570. const SizedBox(height: 12),
  571. Row(
  572. children: [
  573. Text(
  574. '立即使用',
  575. style: TextStyle(
  576. fontSize: 13,
  577. fontWeight: FontWeight.w600,
  578. color: color,
  579. ),
  580. ),
  581. const SizedBox(width: 4),
  582. Icon(
  583. LucideIcons.arrow_right,
  584. size: 14,
  585. color: color,
  586. ),
  587. ],
  588. ),
  589. ],
  590. ),
  591. ),
  592. ),
  593. );
  594. }
  595. Widget _buildRecentDocuments(BuildContext context) {
  596. return Consumer<DocumentProvider>(
  597. builder: (context, provider, child) {
  598. final recentDocs = provider.documents.take(6).toList();
  599. return Column(
  600. crossAxisAlignment: CrossAxisAlignment.start,
  601. children: [
  602. Row(
  603. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  604. children: [
  605. const Text(
  606. '最近文档',
  607. style: TextStyle(
  608. fontSize: 24,
  609. fontWeight: FontWeight.bold,
  610. color: AppColors.textPrimary,
  611. ),
  612. ),
  613. TextButton.icon(
  614. onPressed: () {},
  615. icon: const Icon(LucideIcons.arrow_right, size: 16),
  616. label: const Text('查看全部'),
  617. style: TextButton.styleFrom(
  618. foregroundColor: AppColors.primary,
  619. ),
  620. ),
  621. ],
  622. ),
  623. const SizedBox(height: 20),
  624. recentDocs.isEmpty
  625. ? Container(
  626. padding: const EdgeInsets.all(48),
  627. decoration: BoxDecoration(
  628. color: AppColors.backgroundLight,
  629. borderRadius: BorderRadius.circular(16),
  630. border: Border.all(color: AppColors.border),
  631. ),
  632. child: Column(
  633. children: [
  634. Icon(
  635. LucideIcons.file_x,
  636. size: 64,
  637. color: AppColors.textSecondary.withOpacity(0.5),
  638. ),
  639. const SizedBox(height: 16),
  640. Text(
  641. '暂无文档',
  642. style: TextStyle(
  643. fontSize: 16,
  644. color: AppColors.textSecondary,
  645. fontWeight: FontWeight.w500,
  646. ),
  647. ),
  648. const SizedBox(height: 8),
  649. Text(
  650. '上传您的第一个文档开始使用',
  651. style: TextStyle(
  652. fontSize: 14,
  653. color: AppColors.textSecondary.withOpacity(0.7),
  654. ),
  655. ),
  656. const SizedBox(height: 24),
  657. AppButton(
  658. text: '上传文档',
  659. type: ButtonType.primary,
  660. icon: LucideIcons.upload,
  661. onPressed: () => context.push(AppRoutes.upload),
  662. ),
  663. ],
  664. ),
  665. )
  666. : LayoutBuilder(
  667. builder: (context, constraints) {
  668. final width = constraints.maxWidth;
  669. final crossAxisCount = width > 1200
  670. ? 4
  671. : width > 900
  672. ? 3
  673. : width > 600
  674. ? 2
  675. : 1;
  676. return GridView.builder(
  677. shrinkWrap: true,
  678. physics: const NeverScrollableScrollPhysics(),
  679. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  680. crossAxisCount: crossAxisCount,
  681. crossAxisSpacing: 16,
  682. mainAxisSpacing: 16,
  683. childAspectRatio: crossAxisCount == 1 ? 2.4 : 1.6,
  684. ),
  685. itemCount: recentDocs.length,
  686. itemBuilder: (context, index) {
  687. final doc = recentDocs[index];
  688. return _buildDocumentCard(context, doc);
  689. },
  690. );
  691. },
  692. ),
  693. ],
  694. );
  695. },
  696. );
  697. }
  698. Widget _buildDocumentCard(BuildContext context, Document doc) {
  699. return Material(
  700. color: Colors.transparent,
  701. child: InkWell(
  702. onTap: () {
  703. if (doc.status == DocumentStatus.completed) {
  704. context.push('${AppRoutes.parse}/${doc.id}');
  705. }
  706. },
  707. borderRadius: BorderRadius.circular(16),
  708. child: Container(
  709. padding: const EdgeInsets.all(20),
  710. decoration: BoxDecoration(
  711. color: Colors.white,
  712. borderRadius: BorderRadius.circular(16),
  713. border: Border.all(
  714. color: _getStatusColor(doc.status).withOpacity(0.2),
  715. width: 1,
  716. ),
  717. boxShadow: [
  718. BoxShadow(
  719. color: Colors.black.withOpacity(0.04),
  720. blurRadius: 12,
  721. offset: const Offset(0, 4),
  722. ),
  723. ],
  724. ),
  725. child: Column(
  726. crossAxisAlignment: CrossAxisAlignment.start,
  727. children: [
  728. Row(
  729. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  730. children: [
  731. Container(
  732. padding: const EdgeInsets.all(8),
  733. decoration: BoxDecoration(
  734. color: _getStatusColor(doc.status).withOpacity(0.1),
  735. borderRadius: BorderRadius.circular(8),
  736. ),
  737. child: Icon(
  738. _getDocumentIcon(doc.type),
  739. color: _getStatusColor(doc.status),
  740. size: 20,
  741. ),
  742. ),
  743. Container(
  744. padding: const EdgeInsets.symmetric(
  745. horizontal: 8,
  746. vertical: 4,
  747. ),
  748. decoration: BoxDecoration(
  749. color: _getStatusColor(doc.status).withOpacity(0.1),
  750. borderRadius: BorderRadius.circular(6),
  751. ),
  752. child: Text(
  753. doc.status.label,
  754. style: TextStyle(
  755. fontSize: 11,
  756. fontWeight: FontWeight.w600,
  757. color: _getStatusColor(doc.status),
  758. ),
  759. ),
  760. ),
  761. ],
  762. ),
  763. Expanded(child: Container(),),
  764. Text(
  765. doc.name,
  766. style: const TextStyle(
  767. fontSize: 15,
  768. fontWeight: FontWeight.w600,
  769. color: AppColors.textPrimary,
  770. height: 1.3,
  771. ),
  772. maxLines: 2,
  773. overflow: TextOverflow.ellipsis,
  774. ),
  775. const SizedBox(height: 8),
  776. Row(
  777. children: [
  778. Icon(
  779. LucideIcons.file_text,
  780. size: 12,
  781. color: AppColors.textSecondary,
  782. ),
  783. const SizedBox(width: 4),
  784. Text(
  785. doc.formattedFileSize,
  786. style: TextStyle(
  787. fontSize: 12,
  788. color: AppColors.textSecondary,
  789. ),
  790. ),
  791. const SizedBox(width: 12),
  792. Icon(
  793. LucideIcons.clock,
  794. size: 12,
  795. color: AppColors.textSecondary,
  796. ),
  797. const SizedBox(width: 4),
  798. Text(
  799. _formatDate(doc.createdAt),
  800. style: TextStyle(
  801. fontSize: 12,
  802. color: AppColors.textSecondary,
  803. ),
  804. ),
  805. ],
  806. ),
  807. ],
  808. ),
  809. ),
  810. ),
  811. );
  812. }
  813. String _formatDate(DateTime date) {
  814. final now = DateTime.now();
  815. final diff = now.difference(date);
  816. if (diff.inDays > 7) {
  817. return '${date.month}/${date.day}';
  818. } else if (diff.inDays > 0) {
  819. return '${diff.inDays}天前';
  820. } else if (diff.inHours > 0) {
  821. return '${diff.inHours}小时前';
  822. } else if (diff.inMinutes > 0) {
  823. return '${diff.inMinutes}分钟前';
  824. } else {
  825. return '刚刚';
  826. }
  827. }
  828. IconData _getDocumentIcon(DocumentType type) {
  829. switch (type) {
  830. case DocumentType.pdf:
  831. return LucideIcons.file_text;
  832. case DocumentType.word:
  833. return LucideIcons.file_text;
  834. case DocumentType.image:
  835. return LucideIcons.image;
  836. default:
  837. return LucideIcons.file;
  838. }
  839. }
  840. Color _getStatusColor(DocumentStatus status) {
  841. switch (status) {
  842. case DocumentStatus.completed:
  843. return AppColors.success;
  844. case DocumentStatus.processing:
  845. case DocumentStatus.parsing:
  846. return AppColors.warning;
  847. case DocumentStatus.failed:
  848. return AppColors.error;
  849. default:
  850. return AppColors.textSecondary;
  851. }
  852. }
  853. }