app_skeletons.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. import 'package:flutter/material.dart';
  2. import 'package:skeletonizer/skeletonizer.dart';
  3. import '../../core/theme/app_colors_extension.dart';
  4. /// 骨架占位卡片容器(列表加载态)
  5. class SkeletonLoadingList extends StatelessWidget {
  6. final int cardCount;
  7. final Widget Function() cardBuilder;
  8. final EdgeInsetsGeometry padding;
  9. const SkeletonLoadingList({
  10. super.key,
  11. this.cardCount = 5,
  12. this.cardBuilder = _defaultBuilder,
  13. this.padding = const EdgeInsets.fromLTRB(16, 16, 16, 24),
  14. });
  15. static Widget _defaultBuilder() => const SkeletonListCard();
  16. @override
  17. Widget build(BuildContext context) {
  18. return SingleChildScrollView(
  19. padding: padding,
  20. child: Skeletonizer(
  21. enabled: true,
  22. child: Column(
  23. children: List.generate(
  24. cardCount,
  25. (_) => Padding(
  26. padding: const EdgeInsets.only(bottom: 16),
  27. child: cardBuilder(),
  28. ),
  29. ),
  30. ),
  31. ),
  32. );
  33. }
  34. }
  35. /// 匹配 ListCard 的骨架占位
  36. class SkeletonListCard extends StatelessWidget {
  37. const SkeletonListCard({super.key});
  38. @override
  39. Widget build(BuildContext context) {
  40. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  41. return Skeletonizer(
  42. enabled: true,
  43. child: Container(
  44. padding: const EdgeInsets.all(12),
  45. decoration: BoxDecoration(
  46. color: colors.bgCard,
  47. borderRadius: BorderRadius.circular(8),
  48. ),
  49. child: Column(
  50. crossAxisAlignment: CrossAxisAlignment.start,
  51. children: [
  52. Row(
  53. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  54. children: [Bone.text(width: 120), Bone.text(width: 80)],
  55. ),
  56. SizedBox(height: 6),
  57. Bone.text(width: 140),
  58. SizedBox(height: 6),
  59. Bone.text(),
  60. SizedBox(height: 8),
  61. Row(
  62. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  63. children: [
  64. Bone.text(width: 100),
  65. Bone(
  66. width: 48,
  67. height: 20,
  68. borderRadius: BorderRadius.circular(4),
  69. ),
  70. ],
  71. ),
  72. ],
  73. ),
  74. ),
  75. );
  76. }
  77. }
  78. /// 匹配车辆列表卡片的骨架占位
  79. class SkeletonVehicleCard extends StatelessWidget {
  80. const SkeletonVehicleCard({super.key});
  81. @override
  82. Widget build(BuildContext context) {
  83. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  84. return Skeletonizer(
  85. enabled: true,
  86. child: Container(
  87. padding: const EdgeInsets.all(12),
  88. decoration: BoxDecoration(
  89. color: colors.bgCard,
  90. borderRadius: BorderRadius.circular(8),
  91. ),
  92. child: Column(
  93. crossAxisAlignment: CrossAxisAlignment.start,
  94. children: [
  95. Row(
  96. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  97. children: [
  98. Bone.text(width: 100),
  99. Bone(
  100. width: 48,
  101. height: 20,
  102. borderRadius: BorderRadius.circular(4),
  103. ),
  104. ],
  105. ),
  106. SizedBox(height: 6),
  107. Row(
  108. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  109. children: [
  110. Bone.text(width: 140),
  111. Bone(
  112. width: 40,
  113. height: 16,
  114. borderRadius: BorderRadius.circular(4),
  115. ),
  116. ],
  117. ),
  118. SizedBox(height: 6),
  119. Row(
  120. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  121. children: [
  122. Expanded(child: Bone.text()),
  123. const SizedBox(width: 8),
  124. Bone.text(width: 120),
  125. ],
  126. ),
  127. ],
  128. ),
  129. ),
  130. );
  131. }
  132. }
  133. /// 匹配外勤日志卡片的骨架占位
  134. class SkeletonOutingLogCard extends StatelessWidget {
  135. const SkeletonOutingLogCard({super.key});
  136. @override
  137. Widget build(BuildContext context) {
  138. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  139. return Skeletonizer(
  140. enabled: true,
  141. child: Container(
  142. padding: const EdgeInsets.all(12),
  143. decoration: BoxDecoration(
  144. color: colors.bgCard,
  145. borderRadius: BorderRadius.circular(8),
  146. ),
  147. child: Column(
  148. crossAxisAlignment: CrossAxisAlignment.start,
  149. children: [
  150. Bone.text(width: 100),
  151. SizedBox(height: 4),
  152. Row(
  153. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  154. children: [
  155. Expanded(flex: 3, child: Bone.text()),
  156. const SizedBox(width: 8),
  157. Bone(
  158. width: 48,
  159. height: 20,
  160. borderRadius: BorderRadius.circular(4),
  161. ),
  162. ],
  163. ),
  164. SizedBox(height: 4),
  165. Bone.text(),
  166. SizedBox(height: 4),
  167. Row(
  168. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  169. children: [
  170. Expanded(flex: 2, child: Bone.text()),
  171. const SizedBox(width: 8),
  172. Bone.text(width: 80),
  173. ],
  174. ),
  175. ],
  176. ),
  177. ),
  178. );
  179. }
  180. }
  181. /// 匹配公告卡片的骨架占位
  182. class SkeletonAnnouncementCard extends StatelessWidget {
  183. const SkeletonAnnouncementCard({super.key});
  184. @override
  185. Widget build(BuildContext context) {
  186. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  187. return Skeletonizer(
  188. enabled: true,
  189. child: Container(
  190. padding: const EdgeInsets.all(12),
  191. decoration: BoxDecoration(
  192. color: colors.bgCard,
  193. borderRadius: BorderRadius.circular(8),
  194. ),
  195. child: Column(
  196. crossAxisAlignment: CrossAxisAlignment.start,
  197. children: [
  198. Bone.text(),
  199. SizedBox(height: 8),
  200. Row(
  201. children: [
  202. Bone(
  203. width: 56,
  204. height: 20,
  205. borderRadius: BorderRadius.circular(4),
  206. ),
  207. const SizedBox(width: 8),
  208. Bone.text(width: 60),
  209. const Spacer(),
  210. Bone.text(width: 120),
  211. ],
  212. ),
  213. ],
  214. ),
  215. ),
  216. );
  217. }
  218. }
  219. /// 匹配费用申请导入列表卡片的骨架占位
  220. class SkeletonImportCard extends StatelessWidget {
  221. const SkeletonImportCard({super.key});
  222. @override
  223. Widget build(BuildContext context) {
  224. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  225. return Skeletonizer(
  226. enabled: true,
  227. child: Container(
  228. padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
  229. decoration: BoxDecoration(
  230. color: colors.bgCard,
  231. borderRadius: BorderRadius.circular(12),
  232. ),
  233. child: Column(
  234. crossAxisAlignment: CrossAxisAlignment.start,
  235. children: [
  236. Row(
  237. children: [
  238. Bone.square(size: 22, borderRadius: BorderRadius.circular(4)),
  239. const SizedBox(width: 8),
  240. Expanded(flex: 3, child: Bone.text()),
  241. const Spacer(),
  242. Bone.text(width: 80),
  243. ],
  244. ),
  245. SizedBox(height: 8),
  246. Row(children: [const SizedBox(width: 30), Bone.text(width: 140)]),
  247. SizedBox(height: 8),
  248. Row(
  249. children: [
  250. Bone.square(size: 18, borderRadius: BorderRadius.circular(4)),
  251. const SizedBox(width: 4),
  252. Bone.text(width: 24),
  253. const SizedBox(width: 8),
  254. Expanded(flex: 3, child: Bone.text()),
  255. const Spacer(),
  256. Bone.text(width: 64),
  257. ],
  258. ),
  259. SizedBox(height: 8),
  260. Row(
  261. children: [
  262. const SizedBox(width: 54),
  263. Expanded(flex: 2, child: Bone.text()),
  264. ],
  265. ),
  266. ],
  267. ),
  268. ),
  269. );
  270. }
  271. }
  272. /// 匹配消息卡片的骨架占位
  273. class SkeletonMessageCard extends StatelessWidget {
  274. const SkeletonMessageCard({super.key});
  275. @override
  276. Widget build(BuildContext context) {
  277. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  278. return Skeletonizer(
  279. enabled: true,
  280. child: Container(
  281. height: 88,
  282. padding: const EdgeInsets.all(12),
  283. decoration: BoxDecoration(
  284. color: colors.bgCard,
  285. borderRadius: BorderRadius.circular(8),
  286. ),
  287. child: Row(
  288. children: [
  289. Bone.circle(size: 40),
  290. SizedBox(width: 12),
  291. Expanded(
  292. child: Column(
  293. crossAxisAlignment: CrossAxisAlignment.start,
  294. children: [
  295. Row(
  296. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  297. children: [
  298. Expanded(flex: 3, child: Bone.text()),
  299. const Spacer(),
  300. Bone.text(width: 80),
  301. ],
  302. ),
  303. SizedBox(height: 2),
  304. Bone.text(width: 80),
  305. SizedBox(height: 2),
  306. Expanded(flex: 2, child: Bone.text()),
  307. ],
  308. ),
  309. ),
  310. SizedBox(width: 12),
  311. Bone(width: 8, height: 8, borderRadius: BorderRadius.circular(4)),
  312. ],
  313. ),
  314. ),
  315. );
  316. }
  317. }
  318. /// 详情页骨架(ExpenseDetailPage / ExpenseApplyDetailPage)
  319. /// 匹配 FormSection 卡片布局:基本信息 + 明细 + 附件
  320. class SkeletonDetailPage extends StatelessWidget {
  321. const SkeletonDetailPage({super.key});
  322. @override
  323. Widget build(BuildContext context) {
  324. return SingleChildScrollView(
  325. padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
  326. child: Skeletonizer(
  327. enabled: true,
  328. child: Column(
  329. children: [
  330. _FormCard(rows: 7),
  331. const SizedBox(height: 16),
  332. _FormCard(rows: 2),
  333. const SizedBox(height: 16),
  334. _FormCard(rows: 3),
  335. const SizedBox(height: 24),
  336. const Center(child: Bone(width: 120, height: 14)),
  337. ],
  338. ),
  339. ),
  340. );
  341. }
  342. }
  343. /// 表单页骨架(ExpenseApplyCreatePage / ExpenseCreatePage)
  344. /// 匹配 FormSection + 输入框 + 底部操作栏布局
  345. class SkeletonFormPage extends StatelessWidget {
  346. final bool showImportLink;
  347. const SkeletonFormPage({super.key, this.showImportLink = false});
  348. @override
  349. Widget build(BuildContext context) {
  350. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  351. final bottomPadding = MediaQuery.of(context).padding.bottom;
  352. return Stack(
  353. children: [
  354. SingleChildScrollView(
  355. padding: EdgeInsets.fromLTRB(16, 16, 16, 80 + bottomPadding),
  356. child: Skeletonizer(
  357. enabled: true,
  358. child: Column(
  359. children: [
  360. if (showImportLink) ...[
  361. // 导入申请单按钮骨架
  362. Container(
  363. padding: const EdgeInsets.symmetric(
  364. horizontal: 12,
  365. vertical: 10,
  366. ),
  367. decoration: BoxDecoration(
  368. color: colors.bgCard,
  369. borderRadius: BorderRadius.circular(12),
  370. ),
  371. child: Row(
  372. children: [
  373. Bone.square(
  374. size: 20,
  375. borderRadius: BorderRadius.circular(4),
  376. ),
  377. const SizedBox(width: 8),
  378. Bone(width: 160, height: 14),
  379. ],
  380. ),
  381. ),
  382. const SizedBox(height: 16),
  383. ],
  384. _FormCard(rows: 5),
  385. const SizedBox(height: 16),
  386. _FormCard(rows: 2),
  387. const SizedBox(height: 16),
  388. _FormCard(rows: 3),
  389. const SizedBox(height: 24),
  390. const Center(child: Bone(width: 120, height: 14)),
  391. ],
  392. ),
  393. ),
  394. ),
  395. // 底部操作栏骨架,贴底并适配安全区
  396. Positioned(
  397. left: 0,
  398. right: 0,
  399. bottom: 0,
  400. child: Skeletonizer(
  401. enabled: true,
  402. child: Container(
  403. padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + bottomPadding),
  404. decoration: BoxDecoration(
  405. color: colors.bgCard,
  406. border: Border(top: BorderSide(color: colors.border)),
  407. ),
  408. child: Row(
  409. children: [
  410. Expanded(
  411. child: Bone(
  412. height: 44,
  413. borderRadius: BorderRadius.circular(8),
  414. ),
  415. ),
  416. const SizedBox(width: 12),
  417. Expanded(
  418. child: Bone(
  419. height: 44,
  420. borderRadius: BorderRadius.circular(8),
  421. ),
  422. ),
  423. ],
  424. ),
  425. ),
  426. ),
  427. ),
  428. ],
  429. );
  430. }
  431. }
  432. class _FormCard extends StatelessWidget {
  433. final int rows;
  434. const _FormCard({required this.rows});
  435. @override
  436. Widget build(BuildContext context) {
  437. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  438. return Container(
  439. padding: const EdgeInsets.all(16),
  440. decoration: BoxDecoration(
  441. color: colors.bgCard,
  442. borderRadius: BorderRadius.circular(12),
  443. ),
  444. child: Column(
  445. crossAxisAlignment: CrossAxisAlignment.start,
  446. children: [
  447. Row(
  448. children: [
  449. Bone.square(size: 20, borderRadius: BorderRadius.circular(4)),
  450. const SizedBox(width: 8),
  451. Bone(width: 80, height: 16),
  452. const Spacer(),
  453. Bone(width: 40, height: 16),
  454. ],
  455. ),
  456. const SizedBox(height: 20),
  457. ...List.generate(
  458. rows,
  459. (i) => Padding(
  460. padding: EdgeInsets.only(bottom: i < rows - 1 ? 16 : 0),
  461. child: Row(
  462. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  463. children: [
  464. Bone(width: 64, height: 14),
  465. Bone(width: 140 + (i % 3) * 20, height: 14),
  466. ],
  467. ),
  468. ),
  469. ),
  470. ],
  471. ),
  472. );
  473. }
  474. }
  475. // ═══ 报表页骨架 — 各页面按实际布局独立定义 ═══
  476. /// 事前申请明细报表骨架(ExpenseApplyDetailReportPage)
  477. /// 布局:筛选栏 + 统计卡片(1行2个) + 折线图(单线) + 明细列表
  478. class SkeletonExpenseApplyDetailReportPage extends StatelessWidget {
  479. const SkeletonExpenseApplyDetailReportPage({super.key});
  480. @override
  481. Widget build(BuildContext context) {
  482. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  483. return Column(
  484. children: [
  485. _dateFilterChip(colors),
  486. Expanded(
  487. child: SingleChildScrollView(
  488. child: Skeletonizer(
  489. enabled: true,
  490. child: Column(
  491. children: [
  492. // 统计卡片 — 1行2列
  493. Padding(
  494. padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
  495. child: Row(
  496. children: [
  497. Expanded(
  498. child: Bone(
  499. height: 80,
  500. borderRadius: BorderRadius.circular(8),
  501. ),
  502. ),
  503. const SizedBox(width: 8),
  504. Expanded(
  505. child: Bone(
  506. height: 80,
  507. borderRadius: BorderRadius.circular(8),
  508. ),
  509. ),
  510. ],
  511. ),
  512. ),
  513. const SizedBox(height: 16),
  514. // 折线图 — 标题 + 1个图例 + 图表区
  515. Padding(
  516. padding: const EdgeInsets.symmetric(horizontal: 12),
  517. child: Container(
  518. padding: const EdgeInsets.all(16),
  519. decoration: BoxDecoration(
  520. color: colors.bgCard,
  521. borderRadius: BorderRadius.circular(8),
  522. ),
  523. child: Column(
  524. crossAxisAlignment: CrossAxisAlignment.start,
  525. children: [
  526. Bone(width: 120, height: 16),
  527. const SizedBox(height: 8),
  528. Row(
  529. children: [
  530. Bone.square(
  531. size: 8,
  532. borderRadius: BorderRadius.circular(4),
  533. ),
  534. const SizedBox(width: 4),
  535. Bone(width: 60, height: 12),
  536. ],
  537. ),
  538. const SizedBox(height: 16),
  539. Row(
  540. children: [
  541. Expanded(
  542. child: Bone(
  543. height: 200,
  544. borderRadius: BorderRadius.circular(8),
  545. ),
  546. ),
  547. ],
  548. ),
  549. ],
  550. ),
  551. ),
  552. ),
  553. const SizedBox(height: 16),
  554. // 明细列表
  555. _detailListSkeleton(colors),
  556. const SizedBox(height: 80),
  557. ],
  558. ),
  559. ),
  560. ),
  561. ),
  562. ],
  563. );
  564. }
  565. }
  566. /// 费用报销明细报表骨架(ExpenseDetailReportPage)
  567. /// 布局:筛选栏 + 统计卡片(2+1) + 双折线图 + 柱形图 + 明细列表
  568. class SkeletonExpenseDetailReportPage extends StatelessWidget {
  569. const SkeletonExpenseDetailReportPage({super.key});
  570. @override
  571. Widget build(BuildContext context) {
  572. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  573. return Column(
  574. children: [
  575. _dateFilterChip(colors),
  576. Expanded(
  577. child: SingleChildScrollView(
  578. child: Skeletonizer(
  579. enabled: true,
  580. child: Column(
  581. children: [
  582. // 统计卡片 — 1行2列 + 1个全宽
  583. Padding(
  584. padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
  585. child: Row(
  586. children: [
  587. Expanded(
  588. child: Bone(
  589. height: 80,
  590. borderRadius: BorderRadius.circular(8),
  591. ),
  592. ),
  593. const SizedBox(width: 8),
  594. Expanded(
  595. child: Bone(
  596. height: 80,
  597. borderRadius: BorderRadius.circular(8),
  598. ),
  599. ),
  600. ],
  601. ),
  602. ),
  603. const SizedBox(height: 8),
  604. Padding(
  605. padding: const EdgeInsets.symmetric(horizontal: 12),
  606. child: Row(
  607. children: [
  608. Expanded(
  609. child: Bone(
  610. height: 80,
  611. borderRadius: BorderRadius.circular(8),
  612. ),
  613. ),
  614. ],
  615. ),
  616. ),
  617. const SizedBox(height: 16),
  618. // 双折线图 — 标题 + 2个图例 + 图表区
  619. Padding(
  620. padding: const EdgeInsets.symmetric(horizontal: 12),
  621. child: Container(
  622. padding: const EdgeInsets.all(16),
  623. decoration: BoxDecoration(
  624. color: colors.bgCard,
  625. borderRadius: BorderRadius.circular(8),
  626. ),
  627. child: Column(
  628. crossAxisAlignment: CrossAxisAlignment.start,
  629. children: [
  630. Bone(width: 120, height: 16),
  631. const SizedBox(height: 8),
  632. Row(
  633. children: [
  634. Bone.square(
  635. size: 8,
  636. borderRadius: BorderRadius.circular(4),
  637. ),
  638. const SizedBox(width: 4),
  639. Bone(width: 60, height: 12),
  640. const SizedBox(width: 16),
  641. Bone.square(
  642. size: 8,
  643. borderRadius: BorderRadius.circular(4),
  644. ),
  645. const SizedBox(width: 4),
  646. Bone(width: 60, height: 12),
  647. ],
  648. ),
  649. const SizedBox(height: 16),
  650. Row(
  651. children: [
  652. Expanded(
  653. child: Bone(
  654. height: 200,
  655. borderRadius: BorderRadius.circular(8),
  656. ),
  657. ),
  658. ],
  659. ),
  660. ],
  661. ),
  662. ),
  663. ),
  664. const SizedBox(height: 16),
  665. // 柱形图 — 标题 + 图表区
  666. Padding(
  667. padding: const EdgeInsets.symmetric(horizontal: 12),
  668. child: Container(
  669. padding: const EdgeInsets.all(16),
  670. decoration: BoxDecoration(
  671. color: colors.bgCard,
  672. borderRadius: BorderRadius.circular(8),
  673. ),
  674. child: Column(
  675. crossAxisAlignment: CrossAxisAlignment.start,
  676. children: [
  677. Bone(width: 160, height: 16),
  678. const SizedBox(height: 16),
  679. Row(
  680. children: [
  681. Expanded(
  682. child: Bone(
  683. height: 220,
  684. borderRadius: BorderRadius.circular(8),
  685. ),
  686. ),
  687. ],
  688. ),
  689. ],
  690. ),
  691. ),
  692. ),
  693. const SizedBox(height: 12),
  694. // 明细列表
  695. _detailListSkeleton(colors),
  696. const SizedBox(height: 80),
  697. ],
  698. ),
  699. ),
  700. ),
  701. ),
  702. ],
  703. );
  704. }
  705. }
  706. // ── 日期筛选 chip 占位 ──
  707. Widget _dateFilterChip(AppColorsExtension colors) => Container(
  708. padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
  709. color: colors.bgCard,
  710. child: Container(
  711. height: 40,
  712. decoration: BoxDecoration(
  713. color: colors.bgSecondaryContainer,
  714. borderRadius: BorderRadius.circular(20),
  715. ),
  716. ),
  717. );
  718. // ── 明细列表骨架 ──
  719. Widget _detailListSkeleton(AppColorsExtension colors) => Padding(
  720. padding: const EdgeInsets.symmetric(horizontal: 12),
  721. child: Container(
  722. width: double.infinity,
  723. decoration: BoxDecoration(
  724. color: colors.bgCard,
  725. borderRadius: BorderRadius.circular(8),
  726. ),
  727. child: Column(
  728. crossAxisAlignment: CrossAxisAlignment.start,
  729. children: [
  730. Padding(
  731. padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
  732. child: Row(
  733. children: [
  734. Bone(width: 80, height: 16),
  735. const Spacer(),
  736. Bone(width: 100, height: 14),
  737. ],
  738. ),
  739. ),
  740. const Divider(height: 1),
  741. ...List.generate(4, (i) {
  742. return Column(
  743. children: [
  744. Padding(
  745. padding: const EdgeInsets.symmetric(
  746. horizontal: 16,
  747. vertical: 12,
  748. ),
  749. child: Column(
  750. crossAxisAlignment: CrossAxisAlignment.start,
  751. children: [
  752. Row(
  753. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  754. children: [
  755. Bone(width: 120 + (i % 3) * 40, height: 14),
  756. Bone(width: 80, height: 16),
  757. ],
  758. ),
  759. const SizedBox(height: 6),
  760. Row(
  761. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  762. children: [
  763. Bone(width: 90, height: 12),
  764. Bone(width: 80, height: 16),
  765. ],
  766. ),
  767. if (i == 0) ...[
  768. const SizedBox(height: 6),
  769. Bone(width: 200, height: 14),
  770. ],
  771. ],
  772. ),
  773. ),
  774. if (i < 3) const Divider(height: 1),
  775. ],
  776. );
  777. }),
  778. ],
  779. ),
  780. ),
  781. );