Files
pluv 22715f7247 feat: manage, account and help pages
- 管理页:单元集 / 计划 / 算法三个维度,卡片带元数据
- 账户页:资料、学习数据、竖直分组的四个功能入口
- 新增设置、文档与帮助(底部可上滑助手)、反馈、关于四个页面
- 抽出 StatBlock 与 GroupedListSection 共用组件
- 关闭 Android 预测式返回,转场改为 M3 水平滑入
2026-10-24 15:21:30 +08:00

447 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import '../models/catalog.dart';
/// 管理页:单元集 / 计划 / 算法三个维度。
class ManagePage extends StatelessWidget {
const ManagePage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('管理'),
bottom: const TabBar(
tabs: <Widget>[
Tab(text: '单元集'),
Tab(text: '计划'),
Tab(text: '算法'),
],
),
),
body: const TabBarView(
children: <Widget>[
_DeckTab(),
_PlanTab(),
_AlgorithmTab(),
],
),
),
);
}
}
class _DeckTab extends StatelessWidget {
const _DeckTab();
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
itemCount: DeckInfo.samples.length + 1,
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(height: 12),
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return _CountLabel(text: '共 ${DeckInfo.samples.length} 个单元集');
}
return _DeckCard(deck: DeckInfo.samples[index - 1]);
},
);
}
}
class _DeckCard extends StatelessWidget {
const _DeckCard({required this.deck});
final DeckInfo deck;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colors = theme.colorScheme;
final TextTheme text = theme.textTheme;
return _ManageCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_Badge(icon: Icons.menu_book_rounded, color: colors.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
deck.title,
style: text.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
deck.description,
style: text.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
_Chip(label: deck.algorithm),
],
),
const SizedBox(height: 14),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: deck.progress,
minHeight: 6,
backgroundColor: colors.surfaceContainerHighest,
),
),
const SizedBox(height: 10),
Row(
children: <Widget>[
Expanded(
child: _MetaText(
'${deck.activatedItems} / ${deck.totalItems} · ${deck.progressPercent}',
),
),
_MetaText('最近 ${deck.lastStudied}'),
],
),
const SizedBox(height: 6),
_MetaText('标识 ${deck.id}'),
],
),
);
}
}
class _PlanTab extends StatelessWidget {
const _PlanTab();
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
itemCount: PlanInfo.samples.length + 1,
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(height: 12),
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return _CountLabel(text: '共 ${PlanInfo.samples.length} 个计划');
}
return _PlanCard(plan: PlanInfo.samples[index - 1]);
},
);
}
}
class _PlanCard extends StatelessWidget {
const _PlanCard({required this.plan});
final PlanInfo plan;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colors = theme.colorScheme;
final TextTheme text = theme.textTheme;
return _ManageCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_Badge(icon: Icons.event_note_rounded, color: colors.tertiary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(plan.title, style: text.titleMedium),
const SizedBox(height: 2),
Text(
plan.description,
style: text.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 8),
_Chip(
label: plan.enabled ? '启用' : '停用',
emphasized: plan.enabled,
),
],
),
const SizedBox(height: 14),
Row(
children: <Widget>[
Expanded(
child: _Metric(
label: '新识记',
value: '${plan.newPerDay}',
suffix: '/ 天',
),
),
Expanded(
child: _Metric(
label: '复习上限',
value: '${plan.reviewPerDay}',
suffix: '/ 天',
),
),
],
),
const SizedBox(height: 10),
_MetaText('时段 ${plan.window}'),
],
),
);
}
}
class _AlgorithmTab extends StatelessWidget {
const _AlgorithmTab();
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
itemCount: AlgorithmInfo.samples.length + 1,
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(height: 12),
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return _CountLabel(
text: '共 ${AlgorithmInfo.samples.length} 个调度算法',
);
}
return _AlgorithmCard(algorithm: AlgorithmInfo.samples[index - 1]);
},
);
}
}
class _AlgorithmCard extends StatelessWidget {
const _AlgorithmCard({required this.algorithm});
final AlgorithmInfo algorithm;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colors = theme.colorScheme;
final TextTheme text = theme.textTheme;
return _ManageCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_Badge(icon: Icons.functions_rounded, color: colors.secondary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Text(algorithm.name, style: text.titleMedium),
const SizedBox(width: 6),
_MetaText('v${algorithm.version}'),
],
),
const SizedBox(height: 2),
Text(
algorithm.description,
style: text.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
],
),
),
if (algorithm.isDefault) ...<Widget>[
const SizedBox(width: 8),
const _Chip(label: '默认', emphasized: true),
],
],
),
const SizedBox(height: 12),
_MetaText(algorithm.parameters),
],
),
);
}
}
/// 卡片统一外观。
class _ManageCard extends StatelessWidget {
const _ManageCard({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
),
child: child,
);
}
}
class _Badge extends StatelessWidget {
const _Badge({required this.icon, required this.color});
final IconData icon;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.16),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, size: 20, color: color),
);
}
}
class _Chip extends StatelessWidget {
const _Chip({required this.label, this.emphasized = false});
final String label;
final bool emphasized;
@override
Widget build(BuildContext context) {
final ColorScheme colors = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: ShapeDecoration(
color: emphasized
? colors.primaryContainer
: colors.surfaceContainerHighest,
shape: const StadiumBorder(),
),
child: Text(
label,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: emphasized
? colors.onPrimaryContainer
: colors.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _Metric extends StatelessWidget {
const _Metric({required this.label, required this.value, this.suffix});
final String label;
final String value;
final String? suffix;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colors = theme.colorScheme;
final String? suffix = this.suffix;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 2),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
Text(
value,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
if (suffix != null) ...<Widget>[
const SizedBox(width: 4),
Text(
suffix,
style: theme.textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
),
),
],
],
),
],
);
}
}
class _MetaText extends StatelessWidget {
const _MetaText(this.value);
final String value;
@override
Widget build(BuildContext context) {
return Text(
value,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
}
class _CountLabel extends StatelessWidget {
const _CountLabel({required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
}
}