refactor: 死代码清理 + 趋势图重构 + 侧边栏交互优化 + 智能体卡片去延迟
- 死代码清理: 删除 AppCard/AppMenuItem/AppTabChip/AppButtons 等 4 个未使用 Widget 文件; 清理 common_widgets/enterprise_widgets/app_status_badge 中未使用 class; 移除 HealthService 等未使用方法; 后端移除 ExerciseService.CreateFromItemsAsync/HealthArchiveService.GetOrCreateAsync/MedicationAgentHandler 死分支/ChatRequest DTO - 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格; X 轴日+月份; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层; 选中点变实心 - 侧边栏: 对话记录改单选删除(灰底高亮); 操作栏浮层修复触摸问题; 常用功能/健康仪表盘间距收紧; _Panel 去外框 - 智能体: 欢迎卡片去掉 400ms 延迟; 胶囊去阴影 - 其他: api_client IP 适配; 多页面 UI 微调; 新增 chat_provider/prelaunch_guardrails 测试
This commit is contained in:
@@ -4,12 +4,30 @@ import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../core/app_design_tokens.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/app_toast.dart';
|
||||
import '../../providers/data_providers.dart';
|
||||
|
||||
const String defaultTrendMetricType = 'blood_pressure';
|
||||
|
||||
const Set<String> supportedTrendMetricTypes = {
|
||||
'blood_pressure',
|
||||
'heart_rate',
|
||||
'glucose',
|
||||
'spo2',
|
||||
'weight',
|
||||
};
|
||||
|
||||
String normalizeTrendMetricType(String? metricType) {
|
||||
if (metricType == null) return defaultTrendMetricType;
|
||||
return supportedTrendMetricTypes.contains(metricType)
|
||||
? metricType
|
||||
: defaultTrendMetricType;
|
||||
}
|
||||
|
||||
/// 健康概览趋势页 — 五大指标合到一个页面
|
||||
class TrendPage extends ConsumerStatefulWidget {
|
||||
final String? metricType; // 可选初始选中指标
|
||||
@@ -20,7 +38,7 @@ class TrendPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
String _selected = 'blood_pressure';
|
||||
String _selected = defaultTrendMetricType;
|
||||
List<Map<String, dynamic>> _allRecords = [];
|
||||
List<Map<String, dynamic>> _filtered = [];
|
||||
bool _loading = true;
|
||||
@@ -84,12 +102,15 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
String get _name => _names[_selected] ?? '';
|
||||
bool get _isBP => _selected == 'blood_pressure';
|
||||
Color get _color =>
|
||||
_metrics.firstWhere((m) => m['key'] == _selected)['color'] as Color;
|
||||
_metrics.firstWhere(
|
||||
(m) => m['key'] == normalizeTrendMetricType(_selected),
|
||||
)['color']
|
||||
as Color;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.metricType != null) _selected = widget.metricType!;
|
||||
_selected = normalizeTrendMetricType(widget.metricType);
|
||||
_loadAll();
|
||||
}
|
||||
|
||||
@@ -293,7 +314,10 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
title: const Text('健康概览'),
|
||||
centerTitle: true,
|
||||
),
|
||||
@@ -431,8 +455,11 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
}
|
||||
|
||||
// X 轴刻度间隔:日标签短,可以排密一点
|
||||
final xInterval =
|
||||
spots.length > 60 ? 5.0 : spots.length > 30 ? 2.0 : 1.0;
|
||||
final xInterval = spots.length > 60
|
||||
? 5.0
|
||||
: spots.length > 30
|
||||
? 2.0
|
||||
: 1.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 20, 16, 12),
|
||||
@@ -490,10 +517,10 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
? (r['systolic'] as num?)?.toDouble()
|
||||
: (r['value'] as num?)?.toDouble();
|
||||
if (v != null && spots.length > 1) {
|
||||
final px = 40.0 +
|
||||
final px =
|
||||
40.0 +
|
||||
(_selectedIdx! / (spots.length - 1)) * paintWidth;
|
||||
final py =
|
||||
paintHeight * (1 - (v - minV) / (maxV - minV));
|
||||
final py = paintHeight * (1 - (v - minV) / (maxV - minV));
|
||||
final d = r['date'] as DateTime;
|
||||
tooltipPos = Offset(px, py);
|
||||
tooltipText1 =
|
||||
@@ -513,191 +540,198 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
minY: minV,
|
||||
maxY: maxV,
|
||||
gridData: const FlGridData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
interval: yStep,
|
||||
getTitlesWidget: (v, meta) => Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
v.toStringAsFixed(0),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 36,
|
||||
interval: xInterval,
|
||||
getTitlesWidget: (v, meta) {
|
||||
final idx = v.toInt();
|
||||
if (idx < 0 || idx >= _filtered.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final d = _filtered[idx]['date'] as DateTime;
|
||||
// 当前可见刻度与上一个可见刻度相比,月份是否变化
|
||||
final prevIdx = idx - xInterval.toInt();
|
||||
final showMonth = idx == 0 ||
|
||||
prevIdx < 0 ||
|
||||
(_filtered[prevIdx]['date'] as DateTime).month !=
|
||||
d.month;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${d.day}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (showMonth) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${d.month}月',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
interval: yStep,
|
||||
getTitlesWidget: (v, meta) => Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(
|
||||
v.toStringAsFixed(0),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: false,
|
||||
color: _color,
|
||||
barWidth: 2.5,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(
|
||||
show: spots.length <= 30,
|
||||
getDotPainter: (spot, _, _, _) {
|
||||
final isSelected = spot.x.toInt() == _selectedIdx;
|
||||
if (isSelected) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 5.5,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 36,
|
||||
interval: xInterval,
|
||||
getTitlesWidget: (v, meta) {
|
||||
final idx = v.toInt();
|
||||
if (idx < 0 || idx >= _filtered.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final d = _filtered[idx]['date'] as DateTime;
|
||||
// 当前可见刻度与上一个可见刻度相比,月份是否变化
|
||||
final prevIdx = idx - xInterval.toInt();
|
||||
final showMonth =
|
||||
idx == 0 ||
|
||||
prevIdx < 0 ||
|
||||
(_filtered[prevIdx]['date'] as DateTime)
|
||||
.month !=
|
||||
d.month;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${d.day}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textHint,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (showMonth) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${d.month}月',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: false,
|
||||
color: _color,
|
||||
strokeWidth: 2.5,
|
||||
strokeColor: Colors.white,
|
||||
);
|
||||
}
|
||||
return FlDotCirclePainter(
|
||||
radius: 3.5,
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
strokeColor: _color,
|
||||
);
|
||||
},
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
_color.withValues(alpha: 0.22),
|
||||
_color.withValues(alpha: 0.02),
|
||||
barWidth: 2.5,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(
|
||||
show: spots.length <= 30,
|
||||
getDotPainter: (spot, _, _, _) {
|
||||
final isSelected =
|
||||
spot.x.toInt() == _selectedIdx;
|
||||
if (isSelected) {
|
||||
return FlDotCirclePainter(
|
||||
radius: 5.5,
|
||||
color: _color,
|
||||
strokeWidth: 2.5,
|
||||
strokeColor: Colors.white,
|
||||
);
|
||||
}
|
||||
return FlDotCirclePainter(
|
||||
radius: 3.5,
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
strokeColor: _color,
|
||||
);
|
||||
},
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
_color.withValues(alpha: 0.22),
|
||||
_color.withValues(alpha: 0.02),
|
||||
],
|
||||
),
|
||||
),
|
||||
shadow: Shadow(
|
||||
color: _color.withValues(alpha: 0.18),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
shadow: Shadow(
|
||||
color: _color.withValues(alpha: 0.18),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
),
|
||||
],
|
||||
lineTouchData: LineTouchData(
|
||||
handleBuiltInTouches: false,
|
||||
touchCallback: (event, response) {
|
||||
if (event is FlTapUpEvent) {
|
||||
final spots = response?.lineBarSpots;
|
||||
if (spots != null && spots.isNotEmpty) {
|
||||
final idx = spots.first.x.toInt();
|
||||
setState(() =>
|
||||
_selectedIdx = (_selectedIdx == idx) ? null : idx);
|
||||
} else {
|
||||
setState(() => _selectedIdx = null);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
if (tooltipPos != null)
|
||||
Positioned(
|
||||
left: tooltipPos.dx.clamp(60.0, chartWidth - 60.0),
|
||||
top: tooltipPos.dy - 56,
|
||||
child: FractionalTranslation(
|
||||
translation: const Offset(-0.5, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: _color.withValues(alpha: 0.35),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF101828)
|
||||
.withValues(alpha: 0.14),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
lineTouchData: LineTouchData(
|
||||
handleBuiltInTouches: false,
|
||||
touchCallback: (event, response) {
|
||||
if (event is FlTapUpEvent) {
|
||||
final spots = response?.lineBarSpots;
|
||||
if (spots != null && spots.isNotEmpty) {
|
||||
final idx = spots.first.x.toInt();
|
||||
setState(
|
||||
() => _selectedIdx = (_selectedIdx == idx)
|
||||
? null
|
||||
: idx,
|
||||
);
|
||||
} else {
|
||||
setState(() => _selectedIdx = null);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
tooltipText1!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textHint,
|
||||
fontWeight: FontWeight.w500,
|
||||
if (tooltipPos != null)
|
||||
Positioned(
|
||||
left: tooltipPos.dx.clamp(60.0, chartWidth - 60.0),
|
||||
top: tooltipPos.dy - 56,
|
||||
child: FractionalTranslation(
|
||||
translation: const Offset(-0.5, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: _color.withValues(alpha: 0.35),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(
|
||||
0xFF101828,
|
||||
).withValues(alpha: 0.14),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
tooltipText1!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textHint,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
tooltipText2!,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: _color,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
tooltipText2!,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: _color,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -731,137 +765,161 @@ class _TrendPageState extends ConsumerState<TrendPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
..._filtered.reversed.take(30).map((r) {
|
||||
final date = r['date'] as DateTime;
|
||||
final abnormal = r['isAbnormal'] == true;
|
||||
final id = r['id']?.toString() ?? '';
|
||||
final idx = _filtered.indexOf(r);
|
||||
final isSelected = idx == _selectedIdx;
|
||||
String display;
|
||||
if (_isBP) {
|
||||
display = '${r['systolic'] ?? '--'}/${r['diastolic'] ?? '--'}';
|
||||
} else {
|
||||
display = '${r['value'] ?? '--'}';
|
||||
}
|
||||
return Dismissible(
|
||||
key: Key(id),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.error,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: const Icon(Icons.delete_outline, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (_) async {
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
await api.delete('/api/health-records/$id');
|
||||
setState(() {
|
||||
_allRecords.removeWhere((x) => x['id']?.toString() == id);
|
||||
_filtered.removeWhere((x) => x['id']?.toString() == id);
|
||||
_selectedIdx = null;
|
||||
});
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (idx < 0) return;
|
||||
setState(() =>
|
||||
_selectedIdx = isSelected ? null : idx);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? _color.withValues(alpha: 0.15)
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? _color.withValues(alpha: 0.55)
|
||||
: AppColors.border,
|
||||
width: isSelected ? 1.5 : 1,
|
||||
),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _color.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getMetricIcon(_selected),
|
||||
size: 21,
|
||||
color: _color,
|
||||
),
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: AppRadius.xlBorder,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
..._filtered.reversed.take(30).toList().asMap().entries.map((
|
||||
entry,
|
||||
) {
|
||||
final r = entry.value;
|
||||
final isLast =
|
||||
entry.key == _filtered.reversed.take(30).length - 1;
|
||||
final date = r['date'] as DateTime;
|
||||
final abnormal = r['isAbnormal'] == true;
|
||||
final id = r['id']?.toString() ?? '';
|
||||
final idx = _filtered.indexOf(r);
|
||||
final isSelected = idx == _selectedIdx;
|
||||
String display;
|
||||
if (_isBP) {
|
||||
display =
|
||||
'${r['systolic'] ?? '--'}/${r['diastolic'] ?? '--'}';
|
||||
} else {
|
||||
display = '${r['value'] ?? '--'}';
|
||||
}
|
||||
return Dismissible(
|
||||
key: Key(id),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
color: AppColors.error,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${date.month}月${date.day}日 ${date.hour}:${date.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: AppColors.textHint,
|
||||
confirmDismiss: (_) async {
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
await api.delete('/api/health-records/$id');
|
||||
setState(() {
|
||||
_allRecords.removeWhere(
|
||||
(x) => x['id']?.toString() == id,
|
||||
);
|
||||
_filtered.removeWhere((x) => x['id']?.toString() == id);
|
||||
_selectedIdx = null;
|
||||
});
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (idx < 0) return;
|
||||
setState(() => _selectedIdx = isSelected ? null : idx);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
color: isSelected
|
||||
? _color.withValues(alpha: 0.10)
|
||||
: Colors.white,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _color.withAlpha(20),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getMetricIcon(_selected),
|
||||
size: 21,
|
||||
color: _color,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${date.month}月${date.day}日 ${date.hour}:${date.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$display $_unit',
|
||||
style: TextStyle(
|
||||
fontSize: 23,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: abnormal
|
||||
? AppColors.error
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (abnormal)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.errorLight,
|
||||
borderRadius: AppRadius.xsBorder,
|
||||
),
|
||||
child: const Text(
|
||||
'异常',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$display $_unit',
|
||||
style: TextStyle(
|
||||
fontSize: 23,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: abnormal
|
||||
? AppColors.error
|
||||
: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!isLast)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 66),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 0.7,
|
||||
color: Color(0xFFE8ECF2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (abnormal)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.errorLight,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text(
|
||||
'异常',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user