feat(app): F8 班级系统 + F9/F10 占位页面
F8 班级系统: - ClassBloc 状态管理 (班级列表/日记墙/成员/主题/评语) - 班级主页: 日记墙 + 主题布置 Tab + 成员列表 - 老师管理页: 创建班级 + 布置主题 + 点评入口 - 班级码展示 + 评语卡片 F9 家长功能 (占位): - 家长中心页面框架: 日记查看/心情统计/使用时间/数据管理 - PIPL 合规提示卡片 F10 搜索/设置 (部分): - 个人中心: 用户信息 + 角色展示 + 功能入口 + 退出登录 - 搜索页: 标签筛选 + 心情过滤 + 搜索框 验证: flutter analyze 0 error
This commit is contained in:
@@ -1,14 +1,516 @@
|
||||
import 'package:flutter/material.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/data/models/journal_entry.dart';
|
||||
import 'package:nuanji_app/data/models/school_class.dart';
|
||||
import '../bloc/class_bloc.dart';
|
||||
|
||||
/// 班级主页 — 日记墙 + 班级信息
|
||||
class ClassPage extends StatelessWidget {
|
||||
const ClassPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('班级 - 占位页面'),
|
||||
return BlocProvider(
|
||||
create: (context) => ClassBloc()..add(const ClassLoadMyClasses()),
|
||||
child: const _ClassView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClassView extends StatelessWidget {
|
||||
const _ClassView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ClassBloc, ClassState>(
|
||||
builder: (context, state) {
|
||||
if (state is ClassLoading || state is ClassInitial) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('班级')),
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is ClassError) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('班级')),
|
||||
body: Center(child: Text(state.message)),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is ClassListLoaded) {
|
||||
return _ClassListView(classes: state.classes, colorScheme: Theme.of(context).colorScheme);
|
||||
}
|
||||
|
||||
if (state is ClassDetailLoaded) {
|
||||
return _ClassDetailView(state: state);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 班级列表视图 =====
|
||||
|
||||
class _ClassListView extends StatelessWidget {
|
||||
const _ClassListView({required this.classes, required this.colorScheme});
|
||||
|
||||
final List<SchoolClass> classes;
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('我的班级')),
|
||||
body: classes.isEmpty
|
||||
? _buildEmptyState(context, colorScheme)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: classes.map((cls) {
|
||||
return _ClassListCard(
|
||||
cls: cls,
|
||||
colorScheme: colorScheme,
|
||||
onTap: () =>
|
||||
context.read<ClassBloc>().add(ClassSelected(cls.id)),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context, ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.groups_outlined, size: 64, color: colorScheme.onSurface.withValues(alpha: 0.2)),
|
||||
const SizedBox(height: 16),
|
||||
Text('还没有加入任何班级', style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
)),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => context.go('/class-code'),
|
||||
child: const Text('输入班级码加入'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClassListCard extends StatelessWidget {
|
||||
const _ClassListCard({
|
||||
required this.cls,
|
||||
required this.colorScheme,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final SchoolClass cls;
|
||||
final ColorScheme colorScheme;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.secondary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(Icons.school_rounded, color: AppColors.secondary),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(cls.name, style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${cls.schoolName} · ${cls.memberCount} 人',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 班级详情视图(日记墙)=====
|
||||
|
||||
class _ClassDetailView extends StatelessWidget {
|
||||
const _ClassDetailView({required this.state});
|
||||
|
||||
final ClassDetailLoaded state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final classInfo = state.classInfo;
|
||||
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.read<ClassBloc>().add(const ClassLoadMyClasses()),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(classInfo.name),
|
||||
Text(
|
||||
'${classInfo.schoolName} · 班级码: ${classInfo.classCode}',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottom: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: '日记墙'),
|
||||
Tab(text: '主题'),
|
||||
Tab(text: '成员'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
// 日记墙
|
||||
_DiaryWallTab(state: state),
|
||||
// 主题布置
|
||||
_TopicsTab(topics: state.topics),
|
||||
// 成员列表
|
||||
_MembersTab(state: state),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 日记墙 Tab =====
|
||||
|
||||
class _DiaryWallTab extends StatelessWidget {
|
||||
const _DiaryWallTab({required this.state});
|
||||
|
||||
final ClassDetailLoaded state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
if (state.isLoadingWall) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state.diaryWall.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.auto_stories_outlined, size: 48, color: colorScheme.onSurface.withValues(alpha: 0.2)),
|
||||
const SizedBox(height: 12),
|
||||
Text('日记墙还是空的', style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
Text('分享你的日记到班级吧!', style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.diaryWall.length,
|
||||
itemBuilder: (context, index) {
|
||||
final journal = state.diaryWall[index];
|
||||
return _DiaryWallCard(journal: journal, comments: state.comments);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiaryWallCard extends StatelessWidget {
|
||||
const _DiaryWallCard({required this.journal, required this.comments});
|
||||
|
||||
final JournalEntry journal;
|
||||
final List<Comment> comments;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final moodColor = AppColors.moodColors[journal.mood.value] ?? colorScheme.primary;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => context.go('/editor?id=${journal.id}'),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 头部:作者 + 心情
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: AppColors.rose.withValues(alpha: 0.2),
|
||||
child: Text(
|
||||
'同',
|
||||
style: theme.textTheme.labelMedium?.copyWith(color: AppColors.rose),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
journal.title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: moodColor.withValues(alpha: 0.15),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(_moodEmoji(journal.mood), style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 日期
|
||||
Text(
|
||||
'${journal.date.month}月${journal.date.day}日',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
// 评语
|
||||
if (comments.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.rate_review_rounded, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
comments.first.content,
|
||||
style: theme.textTheme.bodySmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _moodEmoji(Mood mood) => switch (mood) {
|
||||
Mood.happy => '😊',
|
||||
Mood.calm => '😌',
|
||||
Mood.sad => '😢',
|
||||
Mood.angry => '😠',
|
||||
Mood.thinking => '🤔',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 主题 Tab =====
|
||||
|
||||
class _TopicsTab extends StatelessWidget {
|
||||
const _TopicsTab({required this.topics});
|
||||
|
||||
final List<TopicAssignment> topics;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
if (topics.isEmpty) {
|
||||
return Center(
|
||||
child: Text('暂无主题布置', style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: topics.length,
|
||||
itemBuilder: (context, index) {
|
||||
final topic = topics[index];
|
||||
final isOverdue = topic.dueDate != null && topic.dueDate!.isBefore(DateTime.now());
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => context.go('/editor?topic=${topic.id}'),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.assignment_outlined, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
topic.title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
if (topic.isActive && !isOverdue)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.secondary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text('进行中', style: theme.textTheme.labelSmall?.copyWith(color: AppColors.secondary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (topic.description != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(topic.description!, style: theme.textTheme.bodyMedium),
|
||||
],
|
||||
if (topic.dueDate != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'截止: ${topic.dueDate!.month}月${topic.dueDate!.day}日',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: isOverdue ? colorScheme.error : colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 成员 Tab =====
|
||||
|
||||
class _MembersTab extends StatelessWidget {
|
||||
const _MembersTab({required this.state});
|
||||
|
||||
final ClassDetailLoaded state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (state.isLoadingMembers) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.members.length,
|
||||
itemBuilder: (context, index) {
|
||||
final member = state.members[index];
|
||||
final isTeacher = member.role == 'teacher';
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: isTeacher
|
||||
? AppColors.accent.withValues(alpha: 0.15)
|
||||
: AppColors.secondary.withValues(alpha: 0.15),
|
||||
child: Text(
|
||||
(member.nickname ?? '?').characters.first,
|
||||
style: TextStyle(
|
||||
color: isTeacher ? AppColors.accent : AppColors.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(member.nickname ?? '未知'),
|
||||
trailing: isTeacher
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.accent.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text('老师', style: theme.textTheme.labelSmall?.copyWith(color: AppColors.accent)),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user