skeleton_list_card.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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. /// │ applicant(short) │ R2: Text(13)
  10. /// │ description │ R3: Text(14)
  11. /// │ date ← spaceBetween → tag │ R4: Flexible(12) + statusTag
  12. /// └────────────────────────────┘
  13. class SkeletonListCard extends StatelessWidget {
  14. const SkeletonListCard({super.key});
  15. @override
  16. Widget build(BuildContext context) {
  17. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  18. return Container(
  19. padding: const EdgeInsets.all(12),
  20. decoration: BoxDecoration(
  21. color: colors.bgCard,
  22. borderRadius: BorderRadius.circular(8),
  23. ),
  24. child: Column(children: [
  25. TDSkeleton.fromRowCol(
  26. animation: TDSkeletonAnimation.flashed,
  27. rowCol: TDSkeletonRowCol(
  28. objects: const [
  29. [
  30. TDSkeletonRowColObj.text(width: 120, height: 17, flex: 0),
  31. TDSkeletonRowColObj.spacer(flex: 1),
  32. TDSkeletonRowColObj.text(width: 80, height: 20, flex: 0),
  33. ],
  34. [
  35. TDSkeletonRowColObj.text(width: 140, height: 14, flex: 0),
  36. ],
  37. [
  38. TDSkeletonRowColObj.text(height: 17),
  39. ],
  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(rowSpacing: (_) => 6),
  47. ),
  48. ),
  49. ]),
  50. );
  51. }
  52. }
  53. /// Skeleton 占位卡片,匹配车辆列表卡片布局(基于 TDSkeleton.fromRowCol):
  54. ///
  55. /// 真实卡片结构:
  56. /// ┌────────────────────────────┐
  57. /// │ 车牌号 ← spaceBetween → 徽章│ R1: Text(16/700) + statusTag
  58. /// │ 6px │
  59. /// │ 申请单号 ← spaceBetween → 标签│ R2: Text(12) + purposeTag
  60. /// │ 6px │
  61. /// │ 路线(ellipsis) ← → 时间 │ R3: Flexible(13) + Text(12)
  62. /// └────────────────────────────┘
  63. class SkeletonVehicleCard extends StatelessWidget {
  64. const SkeletonVehicleCard({super.key});
  65. @override
  66. Widget build(BuildContext context) {
  67. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  68. return Container(
  69. padding: const EdgeInsets.all(12),
  70. decoration: BoxDecoration(
  71. color: colors.bgCard,
  72. borderRadius: BorderRadius.circular(8),
  73. ),
  74. child: Column(children: [
  75. 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. }
  107. /// Skeleton 占位卡片,匹配外勤日志卡片布局(基于 TDSkeleton.fromRowCol):
  108. ///
  109. /// 真实卡片结构:
  110. /// ┌────────────────────────────┐
  111. /// │ visitNo │ R1: Text(11)
  112. /// │ 4px │
  113. /// │ customerName ← → statusTag │ R2: Flexible(15/700) + statusTag
  114. /// │ 4px │
  115. /// │ checkInAddress │ R3: Text(12)
  116. /// │ 4px │
  117. /// │ summary(ellipsis) ← → date │ R4: Flexible(12) + Text(11)
  118. /// └────────────────────────────┘
  119. class SkeletonOutingLogCard extends StatelessWidget {
  120. const SkeletonOutingLogCard({super.key});
  121. @override
  122. Widget build(BuildContext context) {
  123. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  124. return Container(
  125. padding: const EdgeInsets.all(12),
  126. decoration: BoxDecoration(
  127. color: colors.bgCard,
  128. borderRadius: BorderRadius.circular(8),
  129. ),
  130. child: Column(children: [
  131. TDSkeleton.fromRowCol(
  132. animation: TDSkeletonAnimation.flashed,
  133. rowCol: TDSkeletonRowCol(
  134. objects: const [
  135. // R1: visitNo
  136. [TDSkeletonRowColObj.text(width: 100, height: 13)],
  137. // R2: customerName + statusTag
  138. [
  139. TDSkeletonRowColObj.text(height: 18, flex: 3),
  140. TDSkeletonRowColObj.spacer(flex: 1),
  141. TDSkeletonRowColObj.rect(width: 48, height: 20, flex: 0),
  142. ],
  143. // R3: checkInAddress
  144. [TDSkeletonRowColObj.text(height: 14)],
  145. // R4: summary + date
  146. [
  147. TDSkeletonRowColObj.text(height: 14, flex: 2),
  148. TDSkeletonRowColObj.spacer(flex: 1),
  149. TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0),
  150. ],
  151. ],
  152. style: TDSkeletonRowColStyle(rowSpacing: (_) => 4),
  153. ),
  154. ),
  155. ]),
  156. );
  157. }
  158. }
  159. /// Skeleton 占位卡片,匹配公告卡片布局(基于 TDSkeleton.fromRowCol):
  160. ///
  161. /// 真实卡片结构:
  162. /// ┌────────────────────────────┐
  163. /// │ title │ R1: Text(15/600)
  164. /// │ 8px │
  165. /// │ typeTag publisher ← → date│ R2: tag(56) + SizedBox(8) + Text(12) + spaceBetween + Text(12)
  166. /// └────────────────────────────┘
  167. class SkeletonAnnouncementCard extends StatelessWidget {
  168. const SkeletonAnnouncementCard({super.key});
  169. @override
  170. Widget build(BuildContext context) {
  171. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  172. return Container(
  173. padding: const EdgeInsets.all(12),
  174. decoration: BoxDecoration(
  175. color: colors.bgCard,
  176. borderRadius: BorderRadius.circular(8),
  177. ),
  178. child: Column(children: [
  179. TDSkeleton.fromRowCol(
  180. animation: TDSkeletonAnimation.flashed,
  181. rowCol: TDSkeletonRowCol(
  182. objects: const [
  183. // R1: title (full width)
  184. [TDSkeletonRowColObj.text(height: 18)],
  185. // R2: typeTag + publisher + spacer(flex:1) + date
  186. [
  187. TDSkeletonRowColObj.rect(width: 56, height: 20, flex: 0),
  188. TDSkeletonRowColObj.spacer(width: 8),
  189. TDSkeletonRowColObj.text(width: 60, height: 14, flex: 0),
  190. TDSkeletonRowColObj.spacer(flex: 1),
  191. TDSkeletonRowColObj.text(width: 120, height: 14, flex: 0),
  192. ],
  193. ],
  194. style: TDSkeletonRowColStyle(rowSpacing: (_) => 8),
  195. ),
  196. ),
  197. ]),
  198. );
  199. }
  200. }
  201. /// Skeleton 占位卡片,匹配费用申请导入列表卡片布局:
  202. ///
  203. /// 真实卡片结构:
  204. /// ┌──────────────────────────────────────┐
  205. /// │ [□ 22] AE202606001 06-15 │ R1: rect(22) + text(wide) + text(narrow)
  206. /// │ 申请事由文字... │ R2: text(medium)
  207. /// │ ─────────────────────────────── │ Divider
  208. /// │ [□ 18] #1 类型/科目 ¥1,500 │ R3: rect(18) + text + text(wide) + amount
  209. /// │ 申请人/会计科目... │ R4: text(wide)
  210. /// └──────────────────────────────────────┘
  211. class SkeletonImportCard extends StatelessWidget {
  212. const SkeletonImportCard({super.key});
  213. @override
  214. Widget build(BuildContext context) {
  215. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  216. return Container(
  217. padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
  218. decoration: BoxDecoration(
  219. color: colors.bgCard,
  220. borderRadius: BorderRadius.circular(12),
  221. ),
  222. child: Column(children: [
  223. TDSkeleton.fromRowCol(
  224. animation: TDSkeletonAnimation.flashed,
  225. rowCol: TDSkeletonRowCol(
  226. objects: const [
  227. // R1: checkbox(22) + aeNo(wide text, 15) + date(narrow)
  228. [
  229. TDSkeletonRowColObj.rect(width: 22, height: 22, flex: 0),
  230. TDSkeletonRowColObj.spacer(width: 8),
  231. TDSkeletonRowColObj.text(height: 17, flex: 3),
  232. TDSkeletonRowColObj.spacer(flex: 1),
  233. TDSkeletonRowColObj.text(width: 80, height: 14, flex: 0),
  234. ],
  235. // R2: reason text (left-aligned with checkbox)
  236. [
  237. TDSkeletonRowColObj.spacer(width: 30),
  238. TDSkeletonRowColObj.text(width: 140, height: 14, flex: 0),
  239. ],
  240. // R3: detail row checkbox(18) + #itm + type text + amount
  241. [
  242. TDSkeletonRowColObj.rect(width: 18, height: 18, flex: 0),
  243. TDSkeletonRowColObj.spacer(width: 4),
  244. TDSkeletonRowColObj.text(width: 24, height: 14, flex: 0),
  245. TDSkeletonRowColObj.spacer(width: 8),
  246. TDSkeletonRowColObj.text(height: 15, flex: 3),
  247. TDSkeletonRowColObj.spacer(flex: 1),
  248. TDSkeletonRowColObj.text(width: 64, height: 17, flex: 0),
  249. ],
  250. // R6: detail sub-text
  251. [
  252. TDSkeletonRowColObj.spacer(width: 54),
  253. TDSkeletonRowColObj.text(height: 14, flex: 2),
  254. ],
  255. ],
  256. style: TDSkeletonRowColStyle(rowSpacing: (_) => 8),
  257. ),
  258. ),
  259. ]),
  260. );
  261. }
  262. }
  263. /// Skeleton 占位卡片,匹配 [MessageItem] 布局(基于 TDSkeleton.fromRowCol):
  264. ///
  265. /// 真实 MessageItem 结构:
  266. /// ┌──────────────────────────────────────────┐
  267. /// │ [● 40×40] 标题文字... MM-DD HH:mm │ [●] │
  268. /// │ 发送人 │ │
  269. /// │ 摘要内容... │ │
  270. /// └──────────────────────────────────────────┘
  271. class SkeletonMessageCard extends StatelessWidget {
  272. const SkeletonMessageCard({super.key});
  273. @override
  274. Widget build(BuildContext context) {
  275. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  276. return Container(
  277. height: 88,
  278. padding: const EdgeInsets.all(12),
  279. decoration: BoxDecoration(
  280. color: colors.bgCard,
  281. borderRadius: BorderRadius.circular(8),
  282. ),
  283. child: Row(
  284. children: [
  285. // 左侧圆形图标占位 40×40
  286. TDSkeleton.fromRowCol(
  287. animation: TDSkeletonAnimation.flashed,
  288. rowCol: TDSkeletonRowCol(
  289. objects: const [
  290. [TDSkeletonRowColObj.rect(width: 40, height: 40, flex: 0)],
  291. ],
  292. ),
  293. ),
  294. const SizedBox(width: 12),
  295. // 中间三行文字骨架
  296. Expanded(
  297. child: TDSkeleton.fromRowCol(
  298. animation: TDSkeletonAnimation.flashed,
  299. rowCol: TDSkeletonRowCol(
  300. objects: const [
  301. // R1: 标题(宽) + 时间(窄)
  302. [
  303. TDSkeletonRowColObj.text(height: 17, flex: 3),
  304. TDSkeletonRowColObj.spacer(flex: 1),
  305. TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0),
  306. ],
  307. // R2: 发送人
  308. [TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0)],
  309. // R3: 摘要
  310. [TDSkeletonRowColObj.text(height: 13, flex: 2)],
  311. ],
  312. style: TDSkeletonRowColStyle(rowSpacing: (_) => 2),
  313. ),
  314. ),
  315. ),
  316. const SizedBox(width: 12),
  317. // 右侧未读红点占位 8×8
  318. TDSkeleton.fromRowCol(
  319. animation: TDSkeletonAnimation.flashed,
  320. rowCol: TDSkeletonRowCol(
  321. objects: const [
  322. [TDSkeletonRowColObj.rect(width: 8, height: 8, flex: 0)],
  323. ],
  324. ),
  325. ),
  326. ],
  327. ),
  328. );
  329. }
  330. }
  331. /// 列表加载态:骨架卡片占位
  332. ///
  333. /// [cardCount] 骨架卡片数量,默认 5
  334. /// [cardBuilder] 骨架卡片构建器,默认 [SkeletonListCard]
  335. class SkeletonLoadingList extends StatelessWidget {
  336. final int cardCount;
  337. final Widget Function() cardBuilder;
  338. final EdgeInsetsGeometry padding;
  339. const SkeletonLoadingList({
  340. super.key,
  341. this.cardCount = 5,
  342. this.cardBuilder = _defaultBuilder,
  343. this.padding = const EdgeInsets.fromLTRB(16, 16, 16, 24),
  344. });
  345. static Widget _defaultBuilder() => const SkeletonListCard();
  346. @override
  347. Widget build(BuildContext context) {
  348. return SingleChildScrollView(
  349. padding: padding,
  350. child: Column(
  351. children: List.generate(
  352. cardCount,
  353. (_) => Padding(
  354. padding: const EdgeInsets.only(bottom: 16),
  355. child: cardBuilder(),
  356. ),
  357. ),
  358. ),
  359. );
  360. }
  361. }