import 'package:flutter/material.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/theme/app_colors_extension.dart'; /// Skeleton 占位卡片,匹配 [ListCard] 布局(基于 TDSkeleton.fromRowCol): /// /// 真实 ListCard 结构: /// ┌────────────────────────────┐ /// │ cardNo ← spaceBetween → amount │ R1: Flexible(14/600) + Text(16/700) /// │ 8px │ /// │ description │ R2: Text(14) /// │ 8px │ /// │ date ← spaceBetween → tag │ R3: Flexible(12) + statusTag /// └────────────────────────────┘ class SkeletonListCard extends StatelessWidget { const SkeletonListCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: TDSkeleton.fromRowCol( animation: TDSkeletonAnimation.flashed, rowCol: TDSkeletonRowCol( objects: const [ // R1: cardNo + amount(spacer 撑开两端) [ TDSkeletonRowColObj.text(width: 120, height: 17, flex: 0), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.text(width: 80, height: 20, flex: 0), ], // R2: description(全宽) [ TDSkeletonRowColObj.text(height: 17), ], // R3: date + statusTag(spacer 撑开两端) [ TDSkeletonRowColObj.text(width: 100, height: 14, flex: 0), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.rect(width: 48, height: 20, flex: 0), ], ], style: TDSkeletonRowColStyle( rowSpacing: (_) => 8, ), ), ), ); } } /// Skeleton 占位卡片,匹配车辆列表卡片布局(基于 TDSkeleton.fromRowCol): /// /// 真实卡片结构: /// ┌────────────────────────────┐ /// │ 车牌号 ← spaceBetween → 徽章│ R1: Text(16/700) + statusTag /// │ 6px │ /// │ 申请单号 ← spaceBetween → 标签│ R2: Text(12) + purposeTag /// │ 6px │ /// │ 路线(ellipsis) ← → 时间 │ R3: Flexible(13) + Text(12) /// └────────────────────────────┘ class SkeletonVehicleCard extends StatelessWidget { const SkeletonVehicleCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: TDSkeleton.fromRowCol( animation: TDSkeletonAnimation.flashed, rowCol: TDSkeletonRowCol( objects: const [ // R1: licensePlate + status badge [ TDSkeletonRowColObj.text(width: 100, height: 20, flex: 0), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.rect(width: 48, height: 20, flex: 0), ], // R2: applicationNo + purpose tag [ TDSkeletonRowColObj.text(width: 140, height: 14, flex: 0), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.rect(width: 40, height: 16, flex: 0), ], // R3: route + date [ TDSkeletonRowColObj.text(height: 15, flex: 2), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.text(width: 120, height: 14, flex: 0), ], ], style: TDSkeletonRowColStyle( rowSpacing: (_) => 6, ), ), ), ); } } /// Skeleton 占位卡片,匹配外勤日志卡片布局(基于 TDSkeleton.fromRowCol): /// /// 真实卡片结构: /// ┌────────────────────────────┐ /// │ visitNo │ R1: Text(11) /// │ 4px │ /// │ customerName ← → statusTag │ R2: Flexible(15/700) + statusTag /// │ 4px │ /// │ checkInAddress │ R3: Text(12) /// │ 4px │ /// │ summary(ellipsis) ← → date │ R4: Flexible(12) + Text(11) /// └────────────────────────────┘ class SkeletonOutingLogCard extends StatelessWidget { const SkeletonOutingLogCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: TDSkeleton.fromRowCol( animation: TDSkeletonAnimation.flashed, rowCol: TDSkeletonRowCol( objects: const [ // R1: visitNo [TDSkeletonRowColObj.text(width: 100, height: 13)], // R2: customerName + statusTag [ TDSkeletonRowColObj.text(height: 18, flex: 3), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.rect(width: 48, height: 20, flex: 0), ], // R3: checkInAddress [TDSkeletonRowColObj.text(height: 14)], // R4: summary + date [ TDSkeletonRowColObj.text(height: 14, flex: 2), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0), ], ], style: TDSkeletonRowColStyle(rowSpacing: (_) => 4), ), ), ); } } /// Skeleton 占位卡片,匹配公告卡片布局(基于 TDSkeleton.fromRowCol): /// /// 真实卡片结构: /// ┌────────────────────────────┐ /// │ title │ R1: Text(15/600) /// │ 8px │ /// │ typeTag publisher ← → date│ R2: tag(56) + SizedBox(8) + Text(12) + spaceBetween + Text(12) /// └────────────────────────────┘ class SkeletonAnnouncementCard extends StatelessWidget { const SkeletonAnnouncementCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: TDSkeleton.fromRowCol( animation: TDSkeletonAnimation.flashed, rowCol: TDSkeletonRowCol( objects: const [ // R1: title (full width) [TDSkeletonRowColObj.text(height: 18)], // R2: typeTag + publisher + spacer(flex:1) + date [ TDSkeletonRowColObj.rect(width: 56, height: 20, flex: 0), TDSkeletonRowColObj.spacer(width: 8), TDSkeletonRowColObj.text(width: 60, height: 14, flex: 0), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.text(width: 120, height: 14, flex: 0), ], ], style: TDSkeletonRowColStyle(rowSpacing: (_) => 8), ), ), ); } } /// Skeleton 占位卡片,匹配 [MessageItem] 布局(基于 TDSkeleton.fromRowCol): /// /// 真实 MessageItem 结构: /// ┌──────────────────────────────────────────┐ /// │ [● 40×40] 标题文字... MM-DD HH:mm │ [●] │ /// │ 发送人 │ │ /// │ 摘要内容... │ │ /// └──────────────────────────────────────────┘ class SkeletonMessageCard extends StatelessWidget { const SkeletonMessageCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Container( height: 88, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ // 左侧圆形图标占位 40×40 TDSkeleton.fromRowCol( animation: TDSkeletonAnimation.flashed, rowCol: TDSkeletonRowCol( objects: const [ [TDSkeletonRowColObj.rect(width: 40, height: 40, flex: 0)], ], ), ), const SizedBox(width: 12), // 中间三行文字骨架 Expanded( child: TDSkeleton.fromRowCol( animation: TDSkeletonAnimation.flashed, rowCol: TDSkeletonRowCol( objects: const [ // R1: 标题(宽) + 时间(窄) [ TDSkeletonRowColObj.text(height: 17, flex: 3), TDSkeletonRowColObj.spacer(flex: 1), TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0), ], // R2: 发送人 [TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0)], // R3: 摘要 [TDSkeletonRowColObj.text(height: 13, flex: 2)], ], style: TDSkeletonRowColStyle(rowSpacing: (_) => 2), ), ), ), const SizedBox(width: 12), // 右侧未读红点占位 8×8 TDSkeleton.fromRowCol( animation: TDSkeletonAnimation.flashed, rowCol: TDSkeletonRowCol( objects: const [ [TDSkeletonRowColObj.rect(width: 8, height: 8, flex: 0)], ], ), ), ], ), ); } } /// 列表加载态:骨架卡片占位 /// /// [cardCount] 骨架卡片数量,默认 5 /// [cardBuilder] 骨架卡片构建器,默认 [SkeletonListCard] class SkeletonLoadingList extends StatelessWidget { final int cardCount; final Widget Function() cardBuilder; const SkeletonLoadingList({ super.key, this.cardCount = 5, this.cardBuilder = _defaultBuilder, }); static Widget _defaultBuilder() => const SkeletonListCard(); @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), physics: const NeverScrollableScrollPhysics(), children: List.generate( cardCount, (_) => Padding( padding: const EdgeInsets.only(bottom: 16), child: cardBuilder(), ), ), ); } }