Files
Aq-Accounting-Flutter/lib/pages/health/widgets/health_card.dart
T
2026-09-28 16:40:32 +08:00

169 lines
4.7 KiB
Dart

import 'package:flutter/material.dart';
/// 强宝健康主页上的一个卡片外壳。
///
/// 三个卡片(减肥进度 / 当前体重 / 今日饮食)结构完全一样:
/// 标题 + 右上角可选的附属信息 + 内容区 + 底部按钮。抽出来是为了
/// **高度和间距自动一致** —— 各自手写一遍迟早会差几个像素。
class HealthCard extends StatelessWidget {
const HealthCard({
super.key,
required this.title,
required this.child,
this.trailing,
this.onTap,
this.action,
});
final String title;
/// 右上角的小字,如「已减 1.8kg」
final Widget? trailing;
final Widget child;
/// 点卡片整体的行为(一般是进详情页)
final VoidCallback? onTap;
/// 底部按钮(如「+ 记一笔」)。为空时不占高度。
final Widget? action;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF5D4037).withValues(alpha: 0.07),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
),
if (trailing != null)
// Flexible:右侧小字是「锦上添花」,空间不够时它该先让位,
// 而不是把标题挤到溢出
Flexible(child: trailing!),
if (onTap != null)
const Padding(
padding: EdgeInsets.only(left: 2),
child: Icon(Icons.chevron_right,
size: 18, color: Color(0xFFBCAAA4)),
),
],
),
const SizedBox(height: 12),
child,
if (action != null) ...[
const SizedBox(height: 12),
action!,
],
],
),
),
);
}
}
/// 一条横向的进度条(减肥进度 / 时间进度共用)
///
/// [value] 会被夹到 0~1:后端虽然已经 Clamp 过,但这里再兜一次,
/// 免得某天后端改了什么导致宽度溢出。
class HealthProgressBar extends StatelessWidget {
const HealthProgressBar({
super.key,
required this.value,
this.color = const Color(0xFFC8956E),
this.height = 8,
this.background = const Color(0xFFF0EAE4),
});
final double value;
final Color color;
final double height;
final Color background;
@override
Widget build(BuildContext context) {
final safe = value.isFinite ? value.clamp(0.0, 1.0) : 0.0;
return ClipRRect(
borderRadius: BorderRadius.circular(height / 2),
child: LinearProgressIndicator(
value: safe,
minHeight: height,
backgroundColor: background,
valueColor: AlwaysStoppedAnimation<Color>(color),
),
);
}
}
/// 一个「标签 + 数值」的小块,卡片里成排用
class HealthStat extends StatelessWidget {
const HealthStat({
super.key,
required this.label,
required this.value,
this.unit,
this.valueColor = const Color(0xFF5D4037),
});
final String label;
final String value;
final String? unit;
final Color valueColor;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(fontSize: 11.5, color: Color(0xFF8D6E63))),
const SizedBox(height: 3),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: valueColor,
),
),
if (unit != null) ...[
const SizedBox(width: 2),
Text(unit!,
style: const TextStyle(
fontSize: 11.5, color: Color(0xFF8D6E63))),
],
],
),
],
);
}
}