skeleton_list_card.dart 14 KB

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