StudentManagement.php 13 KB

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