P2 必须修复: - 教师布置主题 classId 从硬编码改为班级下拉选择器 - 班级日记墙使用服务端 classId 过滤替代前端过滤 - Profile 统计栏接入 JournalRepository 真实数据 - WeeklyPage 从全硬编码改为 JournalRepository 数据驱动 P3 建议改进: - 提取 mood_utils.dart 公共函数,消除 4 处重复定义 - 贴纸库搜索框连接 StickerBloc 按名称过滤 P4 细节打磨: - 家长页多孩子时显示 DropdownButton 选择器 - 搜索结果日记卡片点击跳转 /editor?id= - MonthlyPage 照片数量从 JournalElement 统计 - calendar_page/mood_page/search_page 统一使用 moodToEmoji/moodToLabel
785 lines
24 KiB
Dart
785 lines
24 KiB
Dart
// 心情页面 — 今日心情 + 天气 + 柱状图 + 统计网格 + 心情洞察
|
||
|
||
import 'package:fl_chart/fl_chart.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||
import 'package:nuanji_app/core/theme/app_radius.dart';
|
||
import 'package:nuanji_app/core/utils/mood_utils.dart';
|
||
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||
import 'package:nuanji_app/data/remote/api_client.dart';
|
||
import '../bloc/mood_bloc.dart';
|
||
|
||
/// 心情页面 — 今日心情卡片 + 天气选择 + 柱状图 + 统计网格 + 心情洞察
|
||
class MoodPage extends StatefulWidget {
|
||
const MoodPage({super.key});
|
||
|
||
@override
|
||
State<MoodPage> createState() => _MoodPageState();
|
||
}
|
||
|
||
class _MoodPageState extends State<MoodPage> {
|
||
late final MoodBloc _bloc;
|
||
|
||
// 天气选择状态
|
||
_WeatherType? _selectedWeather;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_bloc = MoodBloc(api: context.read<ApiClient>());
|
||
_bloc.load();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_bloc.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final colorScheme = theme.colorScheme;
|
||
|
||
return ListenableBuilder(
|
||
listenable: _bloc,
|
||
builder: (context, _) {
|
||
final state = _bloc.state;
|
||
|
||
if (state.isLoading) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
|
||
if (state.errorMessage != null) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.error_outline, size: 48, color: colorScheme.error),
|
||
const SizedBox(height: 12),
|
||
Text(state.errorMessage!, style: theme.textTheme.bodyMedium),
|
||
const SizedBox(height: 16),
|
||
FilledButton.tonal(
|
||
onPressed: _bloc.load,
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 5A: 今日心情卡片
|
||
_TodayMoodCard(stats: state.stats),
|
||
const SizedBox(height: 16),
|
||
|
||
// 5B: 天气卡片
|
||
_WeatherCard(
|
||
selectedWeather: _selectedWeather,
|
||
onWeatherSelected: (w) {
|
||
setState(() {
|
||
_selectedWeather =
|
||
_selectedWeather == w ? null : w;
|
||
});
|
||
},
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 周期选择器
|
||
_PeriodPills(
|
||
selectedPeriod: state.selectedPeriod,
|
||
onPeriodChanged: _bloc.changePeriod,
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 5C: 柱状图
|
||
_MoodBarChart(
|
||
moodCounts: state.stats.moodCounts,
|
||
selectedPeriod: state.selectedPeriod,
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
// 5D: 统计网格
|
||
_StatsGrid(stats: state.stats),
|
||
const SizedBox(height: 24),
|
||
|
||
// 5E: 心情洞察卡片
|
||
_InsightCard(stats: state.stats),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 5A: 今日心情卡片 =====
|
||
|
||
class _TodayMoodCard extends StatelessWidget {
|
||
const _TodayMoodCard({required this.stats});
|
||
|
||
final MoodStats stats;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final now = DateTime.now();
|
||
final dateStr =
|
||
'${now.year}年${now.month}月${now.day}日';
|
||
final dominantEmoji = stats.dominantMood != null
|
||
? moodToEmoji(stats.dominantMood!)
|
||
: '📝';
|
||
final dominantLabel = stats.dominantMood != null
|
||
? moodToLabel(stats.dominantMood!)
|
||
: '暂无记录';
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(24),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [AppColors.accent, AppColors.tertiary],
|
||
),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
// 装饰圆
|
||
Positioned(
|
||
right: -20,
|
||
top: -20,
|
||
child: Container(
|
||
width: 100,
|
||
height: 100,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: Colors.white.withValues(alpha: 0.12),
|
||
),
|
||
),
|
||
),
|
||
// 内容
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'今日心情 · $dateStr',
|
||
style: TextStyle(
|
||
fontFamily: 'Caveat',
|
||
fontSize: 16,
|
||
color: Colors.white.withValues(alpha: 0.85),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
children: [
|
||
Text(dominantEmoji,
|
||
style: const TextStyle(fontSize: 52)),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
dominantLabel,
|
||
style: theme.textTheme.headlineSmall
|
||
?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'共 ${stats.totalJournals} 篇日记 · 连续 ${stats.streakDays} 天',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color:
|
||
Colors.white.withValues(alpha: 0.75),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 5B: 天气卡片 =====
|
||
|
||
enum _WeatherType {
|
||
sunny('晴', '☀️'),
|
||
cloudy('多云', '⛅'),
|
||
rainy('雨', '🌧️'),
|
||
snowy('雪', '❄️'),
|
||
windy('风', '💨');
|
||
|
||
const _WeatherType(this.label, this.emoji);
|
||
final String label;
|
||
final String emoji;
|
||
}
|
||
|
||
class _WeatherCard extends StatelessWidget {
|
||
const _WeatherCard({
|
||
required this.selectedWeather,
|
||
required this.onWeatherSelected,
|
||
});
|
||
|
||
final _WeatherType? selectedWeather;
|
||
final ValueChanged<_WeatherType> onWeatherSelected;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final isDark = theme.brightness == Brightness.dark;
|
||
final surfaceColor =
|
||
isDark ? AppColors.surfaceDark : AppColors.surfaceLight;
|
||
final surfaceWarmColor = isDark
|
||
? AppColors.surfaceWarmDark
|
||
: AppColors.surfaceWarmLight;
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: surfaceColor,
|
||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'今日天气',
|
||
style: theme.textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: _WeatherType.values.map((w) {
|
||
final isSelected = selectedWeather == w;
|
||
return GestureDetector(
|
||
onTap: () => onWeatherSelected(w),
|
||
child: Container(
|
||
width: 56,
|
||
height: 56,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||
border: Border.all(
|
||
color: isSelected
|
||
? AppColors.accent
|
||
: (isDark
|
||
? AppColors.borderDark
|
||
: AppColors.borderLight),
|
||
width: 2,
|
||
),
|
||
color: isSelected
|
||
? surfaceWarmColor
|
||
: Colors.transparent,
|
||
),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(w.emoji,
|
||
style: const TextStyle(fontSize: 20)),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
w.label,
|
||
style: theme.textTheme.labelSmall
|
||
?.copyWith(fontSize: 11),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 周期选择胶囊 =====
|
||
|
||
class _PeriodPills extends StatelessWidget {
|
||
const _PeriodPills({
|
||
required this.selectedPeriod,
|
||
required this.onPeriodChanged,
|
||
});
|
||
|
||
final StatsPeriod selectedPeriod;
|
||
final ValueChanged<StatsPeriod> onPeriodChanged;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final isDark = theme.brightness == Brightness.dark;
|
||
|
||
final periods = [
|
||
(StatsPeriod.week, '7天'),
|
||
(StatsPeriod.month, '30天'),
|
||
(StatsPeriod.quarter, '3个月'),
|
||
];
|
||
|
||
return Row(
|
||
children: periods.map((p) {
|
||
final isSelected = selectedPeriod == p.$1;
|
||
return Padding(
|
||
padding: const EdgeInsets.only(right: 8),
|
||
child: GestureDetector(
|
||
onTap: () => onPeriodChanged(p.$1),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16,
|
||
vertical: 8,
|
||
),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||
color: isSelected
|
||
? AppColors.accent
|
||
: Colors.transparent,
|
||
border: Border.all(
|
||
color: isSelected
|
||
? AppColors.accent
|
||
: (isDark
|
||
? AppColors.borderDark
|
||
: AppColors.borderLight),
|
||
),
|
||
),
|
||
child: Text(
|
||
p.$2,
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
color: isSelected
|
||
? Colors.white
|
||
: (isDark
|
||
? AppColors.fg2Dark
|
||
: AppColors.fg2Light),
|
||
fontWeight:
|
||
isSelected ? FontWeight.w600 : FontWeight.normal,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 5C: 柱状图 =====
|
||
|
||
class _MoodBarChart extends StatelessWidget {
|
||
const _MoodBarChart({
|
||
required this.moodCounts,
|
||
required this.selectedPeriod,
|
||
});
|
||
|
||
final List<MoodCount> moodCounts;
|
||
final StatsPeriod selectedPeriod;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final colorScheme = theme.colorScheme;
|
||
final isDark = theme.brightness == Brightness.dark;
|
||
|
||
if (moodCounts.isEmpty) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
final maxCount = moodCounts
|
||
.fold<int>(0, (max, mc) => mc.count > max ? mc.count : max);
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? AppColors.surfaceDark
|
||
: AppColors.surfaceLight,
|
||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||
border: Border.all(
|
||
color: isDark
|
||
? AppColors.borderDark
|
||
: AppColors.borderLight,
|
||
),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'心情分布',
|
||
style: theme.textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
SizedBox(
|
||
height: 180,
|
||
child: BarChart(
|
||
BarChartData(
|
||
alignment: BarChartAlignment.spaceAround,
|
||
maxY: (maxCount + 2).toDouble(),
|
||
barGroups: moodCounts.asMap().entries.map((entry) {
|
||
final index = entry.key;
|
||
final mc = entry.value;
|
||
final color =
|
||
AppColors.moodColors[mc.mood.value] ??
|
||
colorScheme.primary;
|
||
return BarChartGroupData(
|
||
x: index,
|
||
barRods: [
|
||
BarChartRodData(
|
||
toY: mc.count.toDouble(),
|
||
color: color,
|
||
width: 14,
|
||
borderRadius: const BorderRadius.only(
|
||
topLeft: Radius.circular(4),
|
||
topRight: Radius.circular(4),
|
||
),
|
||
),
|
||
],
|
||
showingTooltipIndicators: [0],
|
||
);
|
||
}).toList(),
|
||
titlesData: FlTitlesData(
|
||
leftTitles: const AxisTitles(
|
||
sideTitles: SideTitles(showTitles: false),
|
||
),
|
||
rightTitles: const AxisTitles(
|
||
sideTitles: SideTitles(showTitles: false),
|
||
),
|
||
topTitles: const AxisTitles(
|
||
sideTitles: SideTitles(showTitles: false),
|
||
),
|
||
bottomTitles: AxisTitles(
|
||
sideTitles: SideTitles(
|
||
showTitles: true,
|
||
getTitlesWidget: (value, meta) {
|
||
final index = value.toInt();
|
||
if (index < 0 ||
|
||
index >= moodCounts.length) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 8),
|
||
child: Text(
|
||
moodToEmoji(moodCounts[index].mood),
|
||
style: const TextStyle(fontSize: 16),
|
||
),
|
||
);
|
||
},
|
||
reservedSize: 28,
|
||
),
|
||
),
|
||
),
|
||
borderData: FlBorderData(show: false),
|
||
gridData: const FlGridData(show: false),
|
||
barTouchData: BarTouchData(
|
||
touchTooltipData: BarTouchTooltipData(
|
||
getTooltipItem:
|
||
(group, groupIndex, rod, rodIndex) {
|
||
if (groupIndex >= moodCounts.length) {
|
||
return null;
|
||
}
|
||
final mc = moodCounts[groupIndex];
|
||
return BarTooltipItem(
|
||
'${moodToLabel(mc.mood)}: ${mc.count} 篇',
|
||
TextStyle(
|
||
color: isDark
|
||
? AppColors.fgDark
|
||
: AppColors.fgLight,
|
||
fontWeight: FontWeight.w600,
|
||
fontSize: 12,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== 5D: 统计网格 =====
|
||
|
||
class _StatsGrid extends StatelessWidget {
|
||
const _StatsGrid({required this.stats});
|
||
|
||
final MoodStats stats;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final isDark = theme.brightness == Brightness.dark;
|
||
|
||
// 计算好心情占比
|
||
final happyCount = stats.moodCounts
|
||
.where((mc) => mc.mood == Mood.happy)
|
||
.fold<int>(0, (sum, mc) => sum + mc.count);
|
||
final totalCount = stats.moodCounts
|
||
.fold<int>(0, (sum, mc) => sum + mc.count);
|
||
final goodPercent = totalCount > 0
|
||
? (happyCount / totalCount * 100).toStringAsFixed(0)
|
||
: '0';
|
||
|
||
// TODO: 从统计数据获取实际照片数,暂时用 0
|
||
const photoCount = 0;
|
||
|
||
final items = [
|
||
_StatItem(
|
||
emoji: '📝',
|
||
value: '${stats.totalJournals}',
|
||
description: '日记总数',
|
||
color: AppColors.secondary,
|
||
),
|
||
_StatItem(
|
||
emoji: '🔥',
|
||
value: '${stats.streakDays}',
|
||
description: '连续天数',
|
||
color: AppColors.accent,
|
||
),
|
||
_StatItem(
|
||
emoji: '😊',
|
||
value: '$goodPercent%',
|
||
description: '好心情占比',
|
||
color: AppColors.tertiary,
|
||
),
|
||
_StatItem(
|
||
emoji: '📷',
|
||
value: '$photoCount',
|
||
description: '照片数量',
|
||
color: AppColors.rose,
|
||
),
|
||
];
|
||
|
||
return GridView.count(
|
||
crossAxisCount: 2,
|
||
mainAxisSpacing: 12,
|
||
crossAxisSpacing: 12,
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
childAspectRatio: 1.4,
|
||
children: items.map((item) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? AppColors.surfaceDark
|
||
: AppColors.surfaceLight,
|
||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||
border: Border.all(
|
||
color: isDark
|
||
? AppColors.borderDark
|
||
: AppColors.borderLight,
|
||
),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(item.emoji,
|
||
style: const TextStyle(fontSize: 28)),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
item.value,
|
||
style: theme.textTheme.headlineSmall?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
color: item.color,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
item.description,
|
||
style: theme.textTheme.bodySmall?.copyWith(
|
||
fontSize: 12,
|
||
color: isDark
|
||
? AppColors.mutedDark
|
||
: AppColors.mutedLight,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _StatItem {
|
||
final String emoji;
|
||
final String value;
|
||
final String description;
|
||
final Color color;
|
||
|
||
const _StatItem({
|
||
required this.emoji,
|
||
required this.value,
|
||
required this.description,
|
||
required this.color,
|
||
});
|
||
}
|
||
|
||
// ===== 5E: 心情洞察卡片 =====
|
||
|
||
class _InsightCard extends StatelessWidget {
|
||
const _InsightCard({required this.stats});
|
||
|
||
final MoodStats stats;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final isDark = theme.brightness == Brightness.dark;
|
||
|
||
// 计算最频繁心情
|
||
final mostFrequent = stats.moodCounts.isNotEmpty
|
||
? stats.moodCounts.reduce(
|
||
(a, b) => a.count > b.count ? a : b)
|
||
: null;
|
||
|
||
// 计算心情趋势(简化:基于好心情vs坏心情比例判断)
|
||
final happyCount = stats.moodCounts
|
||
.where((mc) =>
|
||
mc.mood == Mood.happy || mc.mood == Mood.calm)
|
||
.fold<int>(0, (sum, mc) => sum + mc.count);
|
||
final sadCount = stats.moodCounts
|
||
.where((mc) =>
|
||
mc.mood == Mood.sad || mc.mood == Mood.angry)
|
||
.fold<int>(0, (sum, mc) => sum + mc.count);
|
||
final trendLabel = happyCount >= sadCount ? 'improving' : 'declining';
|
||
final trendEmoji = happyCount >= sadCount ? '📈' : '📉';
|
||
final trendText = happyCount >= sadCount ? '越来越好' : '需要关注';
|
||
|
||
final insights = [
|
||
_InsightItem(
|
||
emoji: mostFrequent != null
|
||
? moodToEmoji(mostFrequent.mood)
|
||
: '📝',
|
||
title: '最频繁心情',
|
||
detail: mostFrequent != null
|
||
? '${moodToLabel(mostFrequent.mood)} · ${mostFrequent.count} 篇'
|
||
: '暂无数据',
|
||
color: AppColors.secondary,
|
||
),
|
||
_InsightItem(
|
||
emoji: '🔥',
|
||
title: '最长连续',
|
||
detail: '${stats.streakDays} 天',
|
||
color: AppColors.accent,
|
||
),
|
||
_InsightItem(
|
||
emoji: trendEmoji,
|
||
title: '心情趋势',
|
||
detail: trendText,
|
||
color: trendLabel == 'improving'
|
||
? AppColors.secondary
|
||
: AppColors.rose,
|
||
),
|
||
];
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: isDark
|
||
? AppColors.surfaceDark
|
||
: AppColors.surfaceLight,
|
||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||
border: Border.all(
|
||
color: isDark
|
||
? AppColors.borderDark
|
||
: AppColors.borderLight,
|
||
),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'心情洞察',
|
||
style: theme.textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
...insights.map((item) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 16),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color:
|
||
item.color.withValues(alpha: 0.15),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(item.emoji,
|
||
style: const TextStyle(fontSize: 18)),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment:
|
||
CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
item.title,
|
||
style:
|
||
theme.textTheme.bodyMedium?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
item.detail,
|
||
style:
|
||
theme.textTheme.bodySmall?.copyWith(
|
||
color: isDark
|
||
? AppColors.mutedDark
|
||
: AppColors.mutedLight,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _InsightItem {
|
||
final String emoji;
|
||
final String title;
|
||
final String detail;
|
||
final Color color;
|
||
|
||
const _InsightItem({
|
||
required this.emoji,
|
||
required this.title,
|
||
required this.detail,
|
||
required this.color,
|
||
});
|
||
}
|