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
320 lines
10 KiB
Dart
320 lines
10 KiB
Dart
// 老师管理页面 — 创建班级 + 布置主题 + 点评日记
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:nuanji_app/core/theme/app_colors.dart';
|
|
import 'package:nuanji_app/core/theme/app_radius.dart';
|
|
import 'package:nuanji_app/data/repositories/class_repository.dart';
|
|
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
|
import 'package:nuanji_app/data/models/school_class.dart';
|
|
import '../../class_/bloc/class_bloc.dart';
|
|
|
|
/// 老师管理页面 — 教师专属功能入口
|
|
class TeacherPage extends StatelessWidget {
|
|
const TeacherPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider(
|
|
create: (context) => ClassBloc(
|
|
classRepository: context.read<ClassRepository>(),
|
|
journalRepository: context.read<JournalRepository>(),
|
|
)..add(const ClassLoadMyClasses()),
|
|
child: const _TeacherView(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TeacherView extends StatelessWidget {
|
|
const _TeacherView();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('教师管理')),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 创建班级卡片
|
|
_ActionCard(
|
|
icon: Icons.add_circle_outline,
|
|
iconColor: AppColors.accent,
|
|
title: '创建班级',
|
|
subtitle: '创建新班级并邀请学生加入',
|
|
onTap: () => _showCreateClassDialog(context),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// 布置主题卡片
|
|
_ActionCard(
|
|
icon: Icons.assignment_outlined,
|
|
iconColor: AppColors.secondary,
|
|
title: '布置主题',
|
|
subtitle: '给班级布置日记写作主题',
|
|
onTap: () => _showAssignTopicDialog(context),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// 班级码管理
|
|
_ActionCard(
|
|
icon: Icons.qr_code,
|
|
iconColor: AppColors.tertiary,
|
|
title: '班级码管理',
|
|
subtitle: '查看和重置班级码',
|
|
onTap: () {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('班级码: a1b2c3')),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// 最近点评
|
|
Text(
|
|
'快捷功能',
|
|
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_ActionCard(
|
|
icon: Icons.rate_review_outlined,
|
|
iconColor: AppColors.rose,
|
|
title: '点评日记',
|
|
subtitle: '查看学生日记并点评',
|
|
onTap: () => context.go('/class'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_ActionCard(
|
|
icon: Icons.bar_chart_outlined,
|
|
iconColor: colorScheme.primary,
|
|
title: '班级统计',
|
|
subtitle: '查看班级写作活跃度',
|
|
onTap: () => context.go('/mood'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCreateClassDialog(BuildContext context) {
|
|
final nameController = TextEditingController();
|
|
final schoolController = TextEditingController();
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text('创建班级'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(
|
|
controller: nameController,
|
|
decoration: const InputDecoration(
|
|
labelText: '班级名称',
|
|
hintText: '例如: 三年二班',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: schoolController,
|
|
decoration: const InputDecoration(
|
|
labelText: '学校名称(可选)',
|
|
hintText: '例如: 阳光小学',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
child: const Text('取消'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
if (nameController.text.trim().isNotEmpty) {
|
|
context.read<ClassBloc>().add(ClassCreate(
|
|
name: nameController.text.trim(),
|
|
schoolName: schoolController.text.trim().isEmpty
|
|
? null
|
|
: schoolController.text.trim(),
|
|
));
|
|
Navigator.pop(dialogContext);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('班级创建成功!')),
|
|
);
|
|
}
|
|
},
|
|
child: const Text('创建'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAssignTopicDialog(BuildContext context) {
|
|
final titleController = TextEditingController();
|
|
final descController = TextEditingController();
|
|
|
|
// 从 ClassBloc 获取已加载的班级列表
|
|
final classState = context.read<ClassBloc>().state;
|
|
final classes = classState is ClassListLoaded ? classState.classes : <SchoolClass>[];
|
|
|
|
// 无班级时提示先创建
|
|
if (classes.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('请先创建班级后再布置主题')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
String selectedClassId = classes.first.id;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) => StatefulBuilder(
|
|
builder: (context, setDialogState) => AlertDialog(
|
|
title: const Text('布置主题'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// 班级选择下拉框
|
|
DropdownButtonFormField<String>(
|
|
value: selectedClassId,
|
|
decoration: const InputDecoration(
|
|
labelText: '选择班级',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: classes
|
|
.map((c) => DropdownMenuItem(
|
|
value: c.id,
|
|
child: Text(c.name),
|
|
))
|
|
.toList(),
|
|
onChanged: (v) {
|
|
if (v != null) setDialogState(() => selectedClassId = v);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: titleController,
|
|
decoration: const InputDecoration(
|
|
labelText: '主题标题',
|
|
hintText: '例如: 我的周末',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: descController,
|
|
decoration: const InputDecoration(
|
|
labelText: '描述(可选)',
|
|
hintText: '主题要求和说明',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
maxLines: 3,
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
child: const Text('取消'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
if (titleController.text.trim().isNotEmpty) {
|
|
context.read<ClassBloc>().add(TopicAssign(
|
|
classId: selectedClassId,
|
|
title: titleController.text.trim(),
|
|
description: descController.text.trim().isEmpty
|
|
? null
|
|
: descController.text.trim(),
|
|
));
|
|
Navigator.pop(dialogContext);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('主题布置成功!')),
|
|
);
|
|
}
|
|
},
|
|
child: const Text('布置'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 操作卡片
|
|
class _ActionCard extends StatelessWidget {
|
|
const _ActionCard({
|
|
required this.icon,
|
|
required this.iconColor,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.onTap,
|
|
});
|
|
|
|
final IconData icon;
|
|
final Color iconColor;
|
|
final String title;
|
|
final String subtitle;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
return Card(
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: AppRadius.mdBorder,
|
|
side: BorderSide(color: colorScheme.outlineVariant),
|
|
),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: AppRadius.mdBorder,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: iconColor, size: 22),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 2),
|
|
Text(subtitle, style: theme.textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, color: colorScheme.onSurface.withValues(alpha: 0.3)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|