app_sidebar.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_lucide/flutter_lucide.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../utils/constants.dart';
  5. import '../../theme/app_colors.dart';
  6. import '../common/app_button.dart';
  7. /// 应用侧边栏组件
  8. class AppSidebar extends StatefulWidget {
  9. const AppSidebar({Key? key}) : super(key: key);
  10. @override
  11. State<AppSidebar> createState() => _AppSidebarState();
  12. }
  13. class _AppSidebarState extends State<AppSidebar> {
  14. bool _collapsed = false;
  15. @override
  16. Widget build(BuildContext context) {
  17. final currentPath = GoRouterState.of(context).uri.path;
  18. return AnimatedContainer(
  19. duration: const Duration(milliseconds: 240),
  20. curve: Curves.easeOutCubic,
  21. width: _collapsed ? 84 : 280,
  22. decoration: BoxDecoration(
  23. gradient: const LinearGradient(
  24. colors: [
  25. Color(0xFFF9FBFD),
  26. Color(0xFFFFFFFF),
  27. ],
  28. begin: Alignment.topCenter,
  29. end: Alignment.bottomCenter,
  30. ),
  31. border: Border(
  32. right: BorderSide(color: AppColors.borderLight.withOpacity(0.7)),
  33. ),
  34. boxShadow: [
  35. BoxShadow(
  36. color: Colors.black.withOpacity(0.04),
  37. blurRadius: 24,
  38. offset: const Offset(0, 8),
  39. ),
  40. ],
  41. ),
  42. child: SafeArea(
  43. bottom: true,
  44. child: Column(
  45. children: [
  46. _buildBrandSection(),
  47. const SizedBox(height: 8),
  48. Expanded(child: _buildNavigation(currentPath)),
  49. _buildFooter(),
  50. ],
  51. ),
  52. ),
  53. );
  54. }
  55. Widget _buildBrandSection() {
  56. return Padding(
  57. padding: EdgeInsets.symmetric(
  58. horizontal: _collapsed ? 10 : 18,
  59. vertical: 18,
  60. ),
  61. child: Row(
  62. crossAxisAlignment: CrossAxisAlignment.center,
  63. children: [
  64. Container(
  65. width: 48,
  66. height: 48,
  67. decoration: BoxDecoration(
  68. borderRadius: BorderRadius.circular(14),
  69. gradient: const LinearGradient(
  70. colors: [Color(0xFF4D7CFE), Color(0xFF72B6FB)],
  71. begin: Alignment.topLeft,
  72. end: Alignment.bottomRight,
  73. ),
  74. boxShadow: [
  75. BoxShadow(
  76. color: AppColors.primary.withOpacity(0.25),
  77. blurRadius: 16,
  78. offset: const Offset(0, 8),
  79. ),
  80. ],
  81. ),
  82. child: const Icon(
  83. LucideIcons.sparkles,
  84. color: Colors.white,
  85. size: 24,
  86. ),
  87. ),
  88. if (!_collapsed) ...[
  89. const SizedBox(width: 12),
  90. Expanded(
  91. child: Column(
  92. crossAxisAlignment: CrossAxisAlignment.start,
  93. children: const [
  94. Text(
  95. '灵越智报',
  96. style: TextStyle(
  97. fontSize: 18,
  98. fontWeight: FontWeight.w700,
  99. color: AppColors.textPrimary,
  100. letterSpacing: 0.4,
  101. ),
  102. ),
  103. SizedBox(height: 4),
  104. Text(
  105. '智能票据 · 知识化处理',
  106. style: TextStyle(
  107. fontSize: 11,
  108. color: AppColors.textSecondary,
  109. letterSpacing: 0.2,
  110. ),
  111. ),
  112. ],
  113. ),
  114. ),
  115. ],
  116. Material(
  117. color: Colors.transparent,
  118. child: InkWell(
  119. borderRadius: BorderRadius.circular(12),
  120. onTap: () {
  121. setState(() {
  122. _collapsed = !_collapsed;
  123. });
  124. },
  125. child: Container(
  126. width: 32,
  127. height: 32,
  128. decoration: BoxDecoration(
  129. borderRadius: BorderRadius.circular(12),
  130. color: AppColors.backgroundLight,
  131. ),
  132. child: Icon(
  133. _collapsed
  134. ? LucideIcons.chevron_right
  135. : LucideIcons.chevron_left,
  136. size: 18,
  137. color: AppColors.textSecondary,
  138. ),
  139. ),
  140. ),
  141. ),
  142. ],
  143. ),
  144. );
  145. }
  146. Widget _buildNavigation(String currentPath) {
  147. final sections = [
  148. _SidebarSection(
  149. title: '工作台',
  150. items: [
  151. _SidebarItem(
  152. icon: LucideIcons.house,
  153. label: '概览总览',
  154. path: AppRoutes.home,
  155. ),
  156. _SidebarItem(
  157. icon: LucideIcons.upload,
  158. label: '上传与解析',
  159. path: AppRoutes.upload,
  160. badge: '新',
  161. ),
  162. _SidebarItem(
  163. icon: LucideIcons.sparkles,
  164. label: 'AI 处理流程',
  165. path: AppRoutes.process,
  166. ),
  167. ],
  168. ),
  169. _SidebarSection(
  170. title: '业务中心',
  171. items: [
  172. _SidebarItem(
  173. icon: LucideIcons.layout_panel_left,
  174. label: '文档管理',
  175. path: AppRoutes.documents,
  176. ),
  177. _SidebarItem(
  178. icon: LucideIcons.network,
  179. label: '关系牵引',
  180. path: AppRoutes.traction,
  181. ),
  182. _SidebarItem(
  183. icon: LucideIcons.trending_up,
  184. label: '结果分析',
  185. path: AppRoutes.result,
  186. ),
  187. ],
  188. ),
  189. ];
  190. return ListView(
  191. padding: EdgeInsets.symmetric(
  192. horizontal: _collapsed ? 10 : 16,
  193. vertical: 12,
  194. ),
  195. children: [
  196. if (!_collapsed) _buildInsightCard(),
  197. if (!_collapsed) const SizedBox(height: 16),
  198. for (final section in sections) ...[
  199. _buildSectionHeader(section.title),
  200. const SizedBox(height: 8),
  201. for (final item in section.items)
  202. _buildNavItem(
  203. item,
  204. currentPath == item.path,
  205. ),
  206. const SizedBox(height: 18),
  207. ],
  208. ],
  209. );
  210. }
  211. Widget _buildSectionHeader(String title) {
  212. return AnimatedOpacity(
  213. duration: const Duration(milliseconds: 160),
  214. opacity: _collapsed ? 0 : 1,
  215. child: Row(
  216. children: [
  217. Container(
  218. width: 4,
  219. height: 12,
  220. decoration: BoxDecoration(
  221. color: AppColors.primary,
  222. borderRadius: BorderRadius.circular(2),
  223. ),
  224. ),
  225. const SizedBox(width: 8),
  226. if (!_collapsed)
  227. Text(
  228. title,
  229. style: const TextStyle(
  230. fontSize: 12,
  231. fontWeight: FontWeight.w600,
  232. letterSpacing: 0.4,
  233. color: AppColors.textSecondary,
  234. ),
  235. ),
  236. ],
  237. ),
  238. );
  239. }
  240. Widget _buildNavItem(_SidebarItem item, bool isActive) {
  241. final itemContent = InkWell(
  242. borderRadius: BorderRadius.circular(12),
  243. onTap: () => context.go(item.path),
  244. child: AnimatedContainer(
  245. duration: const Duration(milliseconds: 180),
  246. curve: Curves.easeOut,
  247. margin: const EdgeInsets.symmetric(vertical: 4),
  248. padding: EdgeInsets.symmetric(
  249. horizontal: _collapsed ? 12 : 20,
  250. vertical: 13,
  251. ),
  252. decoration: BoxDecoration(
  253. color: isActive
  254. ? AppColors.primary.withOpacity(0.12)
  255. : Colors.transparent,
  256. borderRadius: BorderRadius.circular(12),
  257. border: Border.all(
  258. color: isActive
  259. ? AppColors.primary.withOpacity(0.4)
  260. : Colors.transparent,
  261. width: 1,
  262. ),
  263. ),
  264. child: Row(
  265. children: [
  266. AnimatedContainer(
  267. duration: const Duration(milliseconds: 240),
  268. curve: Curves.easeOut,
  269. width: 28,
  270. height: 28,
  271. decoration: BoxDecoration(
  272. color: isActive
  273. ? AppColors.primary.withOpacity(0.12)
  274. : AppColors.backgroundLight,
  275. borderRadius: BorderRadius.circular(8),
  276. ),
  277. child: Icon(
  278. item.icon,
  279. size: 16,
  280. color: isActive ? AppColors.primary : AppColors.textSecondary,
  281. ),
  282. ),
  283. if (!_collapsed) ...[
  284. const SizedBox(width: 14),
  285. Expanded(
  286. child: Row(
  287. children: [
  288. Expanded(
  289. child: Text(
  290. item.label,
  291. style: TextStyle(
  292. fontSize: 15,
  293. fontWeight:
  294. isActive ? FontWeight.w600 : FontWeight.w500,
  295. color: isActive
  296. ? AppColors.primary
  297. : AppColors.textSecondary,
  298. ),
  299. ),
  300. ),
  301. if (item.badge != null)
  302. Container(
  303. padding: const EdgeInsets.symmetric(
  304. horizontal: 8,
  305. vertical: 2,
  306. ),
  307. decoration: BoxDecoration(
  308. color: AppColors.primary,
  309. borderRadius: BorderRadius.circular(12),
  310. ),
  311. child: Text(
  312. item.badge!,
  313. style: const TextStyle(
  314. fontSize: 10,
  315. color: Colors.white,
  316. fontWeight: FontWeight.w600,
  317. letterSpacing: 0.4,
  318. ),
  319. ),
  320. ),
  321. ],
  322. ),
  323. ),
  324. ],
  325. ],
  326. ),
  327. ),
  328. );
  329. if (_collapsed) {
  330. return Tooltip(
  331. message: item.label,
  332. preferBelow: false,
  333. child: itemContent,
  334. );
  335. }
  336. return itemContent;
  337. }
  338. Widget _buildInsightCard() {
  339. return Container(
  340. padding: const EdgeInsets.all(16),
  341. decoration: BoxDecoration(
  342. borderRadius: BorderRadius.circular(16),
  343. gradient: const LinearGradient(
  344. colors: [Color(0xFFEDF4FF), Color(0xFFFFFFFF)],
  345. begin: Alignment.topLeft,
  346. end: Alignment.bottomRight,
  347. ),
  348. border: Border.all(color: AppColors.primary.withOpacity(0.08)),
  349. ),
  350. child: Column(
  351. crossAxisAlignment: CrossAxisAlignment.start,
  352. children: [
  353. Row(
  354. children: [
  355. Container(
  356. padding: const EdgeInsets.all(10),
  357. decoration: BoxDecoration(
  358. color: Colors.white,
  359. borderRadius: BorderRadius.circular(12),
  360. border:
  361. Border.all(color: AppColors.primary.withOpacity(0.12)),
  362. ),
  363. child: Icon(
  364. LucideIcons.activity,
  365. size: 18,
  366. color: AppColors.primary,
  367. ),
  368. ),
  369. const SizedBox(width: 12),
  370. const Expanded(
  371. child: Text(
  372. '今日处理 58 份文档',
  373. style: TextStyle(
  374. fontSize: 13,
  375. fontWeight: FontWeight.w600,
  376. color: AppColors.textPrimary,
  377. ),
  378. ),
  379. ),
  380. ],
  381. ),
  382. const SizedBox(height: 12),
  383. Row(
  384. children: [
  385. Expanded(
  386. child: Column(
  387. crossAxisAlignment: CrossAxisAlignment.start,
  388. children: const [
  389. Text(
  390. '成功率',
  391. style: TextStyle(
  392. fontSize: 11,
  393. color: AppColors.textSecondary,
  394. ),
  395. ),
  396. SizedBox(height: 4),
  397. Text(
  398. '98.4%',
  399. style: TextStyle(
  400. fontSize: 15,
  401. fontWeight: FontWeight.bold,
  402. color: AppColors.primary,
  403. ),
  404. ),
  405. ],
  406. ),
  407. ),
  408. Container(
  409. width: 1,
  410. height: 32,
  411. color: AppColors.borderLight,
  412. ),
  413. Expanded(
  414. child: Column(
  415. crossAxisAlignment: CrossAxisAlignment.start,
  416. children: const [
  417. Text(
  418. '平均耗时',
  419. style: TextStyle(
  420. fontSize: 11,
  421. color: AppColors.textSecondary,
  422. ),
  423. ),
  424. SizedBox(height: 4),
  425. Text(
  426. '13.6s',
  427. style: TextStyle(
  428. fontSize: 15,
  429. fontWeight: FontWeight.bold,
  430. color: AppColors.textPrimary,
  431. ),
  432. ),
  433. ],
  434. ),
  435. ),
  436. ],
  437. ),
  438. ],
  439. ),
  440. );
  441. }
  442. Widget _buildFooter() {
  443. return Container(
  444. padding: EdgeInsets.symmetric(
  445. horizontal: _collapsed ? 10 : 18,
  446. vertical: 20,
  447. ),
  448. decoration: BoxDecoration(
  449. border: Border(
  450. top: BorderSide(color: AppColors.borderLight.withOpacity(0.6)),
  451. ),
  452. ),
  453. child: Column(
  454. crossAxisAlignment:
  455. _collapsed ? CrossAxisAlignment.center : CrossAxisAlignment.start,
  456. children: [
  457. if (!_collapsed)
  458. Container(
  459. padding: const EdgeInsets.all(14),
  460. decoration: BoxDecoration(
  461. color: AppColors.primary.withOpacity(0.08),
  462. borderRadius: BorderRadius.circular(14),
  463. ),
  464. child: Column(
  465. crossAxisAlignment: CrossAxisAlignment.start,
  466. children: [
  467. const Text(
  468. '高级能力体验',
  469. style: TextStyle(
  470. fontSize: 13,
  471. fontWeight: FontWeight.w600,
  472. color: AppColors.textPrimary,
  473. ),
  474. ),
  475. const SizedBox(height: 6),
  476. Text(
  477. '解锁批量导出、自动化流程、团队协作等高级特性',
  478. style: TextStyle(
  479. fontSize: 11,
  480. color: AppColors.textSecondary.withOpacity(0.8),
  481. height: 1.4,
  482. ),
  483. ),
  484. const SizedBox(height: 12),
  485. AppButton(
  486. text: '立即升级',
  487. type: ButtonType.primary,
  488. size: ButtonSize.small,
  489. fullWidth: true,
  490. onPressed: () {},
  491. ),
  492. ],
  493. ),
  494. ),
  495. if (!_collapsed) const SizedBox(height: 16),
  496. InkWell(
  497. borderRadius: BorderRadius.circular(12),
  498. onTap: () {},
  499. child: Padding(
  500. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10),
  501. child: Row(
  502. children: [
  503. const CircleAvatar(
  504. radius: 16,
  505. backgroundColor: AppColors.backgroundLight,
  506. child: Icon(
  507. LucideIcons.user_round,
  508. size: 16,
  509. color: AppColors.textSecondary,
  510. ),
  511. ),
  512. if (!_collapsed) ...[
  513. const SizedBox(width: 10),
  514. Expanded(
  515. child: Column(
  516. crossAxisAlignment: CrossAxisAlignment.start,
  517. children: const [
  518. Text(
  519. 'Han Wen',
  520. style: TextStyle(
  521. fontSize: 13,
  522. fontWeight: FontWeight.w600,
  523. color: AppColors.textPrimary,
  524. ),
  525. ),
  526. SizedBox(height: 2),
  527. Text(
  528. '产品体验账号',
  529. style: TextStyle(
  530. fontSize: 11,
  531. color: AppColors.textSecondary,
  532. ),
  533. ),
  534. ],
  535. ),
  536. ),
  537. const Icon(
  538. LucideIcons.log_out,
  539. size: 16,
  540. color: AppColors.textSecondary,
  541. ),
  542. ],
  543. ],
  544. ),
  545. ),
  546. ),
  547. ],
  548. ),
  549. );
  550. }
  551. }
  552. class _SidebarItem {
  553. final IconData icon;
  554. final String label;
  555. final String path;
  556. final String? badge;
  557. _SidebarItem({
  558. required this.icon,
  559. required this.label,
  560. required this.path,
  561. this.badge,
  562. });
  563. }
  564. class _SidebarSection {
  565. final String title;
  566. final List<_SidebarItem> items;
  567. _SidebarSection({required this.title, required this.items});
  568. }