| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097 |
- 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<AppColorsExtension>()!;
- 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<AppColorsExtension>()!;
- 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<AppColorsExtension>()!;
- 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<AppColorsExtension>()!;
- 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<AppColorsExtension>()!;
- 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<AppColorsExtension>()!;
- 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)),
- ],
- ),
- ),
- );
- }
- }
- /// 详情页骨架 — 匹配 FormSection 卡片布局。
- ///
- /// [sectionRows] 每个 FormSection 的占位行数,默认值对应 ExpenseDetailPage 布局。
- class SkeletonDetailPage extends StatelessWidget {
- final List<int> sectionRows;
- final bool showPageFooter;
- const SkeletonDetailPage({
- super.key,
- this.sectionRows = const [7, 2, 3],
- this.showPageFooter = true,
- });
- @override
- Widget build(BuildContext context) {
- return SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
- child: Skeletonizer(
- enabled: true,
- child: Column(
- children: [
- for (var i = 0; i < sectionRows.length; i++) ...[
- _FormCard(rows: sectionRows[i]),
- if (i < sectionRows.length - 1) const SizedBox(height: 16),
- ],
- if (showPageFooter) ...[
- const SizedBox(height: 24),
- const Center(child: Bone(width: 120, height: 14)),
- ],
- ],
- ),
- ),
- );
- }
- }
- /// 表单页骨架 — 匹配 FormSection + 输入框 + 底部操作栏布局。
- ///
- /// [sectionRows] 每个 FormSection 的占位行数,例如 `[4, 6, 4]` 表示 3 个卡片分别为 4、6、4 行。
- /// [bottomButtonCount] 底部操作栏按钮数(1 或 2),默认 2。
- /// [showImportLink] 是否显示导入申请单入口骨架。
- class SkeletonFormPage extends StatelessWidget {
- final List<int> sectionRows;
- final int bottomButtonCount;
- final bool showImportLink;
- final bool showPageFooter;
- const SkeletonFormPage({
- super.key,
- this.sectionRows = const [5, 2, 3],
- this.bottomButtonCount = 2,
- this.showImportLink = false,
- this.showPageFooter = true,
- });
- @override
- Widget build(BuildContext context) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- 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),
- ],
- for (var i = 0; i < sectionRows.length; i++) ...[
- _FormCard(rows: sectionRows[i]),
- if (i < sectionRows.length - 1) const SizedBox(height: 16),
- ],
- if (showPageFooter) ...[
- 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: [
- if (bottomButtonCount == 1) ...[
- Expanded(
- child: Bone(
- height: 44,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ] else ...[
- 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<AppColorsExtension>()!;
- 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),
- ],
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
- // ═══ 报表页骨架 — 各页面按实际布局独立定义 ═══
- /// 事前申请明细报表骨架(ExpenseApplyDetailReportPage)
- /// 布局:筛选栏 + 统计卡片(1行2个) + 折线图(单线) + 明细列表
- class SkeletonExpenseApplyDetailReportPage extends StatelessWidget {
- const SkeletonExpenseApplyDetailReportPage({super.key});
- @override
- Widget build(BuildContext context) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- return Column(
- children: [
- _dateFilterChip(colors),
- Expanded(
- child: SingleChildScrollView(
- child: Skeletonizer(
- enabled: true,
- child: Column(
- children: [
- // 统计卡片 — 1行2列
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
- child: Row(
- children: [
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 16),
- // 折线图 — 标题 + 1个图例 + 图表区
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Bone(width: 120, height: 16),
- const SizedBox(height: 8),
- Row(
- children: [
- Bone.square(
- size: 8,
- borderRadius: BorderRadius.circular(4),
- ),
- const SizedBox(width: 4),
- Bone(width: 60, height: 12),
- ],
- ),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: Bone(
- height: 200,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: 16),
- // 明细列表
- _detailListSkeleton(colors),
- const SizedBox(height: 80),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
- }
- /// 费用报销明细报表骨架(ExpenseDetailReportPage)
- /// 布局:筛选栏 + 统计卡片(2+1) + 双折线图 + 柱形图 + 明细列表
- class SkeletonExpenseDetailReportPage extends StatelessWidget {
- const SkeletonExpenseDetailReportPage({super.key});
- @override
- Widget build(BuildContext context) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- return Column(
- children: [
- _dateFilterChip(colors),
- Expanded(
- child: SingleChildScrollView(
- child: Skeletonizer(
- enabled: true,
- child: Column(
- children: [
- // 统计卡片 — 1行2列 + 1个全宽
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
- child: Row(
- children: [
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Row(
- children: [
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 16),
- // 双折线图 — 标题 + 2个图例 + 图表区
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Bone(width: 120, height: 16),
- const SizedBox(height: 8),
- Row(
- children: [
- Bone.square(
- size: 8,
- borderRadius: BorderRadius.circular(4),
- ),
- const SizedBox(width: 4),
- Bone(width: 60, height: 12),
- const SizedBox(width: 16),
- Bone.square(
- size: 8,
- borderRadius: BorderRadius.circular(4),
- ),
- const SizedBox(width: 4),
- Bone(width: 60, height: 12),
- ],
- ),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: 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(8),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Bone(width: 160, height: 16),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: Bone(
- height: 220,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: 12),
- // 明细列表
- _detailListSkeleton(colors),
- const SizedBox(height: 80),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
- }
- /// 用车明细报表骨架(VehicleDetailReportPage)
- /// 布局:筛选栏 + 统计卡片(2+1) + 双折线图 + 柱形图 + 明细列表
- class SkeletonVehicleDetailReportPage extends StatelessWidget {
- const SkeletonVehicleDetailReportPage({super.key});
- @override
- Widget build(BuildContext context) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- return Column(
- children: [
- _dateFilterChip(colors),
- Expanded(
- child: SingleChildScrollView(
- child: Skeletonizer(
- enabled: true,
- child: Column(
- children: [
- // 统计卡片 — 1行2列 + 1个全宽
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
- child: Row(
- children: [
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Row(
- children: [
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 16),
- // 双折线图 — 标题 + 2个图例 + 图表区
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Bone(width: 120, height: 16),
- const SizedBox(height: 8),
- Row(
- children: [
- Bone.square(
- size: 8,
- borderRadius: BorderRadius.circular(4),
- ),
- const SizedBox(width: 4),
- Bone(width: 60, height: 12),
- const SizedBox(width: 16),
- Bone.square(
- size: 8,
- borderRadius: BorderRadius.circular(4),
- ),
- const SizedBox(width: 4),
- Bone(width: 60, height: 12),
- ],
- ),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: 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(8),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Bone(width: 160, height: 16),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: Bone(
- height: 220,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: 12),
- // 明细列表
- _detailListSkeleton(colors),
- const SizedBox(height: 80),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
- }
- // ── 日期筛选 chip 占位 ──
- Widget _dateFilterChip(AppColorsExtension colors) => Container(
- padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
- color: colors.bgCard,
- child: Container(
- height: 40,
- decoration: BoxDecoration(
- color: colors.bgSecondaryContainer,
- borderRadius: BorderRadius.circular(20),
- ),
- ),
- );
- // ── 明细列表骨架 ──
- Widget _detailListSkeleton(AppColorsExtension colors) => Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Container(
- width: double.infinity,
- decoration: BoxDecoration(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
- child: Row(
- children: [
- Bone(width: 80, height: 16),
- const Spacer(),
- Bone(width: 100, height: 14),
- ],
- ),
- ),
- const Divider(height: 1),
- ...List.generate(4, (i) {
- return Column(
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Bone(width: 120 + (i % 3) * 40, height: 14),
- Bone(width: 80, height: 16),
- ],
- ),
- const SizedBox(height: 6),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Bone(width: 90, height: 12),
- Bone(width: 80, height: 16),
- ],
- ),
- if (i == 0) ...[
- const SizedBox(height: 6),
- Bone(width: 200, height: 14),
- ],
- ],
- ),
- ),
- if (i < 3) const Divider(height: 1),
- ],
- );
- }),
- ],
- ),
- ),
- );
- /// 加班明细报表骨架(OvertimeDetailReportPage)
- /// 布局:筛选栏 + 统计卡片(2x2) + 折线图(单线) + 明细列表
- class SkeletonOTDetailReportPage extends StatelessWidget {
- const SkeletonOTDetailReportPage({super.key});
- @override
- Widget build(BuildContext context) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- return Column(
- children: [
- _dateFilterChip(colors),
- Expanded(
- child: SingleChildScrollView(
- child: Skeletonizer(
- enabled: true,
- child: Column(
- children: [
- // 统计卡片 — 2行2列
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
- child: Row(
- children: [
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Row(
- children: [
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: Bone(
- height: 80,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 16),
- // 折线图 — 标题 + 1个图例 + 图表区
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: colors.bgCard,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Bone(width: 120, height: 16),
- const SizedBox(height: 8),
- Row(
- children: [
- Bone.square(
- size: 8,
- borderRadius: BorderRadius.circular(4),
- ),
- const SizedBox(width: 4),
- Bone(width: 60, height: 12),
- ],
- ),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: Bone(
- height: 200,
- borderRadius: BorderRadius.circular(8),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: 16),
- // 明细列表
- _detailListSkeleton(colors),
- const SizedBox(height: 80),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
- }
|