import 'package:flutter/material.dart'; import 'package:skeletonizer/skeletonizer.dart'; import '../../core/theme/app_colors_extension.dart'; /// 骨架占位卡片容器(列表加载态) class SkeletonLoadingList extends StatelessWidget { final int cardCount; final Widget Function() cardBuilder; final EdgeInsetsGeometry padding; const SkeletonLoadingList({ super.key, this.cardCount = 5, this.cardBuilder = _defaultBuilder, this.padding = const EdgeInsets.fromLTRB(16, 16, 16, 24), }); static Widget _defaultBuilder() => const SkeletonListCard(); @override Widget build(BuildContext context) { return SingleChildScrollView( padding: padding, child: Skeletonizer( enabled: true, child: Column( children: List.generate( cardCount, (_) => Padding( padding: const EdgeInsets.only(bottom: 16), child: cardBuilder(), ), ), ), ), ); } } /// 匹配 ListCard 的骨架占位 class SkeletonListCard extends StatelessWidget { const SkeletonListCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Skeletonizer( enabled: true, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Bone.text(width: 120), Bone.text(width: 80), ], ), SizedBox(height: 6), Bone.text(width: 140), SizedBox(height: 6), Bone.text(), SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Bone.text(width: 100), Bone(width: 48, height: 20, borderRadius: BorderRadius.circular(4)), ], ), ], ), ), ); } } /// 匹配车辆列表卡片的骨架占位 class SkeletonVehicleCard extends StatelessWidget { const SkeletonVehicleCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Skeletonizer( enabled: true, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Bone.text(width: 100), Bone(width: 48, height: 20, borderRadius: BorderRadius.circular(4)), ], ), SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Bone.text(width: 140), Bone(width: 40, height: 16, borderRadius: BorderRadius.circular(4)), ], ), SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(child: Bone.text()), const SizedBox(width: 8), Bone.text(width: 120), ], ), ], ), ), ); } } /// 匹配外勤日志卡片的骨架占位 class SkeletonOutingLogCard extends StatelessWidget { const SkeletonOutingLogCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Skeletonizer( enabled: true, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Bone.text(width: 100), SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(flex: 3, child: Bone.text()), const SizedBox(width: 8), Bone(width: 48, height: 20, borderRadius: BorderRadius.circular(4)), ], ), SizedBox(height: 4), Bone.text(), SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(flex: 2, child: Bone.text()), const SizedBox(width: 8), Bone.text(width: 80), ], ), ], ), ), ); } } /// 匹配公告卡片的骨架占位 class SkeletonAnnouncementCard extends StatelessWidget { const SkeletonAnnouncementCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Skeletonizer( enabled: true, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Bone.text(), SizedBox(height: 8), Row( children: [ Bone(width: 56, height: 20, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Bone.text(width: 60), const Spacer(), Bone.text(width: 120), ], ), ], ), ), ); } } /// 匹配费用申请导入列表卡片的骨架占位 class SkeletonImportCard extends StatelessWidget { const SkeletonImportCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Skeletonizer( enabled: true, child: Container( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Bone.square(size: 22, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Expanded(flex: 3, child: Bone.text()), const Spacer(), Bone.text(width: 80), ], ), SizedBox(height: 8), Row(children: [const SizedBox(width: 30), Bone.text(width: 140)]), SizedBox(height: 8), Row( children: [ Bone.square(size: 18, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 4), Bone.text(width: 24), const SizedBox(width: 8), Expanded(flex: 3, child: Bone.text()), const Spacer(), Bone.text(width: 64), ], ), SizedBox(height: 8), Row(children: [const SizedBox(width: 54), Expanded(flex: 2, child: Bone.text())]), ], ), ), ); } } /// 匹配消息卡片的骨架占位 class SkeletonMessageCard extends StatelessWidget { const SkeletonMessageCard({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Skeletonizer( enabled: true, child: Container( height: 88, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Bone.circle(size: 40), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(flex: 3, child: Bone.text()), const Spacer(), Bone.text(width: 80), ], ), SizedBox(height: 2), Bone.text(width: 80), SizedBox(height: 2), Expanded(flex: 2, child: Bone.text()), ], ), ), SizedBox(width: 12), Bone(width: 8, height: 8, borderRadius: BorderRadius.circular(4)), ], ), ), ); } } /// 详情页骨架(ExpenseDetailPage / ExpenseApplyDetailPage) /// 匹配 FormSection 卡片布局:基本信息 + 明细 + 附件 class SkeletonDetailPage extends StatelessWidget { const SkeletonDetailPage({super.key}); @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), child: Skeletonizer( enabled: true, child: Column( children: [ _FormCard(rows: 7), const SizedBox(height: 16), _FormCard(rows: 2), const SizedBox(height: 16), _FormCard(rows: 3), const SizedBox(height: 24), const Center(child: Bone(width: 120, height: 14)), ], ), ), ); } } /// 表单页骨架(ExpenseApplyCreatePage / ExpenseCreatePage) /// 匹配 FormSection + 输入框 + 底部操作栏布局 class SkeletonFormPage extends StatelessWidget { final bool showImportLink; const SkeletonFormPage({super.key, this.showImportLink = false}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final bottomPadding = MediaQuery.of(context).padding.bottom; return Stack( children: [ SingleChildScrollView( padding: EdgeInsets.fromLTRB(16, 16, 16, 80 + bottomPadding), child: Skeletonizer( enabled: true, child: Column( children: [ if (showImportLink) ...[ // 导入申请单按钮骨架 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(12), ), child: Row(children: [ Bone.square(size: 20, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Bone(width: 160, height: 14), ]), ), const SizedBox(height: 16), ], _FormCard(rows: 5), const SizedBox(height: 16), _FormCard(rows: 2), const SizedBox(height: 16), _FormCard(rows: 3), const SizedBox(height: 24), const Center(child: Bone(width: 120, height: 14)), ], ), ), ), // 底部操作栏骨架,贴底并适配安全区 Positioned( left: 0, right: 0, bottom: 0, child: Skeletonizer( enabled: true, child: Container( padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + bottomPadding), decoration: BoxDecoration( color: colors.bgCard, border: Border(top: BorderSide(color: colors.border)), ), child: Row( children: [ Expanded(child: Bone(height: 44, borderRadius: BorderRadius.circular(8))), const SizedBox(width: 12), Expanded(child: Bone(height: 44, borderRadius: BorderRadius.circular(8))), ], ), ), ), ), ], ); } } class _FormCard extends StatelessWidget { final int rows; const _FormCard({required this.rows}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Bone.square(size: 20, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Bone(width: 80, height: 16), const Spacer(), Bone(width: 40, height: 16), ]), const SizedBox(height: 20), ...List.generate(rows, (i) => Padding( padding: EdgeInsets.only(bottom: i < rows - 1 ? 16 : 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Bone(width: 64, height: 14), Bone(width: 140 + (i % 3) * 20, height: 14), ], ), )), ], ), ); } } /// 报表页骨架(ExpenseDetailReportPage / ExpenseApplyDetailReportPage) /// 匹配 筛选栏 + 统计卡片(2x2) + 图表卡片 + 明细列表 布局 class SkeletonReportPage extends StatelessWidget { const SkeletonReportPage({super.key}); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return SingleChildScrollView( child: Skeletonizer( enabled: true, child: Column( children: [ // 日期筛选栏 Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), color: colors.bgCard, child: Row(children: [ Expanded(child: Bone(height: 40, borderRadius: BorderRadius.circular(20))), const SizedBox(width: 8), Bone(width: 14, height: 14), const SizedBox(width: 8), Expanded(child: Bone(height: 40, borderRadius: BorderRadius.circular(20))), const SizedBox(width: 8), Bone(width: 40, height: 40, borderRadius: BorderRadius.circular(20)), ]), ), const SizedBox(height: 16), // 统计卡片 — 2行 x 2列 Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Column(children: [ Row(children: [ Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))), const SizedBox(width: 8), Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))), ]), const SizedBox(height: 8), Row(children: [ Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))), const SizedBox(width: 8), Expanded(child: Bone(height: 80, borderRadius: BorderRadius.circular(12))), ]), ]), ), const SizedBox(height: 16), // 图表卡片 — 标题 + 图表区 Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: colors.bgCard, borderRadius: BorderRadius.circular(12)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [Bone.square(size: 20, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Bone(width: 120, height: 16)]), const SizedBox(height: 16), Bone(height: 200, borderRadius: BorderRadius.circular(8)), ]), ), ), const SizedBox(height: 16), // 明细列表卡片 — 表头 + 行 Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: colors.bgCard, borderRadius: BorderRadius.circular(12)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [Bone.square(size: 20, borderRadius: BorderRadius.circular(4)), const SizedBox(width: 8), Bone(width: 80, height: 16)]), const SizedBox(height: 16), ...List.generate(6, (_) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Bone(width: 60, height: 14), Bone(width: 100, height: 14), Bone(width: 80, height: 14), Bone(width: 60, height: 14), ]), )), ]), ), ), const SizedBox(height: 24), ], ), ), ); } }