StudentManagement.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Filament\Traits\HasUserRole;
  4. use App\Filament\Widgets\StudentStatsWidget;
  5. use App\Models\Student;
  6. use App\Models\Teacher;
  7. use BackedEnum;
  8. use Filament\Actions\Action;
  9. use Filament\Actions\BulkAction;
  10. use Filament\Actions\BulkActionGroup;
  11. use Filament\Actions\CreateAction;
  12. use Filament\Tables;
  13. use Filament\Tables\Table;
  14. use Filament\Pages\Page;
  15. use Illuminate\Support\Facades\DB;
  16. use Filament\Tables\Concerns\InteractsWithTable;
  17. use Filament\Tables\Contracts\HasTable;
  18. use UnitEnum;
  19. class StudentManagement extends Page implements HasTable
  20. {
  21. use HasUserRole, InteractsWithTable;
  22. protected static ?string $title = '师生管理';
  23. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-academic-cap';
  24. protected static ?string $navigationLabel = '师生管理';
  25. protected static string|UnitEnum|null $navigationGroup = '学生管理';
  26. protected static ?int $navigationSort = 1;
  27. protected string $view = 'filament.pages.student-management';
  28. public ?string $selectedTeacherId = null;
  29. public ?string $selectedTeacherName = null;
  30. public function mount(): void
  31. {
  32. $this->initializeUserRole();
  33. }
  34. protected ?string $subheading = '集中管理老师与学生信息,支持筛选与快速操作';
  35. public function filterByTeacher(?string $teacherId): void
  36. {
  37. $this->selectedTeacherId = $teacherId;
  38. if (! $teacherId) {
  39. $this->selectedTeacherName = null;
  40. return;
  41. }
  42. $teacher = Teacher::with('user')->find($teacherId);
  43. $this->selectedTeacherName = $teacher?->user?->full_name
  44. ?? $teacher?->name
  45. ?? '未知老师';
  46. }
  47. public function resetTeacherFilter(): void
  48. {
  49. $this->selectedTeacherId = null;
  50. $this->selectedTeacherName = null;
  51. }
  52. public function table(Table $table): Table
  53. {
  54. $currentUser = auth()->user();
  55. return $table
  56. ->query(
  57. Student::query()
  58. ->with(['teacher.user', 'user'])
  59. ->when($this->selectedTeacherId, fn ($query) => $query->where('teacher_id', $this->selectedTeacherId))
  60. ->when($currentUser?->isTeacher() ?? false, function ($query) use ($currentUser) {
  61. // 如果是老师登录,只显示该老师的学生
  62. $teacherId = $this->getCurrentTeacherId();
  63. if ($teacherId) {
  64. $query->where('teacher_id', $teacherId);
  65. }
  66. })
  67. )
  68. ->columns([
  69. Tables\Columns\TextColumn::make('id')
  70. ->label('ID')
  71. ->sortable()
  72. ->badge()
  73. ->color('gray')
  74. ->copyable()
  75. ->copyMessage('ID已复制')
  76. ->copyMessageDuration(1500),
  77. Tables\Columns\TextColumn::make('student_id')
  78. ->label('学生ID')
  79. ->searchable()
  80. ->copyable()
  81. ->toggleable(isToggledHiddenByDefault: true),
  82. Tables\Columns\TextColumn::make('name')
  83. ->label('姓名')
  84. ->searchable()
  85. ->sortable()
  86. ->weight('bold'),
  87. Tables\Columns\TextColumn::make('grade')
  88. ->label('年级')
  89. ->sortable()
  90. ->badge()
  91. ->color(fn(string $state): string => match($state) {
  92. '一年级' => 'gray',
  93. '二年级' => 'gray',
  94. '三年级' => 'blue',
  95. '四年级' => 'blue',
  96. '五年级' => 'green',
  97. '六年级' => 'green',
  98. default => 'primary',
  99. }),
  100. Tables\Columns\TextColumn::make('class_name')
  101. ->label('班级')
  102. ->sortable(),
  103. Tables\Columns\TextColumn::make('teacher.user.full_name')
  104. ->label('指导老师')
  105. ->sortable()
  106. ->url(fn($record): string =>
  107. optional($record->teacher?->user)->email
  108. ? "mailto:{$record->teacher?->user?->email}"
  109. : ''
  110. )
  111. ->openUrlInNewTab()
  112. ->visible(fn () => !($currentUser?->isTeacher() ?? false)), // 老师登录时不显示老师列
  113. Tables\Columns\TextColumn::make('user.email')
  114. ->label('邮箱')
  115. ->copyable()
  116. ->toggleable(),
  117. Tables\Columns\TextColumn::make('user.login_count')
  118. ->label('登录次数')
  119. ->sortable()
  120. ->alignCenter()
  121. ->badge()
  122. ->color(fn(?int $state): string =>
  123. ($state ?? 0) === 0 ? 'danger' : (($state ?? 0) < 5 ? 'warning' : 'success')
  124. ),
  125. Tables\Columns\TextColumn::make('user.last_login')
  126. ->label('最后登录')
  127. ->dateTime('Y-m-d H:i')
  128. ->sortable()
  129. ->description(fn($record): string =>
  130. $record->user?->last_login ?
  131. \Carbon\Carbon::parse($record->user->last_login)->diffForHumans() :
  132. '从未登录'
  133. ),
  134. Tables\Columns\TextColumn::make('created_at')
  135. ->label('创建时间')
  136. ->dateTime('Y-m-d H:i')
  137. ->sortable()
  138. ->toggleable(isToggledHiddenByDefault: true),
  139. ])
  140. ->filters([
  141. Tables\Filters\SelectFilter::make('grade')
  142. ->label('年级')
  143. ->options(fn() => Student::query()
  144. ->orderBy('grade')
  145. ->pluck('grade', 'grade')
  146. ->filter()
  147. ->toArray()),
  148. Tables\Filters\SelectFilter::make('class_name')
  149. ->label('班级')
  150. ->options(fn() => Student::query()
  151. ->orderBy('class_name')
  152. ->pluck('class_name', 'class_name')
  153. ->filter()
  154. ->toArray()),
  155. Tables\Filters\SelectFilter::make('teacher_id')
  156. ->label('指导老师')
  157. ->options(fn() => Teacher::query()
  158. ->with('user')
  159. ->get()
  160. ->mapWithKeys(fn (Teacher $teacher) => [
  161. $teacher->teacher_id => $teacher->user->full_name
  162. ?? $teacher->name
  163. ?? "老师 #{$teacher->teacher_id}",
  164. ])
  165. ->toArray())
  166. ->visible(fn () => !($currentUser?->isTeacher() ?? false)), // 老师登录时隐藏
  167. Tables\Filters\Filter::make('has_logged_in')
  168. ->label('登录状态')
  169. ->query(fn($query) => $query->whereHas('user', fn ($sub) => $sub->whereNotNull('last_login'))),
  170. ])
  171. ->actions([
  172. Action::make('view')
  173. ->label('查看')
  174. ->icon('heroicon-o-eye')
  175. ->url(fn($record): string => route('filament.admin.resources.students.view', $record))
  176. ->openUrlInNewTab(),
  177. Action::make('edit')
  178. ->label('编辑')
  179. ->icon('heroicon-o-pencil-square')
  180. ->url(fn($record): string => route('filament.admin.resources.students.edit', $record))
  181. ->openUrlInNewTab(),
  182. Action::make('reset_password')
  183. ->label('重置密码')
  184. ->icon('heroicon-o-key')
  185. ->color('warning')
  186. ->action(function ($record) {
  187. $newPassword = 'student123';
  188. $hashedPassword = \Hash::make($newPassword);
  189. DB::table('users')
  190. ->where('user_id', $record->student_id)
  191. ->update(['password_hash' => $hashedPassword]);
  192. \Filament\Notifications\Notification::make()
  193. ->success()
  194. ->title('密码重置成功')
  195. ->body("学生 {$record->name} 的密码已重置为: {$newPassword}")
  196. ->send();
  197. })
  198. ->requiresConfirmation()
  199. ->modalHeading('重置学生密码')
  200. ->modalDescription('确定要重置该学生的密码吗?新密码将是: student123')
  201. ->modalSubmitActionLabel('确认重置'),
  202. ])
  203. ->bulkActions([
  204. BulkActionGroup::make([
  205. BulkAction::make('reset_passwords')
  206. ->label('批量重置密码')
  207. ->icon('heroicon-o-key')
  208. ->color('warning')
  209. ->action(function ($records) {
  210. $newPassword = 'student123';
  211. $hashedPassword = \Hash::make($newPassword);
  212. foreach ($records as $record) {
  213. DB::table('users')
  214. ->where('user_id', $record->student_id)
  215. ->update(['password_hash' => $hashedPassword]);
  216. }
  217. \Filament\Notifications\Notification::make()
  218. ->success()
  219. ->title('批量重置密码成功')
  220. ->body("已为 {$records->count()} 位学生重置密码为: {$newPassword}")
  221. ->send();
  222. })
  223. ->requiresConfirmation()
  224. ->modalHeading('批量重置密码')
  225. ->modalDescription('确定要重置所选学生的密码吗?新密码将是: student123')
  226. ->modalSubmitActionLabel('确认重置'),
  227. ]),
  228. ])
  229. ->searchPlaceholder('搜索学生姓名、ID或邮箱...')
  230. ->emptyStateHeading('暂无学生数据')
  231. ->emptyStateDescription('还没有添加任何学生数据。')
  232. ->emptyStateActions([
  233. CreateAction::make()
  234. ->label('添加新学生')
  235. ->url(route('filament.admin.resources.students.create'))
  236. ->icon('heroicon-o-plus'),
  237. ])
  238. ->paginated([10, 25, 50, 100])
  239. ->poll('60s'); // 每60秒刷新一次
  240. }
  241. public function getHeaderWidgets(): array
  242. {
  243. return [
  244. StudentStatsWidget::class,
  245. ];
  246. }
  247. public function getTeacherOverviewProperty(): array
  248. {
  249. $currentUser = auth()->user();
  250. // 如果是老师,只返回自己的信息
  251. if ($currentUser?->isTeacher() ?? false) {
  252. $teacher = Teacher::query()
  253. ->with([
  254. 'user',
  255. 'students' => fn ($query) => $query
  256. ->select('student_id', 'name', 'grade', 'class_name', 'teacher_id')
  257. ->orderBy('name'),
  258. ])
  259. ->where('teacher_id', $this->getCurrentTeacherId())
  260. ->withCount('students')
  261. ->withMax('students', 'updated_at')
  262. ->first();
  263. if (!$teacher) {
  264. return [];
  265. }
  266. return [[
  267. 'teacher_id' => $teacher->teacher_id,
  268. 'teacher_name' => $teacher->user->full_name ?? $teacher->name ?? '未命名老师',
  269. 'teacher_email' => $teacher->user->email ?? null,
  270. 'students_count' => $teacher->students_count ?? 0,
  271. 'latest_student_activity' => $teacher->students_max_updated_at,
  272. 'students' => $teacher->students
  273. ->sortBy('name')
  274. ->take(4)
  275. ->map(fn (Student $student) => [
  276. 'student_id' => $student->student_id,
  277. 'name' => $student->name,
  278. 'grade' => $student->grade,
  279. 'class_name' => $student->class_name,
  280. ])->values()->toArray(),
  281. ]];
  282. }
  283. // 如果是管理员,返回所有老师信息
  284. $teachers = Teacher::query()
  285. ->with([
  286. 'user',
  287. 'students' => fn ($query) => $query
  288. ->select('student_id', 'name', 'grade', 'class_name', 'teacher_id')
  289. ->orderBy('name'),
  290. ])
  291. ->withCount('students')
  292. ->withMax('students', 'updated_at')
  293. ->orderByDesc('students_count')
  294. ->get();
  295. return $teachers->map(function (Teacher $teacher) {
  296. return [
  297. 'teacher_id' => $teacher->teacher_id,
  298. 'teacher_name' => $teacher->user->full_name ?? $teacher->name ?? '未命名老师',
  299. 'teacher_email' => $teacher->user->email ?? null,
  300. 'students_count' => $teacher->students_count ?? 0,
  301. 'latest_student_activity' => $teacher->students_max_updated_at,
  302. 'students' => $teacher->students
  303. ->sortBy('name')
  304. ->take(4)
  305. ->map(fn (Student $student) => [
  306. 'student_id' => $student->student_id,
  307. 'name' => $student->name,
  308. 'grade' => $student->grade,
  309. 'class_name' => $student->class_name,
  310. ])->values()->toArray(),
  311. ];
  312. })->toArray();
  313. }
  314. }