feat(app): F8 班级系统 + F9/F10 占位页面
F8 班级系统: - ClassBloc 状态管理 (班级列表/日记墙/成员/主题/评语) - 班级主页: 日记墙 + 主题布置 Tab + 成员列表 - 老师管理页: 创建班级 + 布置主题 + 点评入口 - 班级码展示 + 评语卡片 F9 家长功能 (占位): - 家长中心页面框架: 日记查看/心情统计/使用时间/数据管理 - PIPL 合规提示卡片 F10 搜索/设置 (部分): - 个人中心: 用户信息 + 角色展示 + 功能入口 + 退出登录 - 搜索页: 标签筛选 + 心情过滤 + 搜索框 验证: flutter analyze 0 error
This commit is contained in:
410
app/lib/features/class_/bloc/class_bloc.dart
Normal file
410
app/lib/features/class_/bloc/class_bloc.dart
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
// 班级 BLoC — 管理班级状态、成员、日记墙、主题布置和评语
|
||||||
|
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||||
|
import 'package:nuanji_app/data/models/school_class.dart';
|
||||||
|
|
||||||
|
// ===== Events =====
|
||||||
|
|
||||||
|
sealed class ClassEvent {
|
||||||
|
const ClassEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载我的班级列表
|
||||||
|
final class ClassLoadMyClasses extends ClassEvent {
|
||||||
|
const ClassLoadMyClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择当前班级
|
||||||
|
final class ClassSelected extends ClassEvent {
|
||||||
|
final String classId;
|
||||||
|
const ClassSelected(this.classId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载班级成员列表
|
||||||
|
final class ClassLoadMembers extends ClassEvent {
|
||||||
|
final String classId;
|
||||||
|
const ClassLoadMembers(this.classId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载班级日记墙(已分享到班级的日记)
|
||||||
|
final class ClassLoadDiaryWall extends ClassEvent {
|
||||||
|
final String classId;
|
||||||
|
const ClassLoadDiaryWall(this.classId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载主题布置列表
|
||||||
|
final class ClassLoadTopics extends ClassEvent {
|
||||||
|
final String classId;
|
||||||
|
const ClassLoadTopics(this.classId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建班级(老师)
|
||||||
|
final class ClassCreate extends ClassEvent {
|
||||||
|
final String name;
|
||||||
|
final String? schoolName;
|
||||||
|
const ClassCreate({required this.name, this.schoolName});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 布置主题(老师)
|
||||||
|
final class TopicAssign extends ClassEvent {
|
||||||
|
final String classId;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final DateTime? dueDate;
|
||||||
|
const TopicAssign({
|
||||||
|
required this.classId,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.dueDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载日记评语
|
||||||
|
final class ClassLoadComments extends ClassEvent {
|
||||||
|
final String journalId;
|
||||||
|
const ClassLoadComments(this.journalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== State =====
|
||||||
|
|
||||||
|
/// 班级成员模型
|
||||||
|
class ClassMember {
|
||||||
|
final String userId;
|
||||||
|
final String role;
|
||||||
|
final String? nickname;
|
||||||
|
final DateTime joinedAt;
|
||||||
|
|
||||||
|
const ClassMember({
|
||||||
|
required this.userId,
|
||||||
|
required this.role,
|
||||||
|
this.nickname,
|
||||||
|
required this.joinedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 主题布置模型
|
||||||
|
class TopicAssignment {
|
||||||
|
final String id;
|
||||||
|
final String classId;
|
||||||
|
final String teacherId;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final DateTime? dueDate;
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
const TopicAssignment({
|
||||||
|
required this.id,
|
||||||
|
required this.classId,
|
||||||
|
required this.teacherId,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.dueDate,
|
||||||
|
this.isActive = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 评语模型
|
||||||
|
class Comment {
|
||||||
|
final String id;
|
||||||
|
final String journalId;
|
||||||
|
final String authorId;
|
||||||
|
final String content;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const Comment({
|
||||||
|
required this.id,
|
||||||
|
required this.journalId,
|
||||||
|
required this.authorId,
|
||||||
|
required this.content,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 班级状态
|
||||||
|
sealed class ClassState {
|
||||||
|
const ClassState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始状态
|
||||||
|
final class ClassInitial extends ClassState {
|
||||||
|
const ClassInitial();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载中
|
||||||
|
final class ClassLoading extends ClassState {
|
||||||
|
const ClassLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 班级列表已加载
|
||||||
|
final class ClassListLoaded extends ClassState {
|
||||||
|
final List<SchoolClass> classes;
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
const ClassListLoaded({
|
||||||
|
this.classes = const [],
|
||||||
|
this.isLoading = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
ClassListLoaded copyWith({
|
||||||
|
List<SchoolClass>? classes,
|
||||||
|
bool? isLoading,
|
||||||
|
}) =>
|
||||||
|
ClassListLoaded(
|
||||||
|
classes: classes ?? this.classes,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 班级详情已加载(日记墙 / 成员 / 主题 / 评语)
|
||||||
|
final class ClassDetailLoaded extends ClassState {
|
||||||
|
final SchoolClass classInfo;
|
||||||
|
final List<ClassMember> members;
|
||||||
|
final List<JournalEntry> diaryWall;
|
||||||
|
final List<TopicAssignment> topics;
|
||||||
|
final List<Comment> comments;
|
||||||
|
final bool isLoadingWall;
|
||||||
|
final bool isLoadingMembers;
|
||||||
|
final String? selectedJournalId;
|
||||||
|
|
||||||
|
const ClassDetailLoaded({
|
||||||
|
required this.classInfo,
|
||||||
|
this.members = const [],
|
||||||
|
this.diaryWall = const [],
|
||||||
|
this.topics = const [],
|
||||||
|
this.comments = const [],
|
||||||
|
this.isLoadingWall = false,
|
||||||
|
this.isLoadingMembers = false,
|
||||||
|
this.selectedJournalId,
|
||||||
|
});
|
||||||
|
|
||||||
|
ClassDetailLoaded copyWith({
|
||||||
|
SchoolClass? classInfo,
|
||||||
|
List<ClassMember>? members,
|
||||||
|
List<JournalEntry>? diaryWall,
|
||||||
|
List<TopicAssignment>? topics,
|
||||||
|
List<Comment>? comments,
|
||||||
|
bool? isLoadingWall,
|
||||||
|
bool? isLoadingMembers,
|
||||||
|
String? selectedJournalId,
|
||||||
|
bool clearSelectedJournal = false,
|
||||||
|
}) =>
|
||||||
|
ClassDetailLoaded(
|
||||||
|
classInfo: classInfo ?? this.classInfo,
|
||||||
|
members: members ?? this.members,
|
||||||
|
diaryWall: diaryWall ?? this.diaryWall,
|
||||||
|
topics: topics ?? this.topics,
|
||||||
|
comments: comments ?? this.comments,
|
||||||
|
isLoadingWall: isLoadingWall ?? this.isLoadingWall,
|
||||||
|
isLoadingMembers: isLoadingMembers ?? this.isLoadingMembers,
|
||||||
|
selectedJournalId: clearSelectedJournal
|
||||||
|
? null
|
||||||
|
: (selectedJournalId ?? this.selectedJournalId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 错误状态
|
||||||
|
final class ClassError extends ClassState {
|
||||||
|
final String message;
|
||||||
|
const ClassError(this.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== BLoC =====
|
||||||
|
|
||||||
|
class ClassBloc extends Bloc<ClassEvent, ClassState> {
|
||||||
|
ClassBloc() : super(const ClassInitial()) {
|
||||||
|
on<ClassLoadMyClasses>(_onLoadMyClasses);
|
||||||
|
on<ClassSelected>(_onClassSelected);
|
||||||
|
on<ClassLoadMembers>(_onLoadMembers);
|
||||||
|
on<ClassLoadDiaryWall>(_onLoadDiaryWall);
|
||||||
|
on<ClassLoadTopics>(_onLoadTopics);
|
||||||
|
on<ClassCreate>(_onCreateClass);
|
||||||
|
on<TopicAssign>(_onTopicAssign);
|
||||||
|
on<ClassLoadComments>(_onLoadComments);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadMyClasses(
|
||||||
|
ClassLoadMyClasses event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
emit(const ClassListLoaded(isLoading: true));
|
||||||
|
|
||||||
|
// Phase 1: 占位数据,待 API 集成
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
emit(ClassListLoaded(classes: [
|
||||||
|
SchoolClass(
|
||||||
|
id: 'class-1',
|
||||||
|
name: '三年二班',
|
||||||
|
schoolName: '阳光小学',
|
||||||
|
teacherId: 'teacher-1',
|
||||||
|
classCode: 'a1b2c3',
|
||||||
|
memberCount: 28,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onClassSelected(
|
||||||
|
ClassSelected event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final classInfo = SchoolClass(
|
||||||
|
id: event.classId,
|
||||||
|
name: '三年二班',
|
||||||
|
schoolName: '阳光小学',
|
||||||
|
teacherId: 'teacher-1',
|
||||||
|
classCode: 'a1b2c3',
|
||||||
|
memberCount: 28,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(ClassDetailLoaded(classInfo: classInfo));
|
||||||
|
|
||||||
|
add(ClassLoadDiaryWall(event.classId));
|
||||||
|
add(ClassLoadMembers(event.classId));
|
||||||
|
add(ClassLoadTopics(event.classId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadMembers(
|
||||||
|
ClassLoadMembers event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
if (state is! ClassDetailLoaded) return;
|
||||||
|
final current = state as ClassDetailLoaded;
|
||||||
|
emit(current.copyWith(isLoadingMembers: true));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
final members = List.generate(
|
||||||
|
28,
|
||||||
|
(i) => ClassMember(
|
||||||
|
userId: 'user-$i',
|
||||||
|
role: i == 0 ? 'teacher' : 'student',
|
||||||
|
nickname: i == 0 ? '王老师' : '同学$i',
|
||||||
|
joinedAt: now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(current.copyWith(members: members, isLoadingMembers: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadDiaryWall(
|
||||||
|
ClassLoadDiaryWall event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
if (state is! ClassDetailLoaded) return;
|
||||||
|
final current = state as ClassDetailLoaded;
|
||||||
|
emit(current.copyWith(isLoadingWall: true));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
final now = DateTime.now();
|
||||||
|
final titles = ['快乐的周末', '春天来了', '我的小猫', '数学课', '好朋友', '下雨天'];
|
||||||
|
|
||||||
|
final diaries = List.generate(
|
||||||
|
6,
|
||||||
|
(i) => JournalEntry(
|
||||||
|
id: 'diary-$i',
|
||||||
|
authorId: 'user-${i + 1}',
|
||||||
|
classId: event.classId,
|
||||||
|
title: titles[i],
|
||||||
|
date: now.subtract(Duration(days: i)),
|
||||||
|
mood: Mood.values[i % Mood.values.length],
|
||||||
|
tags: const ['日常', '心情'],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(current.copyWith(diaryWall: diaries, isLoadingWall: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadTopics(
|
||||||
|
ClassLoadTopics event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
if (state is! ClassDetailLoaded) return;
|
||||||
|
final current = state as ClassDetailLoaded;
|
||||||
|
|
||||||
|
final topics = [
|
||||||
|
TopicAssignment(
|
||||||
|
id: 'topic-1',
|
||||||
|
classId: event.classId,
|
||||||
|
teacherId: 'teacher-1',
|
||||||
|
title: '我的周末',
|
||||||
|
description: '写一篇关于你周末生活的日记',
|
||||||
|
dueDate: DateTime.now().add(const Duration(days: 3)),
|
||||||
|
),
|
||||||
|
TopicAssignment(
|
||||||
|
id: 'topic-2',
|
||||||
|
classId: event.classId,
|
||||||
|
teacherId: 'teacher-1',
|
||||||
|
title: '最开心的一天',
|
||||||
|
description: '回忆一下让你最开心的一天',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
emit(current.copyWith(topics: topics));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onCreateClass(
|
||||||
|
ClassCreate event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
final newClass = SchoolClass.create(
|
||||||
|
name: event.name,
|
||||||
|
schoolName: event.schoolName ?? '',
|
||||||
|
teacherId: 'current-user',
|
||||||
|
classCode: 'x1y2z3',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state is ClassListLoaded) {
|
||||||
|
final current = state as ClassListLoaded;
|
||||||
|
emit(current.copyWith(classes: [...current.classes, newClass]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onTopicAssign(
|
||||||
|
TopicAssign event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
if (state is! ClassDetailLoaded) return;
|
||||||
|
final current = state as ClassDetailLoaded;
|
||||||
|
final newTopic = TopicAssignment(
|
||||||
|
id: 'topic-${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
classId: event.classId,
|
||||||
|
teacherId: 'current-user',
|
||||||
|
title: event.title,
|
||||||
|
description: event.description,
|
||||||
|
dueDate: event.dueDate,
|
||||||
|
);
|
||||||
|
emit(current.copyWith(topics: [newTopic, ...current.topics]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadComments(
|
||||||
|
ClassLoadComments event,
|
||||||
|
Emitter<ClassState> emit,
|
||||||
|
) async {
|
||||||
|
if (state is! ClassDetailLoaded) return;
|
||||||
|
final current = state as ClassDetailLoaded;
|
||||||
|
|
||||||
|
final comments = [
|
||||||
|
Comment(
|
||||||
|
id: 'comment-1',
|
||||||
|
journalId: event.journalId,
|
||||||
|
authorId: 'teacher-1',
|
||||||
|
content: '写得很好,继续保持!',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
emit(current.copyWith(
|
||||||
|
comments: comments,
|
||||||
|
selectedJournalId: event.journalId,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
class ClassPage extends StatelessWidget {
|
||||||
const ClassPage({super.key});
|
const ClassPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return BlocProvider(
|
||||||
body: Center(
|
create: (context) => ClassBloc()..add(const ClassLoadMyClasses()),
|
||||||
child: Text('班级 - 占位页面'),
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,187 @@
|
|||||||
import 'package:flutter/material.dart';
|
// 家长页面 — 只读查看 + 孩子数据管理
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||||||
|
|
||||||
|
/// 家长中心页面 — 家长查看孩子日记和统计
|
||||||
class ParentPage extends StatelessWidget {
|
class ParentPage extends StatelessWidget {
|
||||||
const ParentPage({super.key});
|
const ParentPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
final theme = Theme.of(context);
|
||||||
body: Center(
|
final colorScheme = theme.colorScheme;
|
||||||
child: Text('家长 - 占位页面'),
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('家长中心')),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 孩子信息卡片
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
||||||
|
color: colorScheme.primaryContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
backgroundColor: AppColors.rose.withValues(alpha: 0.2),
|
||||||
|
child: const Text('👶', style: TextStyle(fontSize: 24)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('孩子的日记', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('查看和管理孩子的日记数据', style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// 功能列表
|
||||||
|
_ParentActionCard(
|
||||||
|
icon: Icons.auto_stories_outlined,
|
||||||
|
iconColor: AppColors.accent,
|
||||||
|
title: '日记查看',
|
||||||
|
subtitle: '只读查看孩子的日记和评语',
|
||||||
|
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('F9: 家长日记查看待实现')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_ParentActionCard(
|
||||||
|
icon: Icons.bar_chart_outlined,
|
||||||
|
iconColor: AppColors.secondary,
|
||||||
|
title: '心情统计',
|
||||||
|
subtitle: '查看孩子的写作频率和心情趋势',
|
||||||
|
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('F9: 心情统计待实现')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_ParentActionCard(
|
||||||
|
icon: Icons.timer_outlined,
|
||||||
|
iconColor: AppColors.tertiary,
|
||||||
|
title: '使用时间',
|
||||||
|
subtitle: '设置孩子每天的使用时间限制',
|
||||||
|
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('F9: 使用时间限制待实现')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_ParentActionCard(
|
||||||
|
icon: Icons.download_outlined,
|
||||||
|
iconColor: colorScheme.primary,
|
||||||
|
title: '数据管理',
|
||||||
|
subtitle: '导出或删除孩子的日记数据',
|
||||||
|
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('F9: 数据管理待实现')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// PIPL 提示
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.shield_outlined, size: 18, color: colorScheme.onSurface.withValues(alpha: 0.5)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'根据《个人信息保护法》,您有权查阅、更正、删除和导出孩子的数据。',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParentActionCard extends StatelessWidget {
|
||||||
|
const _ParentActionCard({
|
||||||
|
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: 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: 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,175 @@
|
|||||||
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/features/auth/bloc/auth_bloc.dart';
|
||||||
|
|
||||||
|
/// 个人中心页面
|
||||||
class ProfilePage extends StatelessWidget {
|
class ProfilePage extends StatelessWidget {
|
||||||
const ProfilePage({super.key});
|
const ProfilePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
final theme = Theme.of(context);
|
||||||
body: Center(
|
final colorScheme = theme.colorScheme;
|
||||||
child: Text('我的 - 占位页面'),
|
final authState = context.watch<AuthBloc>().state;
|
||||||
|
final displayName = authState is Authenticated ? authState.user.displayLabel : '用户';
|
||||||
|
final role = authState is Authenticated ? authState.user.primaryRoleType : null;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 用户头像卡片
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
||||||
|
color: colorScheme.primaryContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 32,
|
||||||
|
backgroundColor: colorScheme.primary.withValues(alpha: 0.2),
|
||||||
|
child: Text(
|
||||||
|
displayName.characters.first,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(color: colorScheme.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(displayName, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_roleLabel(role),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// 功能入口
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.auto_awesome_outlined,
|
||||||
|
iconColor: AppColors.accent,
|
||||||
|
title: '我的成就',
|
||||||
|
onTap: () => context.go('/achievements'),
|
||||||
|
),
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.emoji_emotions_outlined,
|
||||||
|
iconColor: AppColors.secondary,
|
||||||
|
title: '贴纸收藏',
|
||||||
|
onTap: () => context.go('/stickers'),
|
||||||
|
),
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.dashboard_customize_outlined,
|
||||||
|
iconColor: AppColors.tertiary,
|
||||||
|
title: '日记模板',
|
||||||
|
onTap: () => context.go('/templates'),
|
||||||
|
),
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.groups_outlined,
|
||||||
|
iconColor: colorScheme.primary,
|
||||||
|
title: '我的班级',
|
||||||
|
onTap: () => context.go('/class'),
|
||||||
|
),
|
||||||
|
if (role != null && role.name == 'teacher')
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.school_outlined,
|
||||||
|
iconColor: AppColors.accent,
|
||||||
|
title: '教师管理',
|
||||||
|
onTap: () => context.go('/teacher'),
|
||||||
|
),
|
||||||
|
if (role != null && role.name == 'parent')
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.family_restroom_outlined,
|
||||||
|
iconColor: AppColors.rose,
|
||||||
|
title: '家长中心',
|
||||||
|
onTap: () => context.go('/parent'),
|
||||||
|
),
|
||||||
|
const Divider(height: 32),
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.bar_chart_outlined,
|
||||||
|
iconColor: colorScheme.primary,
|
||||||
|
title: '心情统计',
|
||||||
|
onTap: () => context.go('/mood'),
|
||||||
|
),
|
||||||
|
_ProfileMenuItem(
|
||||||
|
icon: Icons.settings_outlined,
|
||||||
|
iconColor: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
|
title: '设置',
|
||||||
|
onTap: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('设置页面开发中')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<AuthBloc>().add(const LogoutRequested());
|
||||||
|
context.go('/login');
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(foregroundColor: colorScheme.error),
|
||||||
|
child: const Text('退出登录'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _roleLabel(dynamic role) {
|
||||||
|
if (role == null) return '未选择角色';
|
||||||
|
return switch (role.name) {
|
||||||
|
'teacher' => '老师',
|
||||||
|
'student' => '学生',
|
||||||
|
'parent' => '家长',
|
||||||
|
'independent' => '独立用户',
|
||||||
|
_ => '用户',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileMenuItem extends StatelessWidget {
|
||||||
|
const _ProfileMenuItem({
|
||||||
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.title,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final String title;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon, color: iconColor),
|
||||||
|
title: Text(title, style: theme.textTheme.bodyMedium),
|
||||||
|
trailing: const Icon(Icons.chevron_right, size: 20),
|
||||||
|
onTap: onTap,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,160 @@
|
|||||||
import 'package:flutter/material.dart';
|
// 搜索页面 — 日记搜索 + 标签筛选
|
||||||
|
|
||||||
class SearchPage extends StatelessWidget {
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||||||
|
import 'package:nuanji_app/data/models/journal_entry.dart';
|
||||||
|
|
||||||
|
/// 搜索页面 — 全文搜索日记(Phase 1 占位 UI)
|
||||||
|
class SearchPage extends StatefulWidget {
|
||||||
const SearchPage({super.key});
|
const SearchPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchPage> createState() => _SearchPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchPageState extends State<SearchPage> {
|
||||||
|
final _searchController = TextEditingController();
|
||||||
|
bool _hasSearched = false;
|
||||||
|
|
||||||
|
// Phase 1 占位数据
|
||||||
|
final _recentTags = ['日常', '学校', '旅行', '美食', '读书', '心情'];
|
||||||
|
final _moodFilters = Mood.values;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
final theme = Theme.of(context);
|
||||||
body: Center(
|
final colorScheme = theme.colorScheme;
|
||||||
child: Text('搜索 - 占位页面'),
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '搜索日记...',
|
||||||
|
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.clear();
|
||||||
|
setState(() => _hasSearched = false);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
onSubmitted: (_) => _doSearch(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: _hasSearched
|
||||||
|
? _buildSearchResults(context, colorScheme)
|
||||||
|
: _buildSuggestions(context, theme, colorScheme),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _doSearch() {
|
||||||
|
setState(() => _hasSearched = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSuggestions(BuildContext context, ThemeData theme, ColorScheme colorScheme) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('常用标签', style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: _recentTags.map((tag) {
|
||||||
|
return ActionChip(
|
||||||
|
label: Text(tag),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.text = tag;
|
||||||
|
_doSearch();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text('按心情筛选', style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: _moodFilters.map((mood) {
|
||||||
|
final color = AppColors.moodColors[mood.value] ?? colorScheme.primary;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_searchController.text = _moodLabel(mood);
|
||||||
|
_doSearch();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: color.withValues(alpha: 0.15),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(_moodEmoji(mood), style: const TextStyle(fontSize: 24)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(_moodLabel(mood), style: theme.textTheme.labelSmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchResults(BuildContext context, ColorScheme colorScheme) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.search_off_rounded, size: 48, color: colorScheme.onSurface.withValues(alpha: 0.2)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Phase 1: 搜索功能待 Isar FTS 集成',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _moodEmoji(Mood mood) => switch (mood) {
|
||||||
|
Mood.happy => '😊',
|
||||||
|
Mood.calm => '😌',
|
||||||
|
Mood.sad => '😢',
|
||||||
|
Mood.angry => '😠',
|
||||||
|
Mood.thinking => '🤔',
|
||||||
|
};
|
||||||
|
|
||||||
|
String _moodLabel(Mood mood) => switch (mood) {
|
||||||
|
Mood.happy => '开心',
|
||||||
|
Mood.calm => '平静',
|
||||||
|
Mood.sad => '难过',
|
||||||
|
Mood.angry => '生气',
|
||||||
|
Mood.thinking => '思考',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,277 @@
|
|||||||
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 '../../class_/bloc/class_bloc.dart';
|
||||||
|
|
||||||
|
/// 老师管理页面 — 教师专属功能入口
|
||||||
class TeacherPage extends StatelessWidget {
|
class TeacherPage extends StatelessWidget {
|
||||||
const TeacherPage({super.key});
|
const TeacherPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return BlocProvider(
|
||||||
body: Center(
|
create: (context) => ClassBloc()..add(const ClassLoadMyClasses()),
|
||||||
child: Text('教师 - 占位页面'),
|
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();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('布置主题'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
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: 'class-1',
|
||||||
|
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: 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: 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user