import 'package:flutter/material.dart'; import '../models/study_plan.dart'; /// 学习首页。 /// /// 信息层级刻意保持简单,和百词斩首页同构: /// 当前材料 → 今日任务(要做什么 + 要多久)→ 唯一主行动 → 快捷入口。 /// 但去掉它的商业化部分:没有广告位、没有课程倒计时、没有电商入口。 class StudyPage extends StatelessWidget { const StudyPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('学习')), body: ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), children: const [ _DeckCard(plan: StudyPlan.sample), SizedBox(height: 14), _TodayPlanCard(plan: StudyPlan.sample), SizedBox(height: 18), _QuickActions(), ], ), ); } } /// 当前单元集:名称、算法、完成度。 class _DeckCard extends StatelessWidget { const _DeckCard({required this.plan}); final StudyPlan plan; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colors = theme.colorScheme; final TextTheme text = theme.textTheme; return _SurfaceCard( onTap: () => _pending(context, '单元集切换'), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: colors.primaryContainer, borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.menu_book_rounded, color: colors.onPrimaryContainer, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( plan.deckName, style: text.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( '调度算法 ${plan.algorithm}', style: text.bodySmall?.copyWith( color: colors.onSurfaceVariant, ), ), ], ), ), Icon( Icons.unfold_more_rounded, size: 20, color: colors.onSurfaceVariant, ), ], ), const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: plan.progress, minHeight: 8, backgroundColor: colors.surfaceContainerHighest, ), ), const SizedBox(height: 8), Row( children: [ Text( '${plan.activatedItems} / ${plan.totalItems}', style: text.bodySmall?.copyWith(color: colors.onSurfaceVariant), ), const Spacer(), Text( plan.progressPercent, style: text.bodySmall?.copyWith( color: colors.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), ], ), ], ), ); } } /// 今日任务:两个数字 + 时长承诺 + 唯一主行动。 class _TodayPlanCard extends StatelessWidget { const _TodayPlanCard({required this.plan}); final StudyPlan plan; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colors = theme.colorScheme; final TextTheme text = theme.textTheme; return _SurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('今日计划', style: text.titleMedium), const SizedBox(height: 18), Row( children: [ Expanded(child: _BigStat(label: '待复习', value: plan.dueItems)), Expanded(child: _BigStat(label: '待新识', value: plan.newItems)), ], ), const SizedBox(height: 16), Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.schedule_rounded, size: 16, color: colors.onSurfaceVariant, ), const SizedBox(width: 6), Text( plan.estimatedLabel, style: text.bodyMedium?.copyWith( color: colors.onSurfaceVariant, ), ), ], ), ), const SizedBox(height: 16), FilledButton( onPressed: plan.isEmpty ? null : () => _pending(context, '学习会话'), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), textStyle: text.titleMedium, ), child: Text(plan.isEmpty ? '今天已完成' : '开始学习'), ), ], ), ); } } class _BigStat extends StatelessWidget { const _BigStat({required this.label, required this.value}); final String label; final int value; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colors = theme.colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: theme.textTheme.bodyMedium?.copyWith( color: colors.onSurfaceVariant, ), ), const SizedBox(height: 2), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( '$value', style: theme.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.w700, height: 1.1, ), ), const SizedBox(width: 6), Text( '条', style: theme.textTheme.bodySmall?.copyWith( color: colors.onSurfaceVariant, ), ), ], ), ], ); } } /// 快捷入口。都是「学习域」内的次级入口,维护类操作不放这里。 class _QuickActions extends StatelessWidget { const _QuickActions(); static const List<(IconData, String)> _items = <(IconData, String)>[ (Icons.star_border_rounded, '收藏夹'), (Icons.volume_up_outlined, '语音缓存'), (Icons.history_rounded, '最近学习'), (Icons.replay_rounded, '难点重练'), ]; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colors = theme.colorScheme; return Row( children: [ for (final (int index, (IconData icon, String label)) in _items.indexed) Expanded( child: Padding( padding: EdgeInsets.only(left: index == 0 ? 0 : 8), child: Material( color: colors.surfaceContainerLow, borderRadius: BorderRadius.circular(16), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () => _pending(context, label), child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Column( children: [ Icon(icon, size: 22, color: colors.primary), const SizedBox(height: 8), Text( label, style: theme.textTheme.labelSmall?.copyWith( color: colors.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ), ), ), ), ], ); } } /// 统一的卡片外观,避免每处重复写圆角和底色。 class _SurfaceCard extends StatelessWidget { const _SurfaceCard({required this.child, this.onTap}); final Widget child; /// 可点击时给出水波纹反馈;为 null 时是纯展示卡片。 final VoidCallback? onTap; @override Widget build(BuildContext context) { final BorderRadius radius = BorderRadius.circular(20); return Material( color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: radius, clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, borderRadius: radius, child: Padding(padding: const EdgeInsets.all(18), child: child), ), ); } } /// 尚未接入内核的入口,点击给出明确反馈而不是静默无响应。 void _pending(BuildContext context, String feature) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$feature 尚未接入内核'), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.fromLTRB(16, 0, 16, 88), ), ); }