skeleton_list_card.dart 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import 'package:flutter/material.dart';
  2. import 'package:tdesign_flutter/tdesign_flutter.dart';
  3. import '../../core/theme/app_colors_extension.dart';
  4. /// Skeleton 占位卡片,匹配 [ListCard] 布局(基于 TDSkeleton.fromRowCol):
  5. ///
  6. /// 真实 ListCard 结构:
  7. /// ┌────────────────────────────┐
  8. /// │ cardNo ← spaceBetween → amount │ R1: Flexible(14/600) + Text(16/700)
  9. /// │ 8px │
  10. /// │ description │ R2: Text(14)
  11. /// │ 8px │
  12. /// │ date ← spaceBetween → tag │ R3: Flexible(12) + statusTag
  13. /// └────────────────────────────┘
  14. class SkeletonListCard extends StatelessWidget {
  15. const SkeletonListCard({super.key});
  16. @override
  17. Widget build(BuildContext context) {
  18. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  19. return Container(
  20. padding: const EdgeInsets.all(12),
  21. decoration: BoxDecoration(
  22. color: colors.bgCard,
  23. borderRadius: BorderRadius.circular(8),
  24. ),
  25. child: TDSkeleton.fromRowCol(
  26. animation: TDSkeletonAnimation.flashed,
  27. rowCol: TDSkeletonRowCol(
  28. objects: const [
  29. // R1: cardNo + amount(spacer 撑开两端)
  30. [
  31. TDSkeletonRowColObj.text(width: 120, height: 17, flex: 0),
  32. TDSkeletonRowColObj.spacer(flex: 1),
  33. TDSkeletonRowColObj.text(width: 80, height: 20, flex: 0),
  34. ],
  35. // R2: description(全宽)
  36. [
  37. TDSkeletonRowColObj.text(height: 17),
  38. ],
  39. // R3: date + statusTag(spacer 撑开两端)
  40. [
  41. TDSkeletonRowColObj.text(width: 100, height: 14, flex: 0),
  42. TDSkeletonRowColObj.spacer(flex: 1),
  43. TDSkeletonRowColObj.rect(width: 48, height: 20, flex: 0),
  44. ],
  45. ],
  46. style: TDSkeletonRowColStyle(
  47. rowSpacing: (_) => 8,
  48. ),
  49. ),
  50. ),
  51. );
  52. }
  53. }
  54. /// Skeleton 占位卡片,匹配车辆列表卡片布局(基于 TDSkeleton.fromRowCol):
  55. ///
  56. /// 真实卡片结构:
  57. /// ┌────────────────────────────┐
  58. /// │ 车牌号 ← spaceBetween → 徽章│ R1: Text(16/700) + statusTag
  59. /// │ 6px │
  60. /// │ 申请单号 ← spaceBetween → 标签│ R2: Text(12) + purposeTag
  61. /// │ 6px │
  62. /// │ 路线(ellipsis) ← → 时间 │ R3: Flexible(13) + Text(12)
  63. /// └────────────────────────────┘
  64. class SkeletonVehicleCard extends StatelessWidget {
  65. const SkeletonVehicleCard({super.key});
  66. @override
  67. Widget build(BuildContext context) {
  68. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  69. return Container(
  70. padding: const EdgeInsets.all(12),
  71. decoration: BoxDecoration(
  72. color: colors.bgCard,
  73. borderRadius: BorderRadius.circular(8),
  74. ),
  75. child: TDSkeleton.fromRowCol(
  76. animation: TDSkeletonAnimation.flashed,
  77. rowCol: TDSkeletonRowCol(
  78. objects: const [
  79. // R1: licensePlate + status badge
  80. [
  81. TDSkeletonRowColObj.text(width: 100, height: 20, flex: 0),
  82. TDSkeletonRowColObj.spacer(flex: 1),
  83. TDSkeletonRowColObj.rect(width: 48, height: 20, flex: 0),
  84. ],
  85. // R2: applicationNo + purpose tag
  86. [
  87. TDSkeletonRowColObj.text(width: 140, height: 14, flex: 0),
  88. TDSkeletonRowColObj.spacer(flex: 1),
  89. TDSkeletonRowColObj.rect(width: 40, height: 16, flex: 0),
  90. ],
  91. // R3: route + date
  92. [
  93. TDSkeletonRowColObj.text(height: 15, flex: 2),
  94. TDSkeletonRowColObj.spacer(flex: 1),
  95. TDSkeletonRowColObj.text(width: 120, height: 14, flex: 0),
  96. ],
  97. ],
  98. style: TDSkeletonRowColStyle(
  99. rowSpacing: (_) => 6,
  100. ),
  101. ),
  102. ),
  103. );
  104. }
  105. }
  106. /// Skeleton 占位卡片,匹配外勤日志卡片布局(基于 TDSkeleton.fromRowCol):
  107. ///
  108. /// 真实卡片结构:
  109. /// ┌────────────────────────────┐
  110. /// │ visitNo │ R1: Text(11)
  111. /// │ 4px │
  112. /// │ customerName ← → statusTag │ R2: Flexible(15/700) + statusTag
  113. /// │ 4px │
  114. /// │ checkInAddress │ R3: Text(12)
  115. /// │ 4px │
  116. /// │ summary(ellipsis) ← → date │ R4: Flexible(12) + Text(11)
  117. /// └────────────────────────────┘
  118. class SkeletonOutingLogCard extends StatelessWidget {
  119. const SkeletonOutingLogCard({super.key});
  120. @override
  121. Widget build(BuildContext context) {
  122. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  123. return Container(
  124. padding: const EdgeInsets.all(12),
  125. decoration: BoxDecoration(
  126. color: colors.bgCard,
  127. borderRadius: BorderRadius.circular(8),
  128. ),
  129. child: TDSkeleton.fromRowCol(
  130. animation: TDSkeletonAnimation.flashed,
  131. rowCol: TDSkeletonRowCol(
  132. objects: const [
  133. // R1: visitNo
  134. [TDSkeletonRowColObj.text(width: 100, height: 13)],
  135. // R2: customerName + statusTag
  136. [
  137. TDSkeletonRowColObj.text(height: 18, flex: 3),
  138. TDSkeletonRowColObj.spacer(flex: 1),
  139. TDSkeletonRowColObj.rect(width: 48, height: 20, flex: 0),
  140. ],
  141. // R3: checkInAddress
  142. [TDSkeletonRowColObj.text(height: 14)],
  143. // R4: summary + date
  144. [
  145. TDSkeletonRowColObj.text(height: 14, flex: 2),
  146. TDSkeletonRowColObj.spacer(flex: 1),
  147. TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0),
  148. ],
  149. ],
  150. style: TDSkeletonRowColStyle(rowSpacing: (_) => 4),
  151. ),
  152. ),
  153. );
  154. }
  155. }
  156. /// Skeleton 占位卡片,匹配公告卡片布局(基于 TDSkeleton.fromRowCol):
  157. ///
  158. /// 真实卡片结构:
  159. /// ┌────────────────────────────┐
  160. /// │ title │ R1: Text(15/600)
  161. /// │ 8px │
  162. /// │ typeTag publisher ← → date│ R2: tag(56) + SizedBox(8) + Text(12) + spaceBetween + Text(12)
  163. /// └────────────────────────────┘
  164. class SkeletonAnnouncementCard extends StatelessWidget {
  165. const SkeletonAnnouncementCard({super.key});
  166. @override
  167. Widget build(BuildContext context) {
  168. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  169. return Container(
  170. padding: const EdgeInsets.all(12),
  171. decoration: BoxDecoration(
  172. color: colors.bgCard,
  173. borderRadius: BorderRadius.circular(8),
  174. ),
  175. child: TDSkeleton.fromRowCol(
  176. animation: TDSkeletonAnimation.flashed,
  177. rowCol: TDSkeletonRowCol(
  178. objects: const [
  179. // R1: title (full width)
  180. [TDSkeletonRowColObj.text(height: 18)],
  181. // R2: typeTag + publisher + spacer(flex:1) + date
  182. [
  183. TDSkeletonRowColObj.rect(width: 56, height: 20, flex: 0),
  184. TDSkeletonRowColObj.spacer(width: 8),
  185. TDSkeletonRowColObj.text(width: 60, height: 14, flex: 0),
  186. TDSkeletonRowColObj.spacer(flex: 1),
  187. TDSkeletonRowColObj.text(width: 120, height: 14, flex: 0),
  188. ],
  189. ],
  190. style: TDSkeletonRowColStyle(rowSpacing: (_) => 8),
  191. ),
  192. ),
  193. );
  194. }
  195. }
  196. /// 列表加载态:骨架卡片占位
  197. ///
  198. /// [cardCount] 骨架卡片数量,默认 5
  199. /// [cardBuilder] 骨架卡片构建器,默认 [SkeletonListCard]
  200. class SkeletonLoadingList extends StatelessWidget {
  201. final int cardCount;
  202. final Widget Function() cardBuilder;
  203. const SkeletonLoadingList({
  204. super.key,
  205. this.cardCount = 5,
  206. this.cardBuilder = _defaultBuilder,
  207. });
  208. static Widget _defaultBuilder() => const SkeletonListCard();
  209. @override
  210. Widget build(BuildContext context) {
  211. return ListView(
  212. padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
  213. physics: const NeverScrollableScrollPhysics(),
  214. children: List.generate(
  215. cardCount,
  216. (_) => Padding(
  217. padding: const EdgeInsets.only(bottom: 16),
  218. child: cardBuilder(),
  219. ),
  220. ),
  221. );
  222. }
  223. }