- 管理页:单元集 / 计划 / 算法三个维度,卡片带元数据 - 账户页:资料、学习数据、竖直分组的四个功能入口 - 新增设置、文档与帮助(底部可上滑助手)、反馈、关于四个页面 - 抽出 StatBlock 与 GroupedListSection 共用组件 - 关闭 Android 预测式返回,转场改为 M3 水平滑入
447 lines
12 KiB
Dart
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,
|
|
),
|
|
);
|
|
}
|
|
}
|