Files
AI-Health/health_app/lib/widgets/app_buttons.dart
MingNian 1c020b8ae5 feat: UI 系统中心化 + 趋势图重构 + 法律文档 H5 上线
- UI 系统: 新增 design_tokens / module_visuals / app_buttons / app_status_badge / app_toast / ai_content 六个共享模块; 28 个页面接入, SnackBar 全部替换为 AppToast
- 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格线; X 轴日+月份分隔; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层, 选中点变实心
- 法律文档 H5: 后端 wwwroot 托管隐私政策/服务协议/关于/个人信息收集清单/第三方 SDK 清单 + 索引页; Program.cs 启用 UseDefaultFiles + UseStaticFiles
- 其他: auth_provider 登录流程调整; api_client IP 适配; 主页智能体栏间距优化
2026-07-08 21:25:07 +08:00

243 lines
6.6 KiB
Dart

import 'package:flutter/material.dart';
import '../core/app_colors.dart';
import '../core/app_design_tokens.dart';
enum AppButtonVariant { primary, secondary, outline, ghost, danger, gradientOutline }
class AppButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final IconData? icon;
final AppButtonVariant variant;
final Color? color;
final LinearGradient? gradient;
final bool loading;
final double height;
final bool expand;
const AppButton({
super.key,
required this.label,
this.onPressed,
this.icon,
this.variant = AppButtonVariant.primary,
this.color,
this.gradient,
this.loading = false,
this.height = 50,
this.expand = true,
});
@override
Widget build(BuildContext context) {
final enabled = onPressed != null && !loading;
final baseColor = color ?? AppColors.primary;
final child = _ButtonContent(
label: label,
icon: icon,
loading: loading,
foreground: _foreground(baseColor),
);
if (variant == AppButtonVariant.gradientOutline) {
return _GradientOutlineButton(
label: label,
icon: icon,
loading: loading,
enabled: enabled,
height: height,
expand: expand,
gradient: gradient ?? AppColors.actionOutlineGradient,
onPressed: onPressed,
);
}
final decoration = _decoration(baseColor, enabled);
final width = expand ? double.infinity : null;
return SizedBox(
height: height,
width: width,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: enabled ? onPressed : null,
borderRadius: AppRadius.lgBorder,
child: Ink(
decoration: decoration,
child: Center(child: child),
),
),
),
);
}
Color _foreground(Color baseColor) {
return switch (variant) {
AppButtonVariant.primary => Colors.white,
AppButtonVariant.secondary => baseColor,
AppButtonVariant.outline => baseColor,
AppButtonVariant.ghost => baseColor,
AppButtonVariant.danger => variant == AppButtonVariant.danger && color == null
? AppColors.error
: Colors.white,
AppButtonVariant.gradientOutline => AppColors.textPrimary,
};
}
BoxDecoration _decoration(Color baseColor, bool enabled) {
final disabledColor = const Color(0xFFE5E7EB);
return switch (variant) {
AppButtonVariant.primary => BoxDecoration(
color: enabled ? baseColor : disabledColor,
borderRadius: AppRadius.lgBorder,
boxShadow: enabled ? AppShadows.soft : AppShadows.none,
),
AppButtonVariant.secondary => BoxDecoration(
color: enabled ? baseColor.withValues(alpha: 0.10) : disabledColor,
borderRadius: AppRadius.lgBorder,
),
AppButtonVariant.outline => BoxDecoration(
color: Colors.white,
borderRadius: AppRadius.lgBorder,
border: Border.all(color: enabled ? baseColor.withValues(alpha: 0.38) : disabledColor),
),
AppButtonVariant.ghost => BoxDecoration(
color: enabled ? baseColor.withValues(alpha: 0.08) : disabledColor,
borderRadius: AppRadius.lgBorder,
),
AppButtonVariant.danger => BoxDecoration(
color: enabled ? (color ?? AppColors.error) : disabledColor,
borderRadius: AppRadius.lgBorder,
),
AppButtonVariant.gradientOutline => const BoxDecoration(),
};
}
}
class _ButtonContent extends StatelessWidget {
final String label;
final IconData? icon;
final bool loading;
final Color foreground;
const _ButtonContent({
required this.label,
required this.icon,
required this.loading,
required this.foreground,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (loading)
SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: foreground),
)
else if (icon != null)
Icon(icon, size: 19, color: foreground),
if (loading || icon != null) const SizedBox(width: 8),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.button.copyWith(color: foreground),
),
),
],
);
}
}
class _GradientOutlineButton extends StatelessWidget {
final String label;
final IconData? icon;
final bool loading;
final bool enabled;
final double height;
final bool expand;
final LinearGradient gradient;
final VoidCallback? onPressed;
const _GradientOutlineButton({
required this.label,
required this.icon,
required this.loading,
required this.enabled,
required this.height,
required this.expand,
required this.gradient,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Container(
height: height,
width: expand ? double.infinity : null,
padding: const EdgeInsets.all(1.5),
decoration: BoxDecoration(
gradient: enabled ? gradient : null,
color: enabled ? null : AppColors.borderLight,
borderRadius: AppRadius.lgBorder,
boxShadow: enabled ? AppShadows.soft : AppShadows.none,
),
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.lg - 1.5),
child: InkWell(
onTap: enabled ? onPressed : null,
borderRadius: BorderRadius.circular(AppRadius.lg - 1.5),
child: Center(
child: _ButtonContent(
label: label,
icon: icon,
loading: loading,
foreground: AppColors.textPrimary,
),
),
),
),
);
}
}
class AppIconTile extends StatelessWidget {
final IconData icon;
final Color color;
final Color backgroundColor;
final double size;
final double iconSize;
final BorderRadius? borderRadius;
const AppIconTile({
super.key,
required this.icon,
required this.color,
required this.backgroundColor,
this.size = 44,
this.iconSize = 22,
this.borderRadius,
});
@override
Widget build(BuildContext context) => Container(
width: size,
height: size,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: borderRadius ?? AppRadius.mdBorder,
border: Border.all(color: color.withValues(alpha: 0.16)),
),
child: Icon(icon, size: iconSize, color: color),
);
}