import 'package:flutter/material.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_typography.dart'; /// 页面骨架:固定顶栏 + 内容区。对应源「根容器固定、滚动只发生在内容区」的布局。 class XPageShell extends StatelessWidget { const XPageShell({ super.key, required this.topBar, required this.child, this.backgroundColor = AppColors.bgPage, }); final Widget topBar; final Widget child; final Color backgroundColor; @override Widget build(BuildContext context) { return Container( color: backgroundColor, child: Column( children: [topBar, Expanded(child: child)], ), ); } } /// 页面大标题 + 副标题,对应 `.page-header` / `.section-title`(28px/w700)。 class XPageTitle extends StatelessWidget { const XPageTitle({super.key, required this.title, this.subtitle}); final String title; final String? subtitle; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: AppTypography.navLargeTitle.copyWith( color: AppColors.onSurface, ), ), if (subtitle != null) ...[ const SizedBox(height: 4), Text( subtitle!, style: AppTypography.bodyMd.copyWith( color: AppColors.onSurfaceVariant, ), ), ], ], ); } } /// 空态 / 加载中占位。 class XPlaceholder extends StatelessWidget { const XPlaceholder({ super.key, required this.text, this.padding = const EdgeInsets.symmetric(vertical: 60), }); final String text; final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { return Padding( padding: padding, child: Center( child: Text( text, textAlign: TextAlign.center, style: AppTypography.bodyMd.copyWith(color: AppColors.textMuted), ), ), ); } } /// 实心主按钮,对应 `.login-btn` / `.start-btn` 等。 class XPrimaryButton extends StatelessWidget { const XPrimaryButton({ super.key, required this.label, required this.onPressed, this.leading, this.loading = false, this.height = 48, this.radius = AppRadius.lg, this.background = AppColors.primary, this.foreground = AppColors.onPrimary, this.fontSize = 17, this.shadows, }); final String label; final VoidCallback? onPressed; final Widget? leading; final bool loading; final double height; final double radius; final Color background; final Color foreground; final double fontSize; final List? shadows; @override Widget build(BuildContext context) { final disabled = onPressed == null || loading; return Opacity( opacity: disabled && !loading ? 0.55 : 1, child: SizedBox( height: height, width: double.infinity, child: Material( color: background, borderRadius: BorderRadius.circular(radius), child: InkWell( onTap: disabled ? null : onPressed, borderRadius: BorderRadius.circular(radius), child: Container( alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(radius), boxShadow: shadows, ), child: loading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(foreground), ), ) : Row( mainAxisSize: MainAxisSize.min, children: [ if (leading != null) ...[ leading!, const SizedBox(width: 8), ], Text( label, style: TextStyle( fontSize: fontSize, fontWeight: FontWeight.w600, color: foreground, ), ), ], ), ), ), ), ), ); } } /// 区块小标题(20/w600),对应 `.chart-title` / `.section-title`。 class XSectionTitle extends StatelessWidget { const XSectionTitle(this.text, {super.key, this.trailing}); final String text; final Widget? trailing; @override Widget build(BuildContext context) { final title = Text( text, style: AppTypography.headlineMd.copyWith(color: AppColors.onSurface), ); if (trailing == null) return title; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [title, trailing!], ); } }