Bläddra i källkod

feat(report): 周报 PDF 固定 latest 路径、后台一键打开与His说明

Made-with: Cursor
yemeishu 1 månad sedan
förälder
incheckning
edcb0d09a2

+ 26 - 0
app/Console/Commands/ReportTeacherWeeklyPdfCommand.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class ReportTeacherWeeklyPdfCommand extends Command
+{
+    protected $signature = 'report:teacher-weekly-pdf';
+
+    protected $description = '生成老师组卷与学情分析周报 PDF,并更新 storage/app/reports/teacher-weekly-stats-latest.pdf';
+
+    public function handle(): int
+    {
+        $script = base_path('scripts/report_teacher_weekly_stats_pdf.php');
+        if (! is_file($script)) {
+            $this->error('找不到脚本:'.$script);
+
+            return self::FAILURE;
+        }
+
+        passthru(escapeshellarg(PHP_BINARY).' '.escapeshellarg($script), $code);
+
+        return $code === 0 ? self::SUCCESS : self::FAILURE;
+    }
+}

+ 76 - 0
app/Filament/Pages/TeacherWeeklyStatsReport.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Filament\Pages;
+
+use Filament\Actions\Action;
+use Filament\Notifications\Notification;
+use Filament\Pages\Page;
+use Illuminate\Support\Facades\Artisan;
+use Livewire\Attributes\Computed;
+use UnitEnum;
+
+class TeacherWeeklyStatsReport extends Page
+{
+    protected static ?string $title = '老师周报统计';
+
+    protected static ?string $navigationLabel = '老师周报统计';
+
+    protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-document-chart-bar';
+
+    protected static string|UnitEnum|null $navigationGroup = '其他';
+
+    protected static ?int $navigationSort = 95;
+
+    protected string $view = 'filament.pages.teacher-weekly-stats-report';
+
+    #[Computed(cache: false)]
+    public function hasLatestPdf(): bool
+    {
+        return is_file($this->latestPdfPath());
+    }
+
+    #[Computed(cache: false)]
+    public function latestPdfMtime(): ?string
+    {
+        $p = $this->latestPdfPath();
+
+        return is_file($p) ? date('Y-m-d H:i:s', filemtime($p)) : null;
+    }
+
+    #[Computed(cache: false)]
+    public function pdfOpenUrl(): string
+    {
+        return route('filament.admin.reports.teacher-weekly-stats.open');
+    }
+
+    protected function latestPdfPath(): string
+    {
+        return storage_path('app/reports/teacher-weekly-stats-latest.pdf');
+    }
+
+    protected function getHeaderActions(): array
+    {
+        return [
+            Action::make('regenerate')
+                ->label('生成 / 刷新周报 PDF')
+                ->icon('heroicon-o-arrow-path')
+                ->action(function (): void {
+                    $code = Artisan::call('report:teacher-weekly-pdf');
+                    if ($code !== 0) {
+                        Notification::make()
+                            ->title('生成失败')
+                            ->danger()
+                            ->body('请查看日志或终端输出')
+                            ->send();
+
+                        return;
+                    }
+                    $this->dispatch('$refresh');
+                    Notification::make()
+                        ->title('周报已生成')
+                        ->success()
+                        ->send();
+                }),
+        ];
+    }
+}

+ 28 - 0
app/Http/Controllers/TeacherWeeklyStatsReportController.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Response;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+
+class TeacherWeeklyStatsReportController extends Controller
+{
+    /**
+     * 浏览器内联打开周报 PDF(固定文件名 teacher-weekly-stats-latest.pdf)
+     */
+    public function open(): BinaryFileResponse|Response
+    {
+        $path = storage_path('app/reports/teacher-weekly-stats-latest.pdf');
+        if (! is_file($path)) {
+            return response(
+                '尚未生成周报 PDF。请在后台「老师周报统计」页面点击「生成/刷新周报」,或执行:php artisan report:teacher-weekly-pdf',
+                404
+            );
+        }
+
+        return response()->file($path, [
+            'Content-Type' => 'application/pdf',
+            'Content-Disposition' => 'inline; filename="teacher-weekly-stats.pdf"',
+        ]);
+    }
+}

+ 53 - 0
resources/views/filament/pages/teacher-weekly-stats-report.blade.php

@@ -0,0 +1,53 @@
+<x-filament-panels::page>
+    <div class="space-y-6">
+        <x-filament::section>
+            <x-slot name="heading">
+                老师组卷与学情分析(近 7 天)
+            </x-slot>
+            <x-slot name="description">
+                数据来自当前环境数据库;生成带时间戳的 PDF 后,会同步覆盖
+                <code class="text-xs rounded bg-gray-100 px-1 py-0.5 dark:bg-gray-800">teacher-weekly-stats-latest.pdf</code>
+                ,便于固定链接打开。
+            </x-slot>
+
+            <div class="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-center">
+                @if($this->hasLatestPdf)
+                    <x-filament::button
+                        tag="a"
+                        :href="$this->pdfOpenUrl"
+                        target="_blank"
+                        rel="noopener"
+                        icon="heroicon-o-arrow-top-right-on-square"
+                    >
+                        在浏览器中打开 PDF(新标签页)
+                    </x-filament::button>
+                    <span class="text-sm text-gray-600 dark:text-gray-400">
+                        最近生成:{{ $this->latestPdfMtime }}
+                    </span>
+                @else
+                    <p class="text-sm text-amber-700 dark:text-amber-400">
+                        尚未生成过周报。请先点击右上角「生成 / 刷新周报 PDF」。
+                    </p>
+                @endif
+            </div>
+        </x-filament::section>
+
+        <x-filament::section>
+            <x-slot name="heading">
+                文件名说明
+            </x-slot>
+            <ul class="list-inside list-disc space-y-2 text-sm text-gray-700 dark:text-gray-300">
+                <li>
+                    每次生成会新增一份带时间戳的文件:
+                    <code class="rounded bg-gray-100 px-1 py-0.5 text-xs dark:bg-gray-800">teacher-weekly-stats-日期_时分秒.pdf</code>
+                    (时分秒为 PHP <code class="text-xs">date('His')</code>,例如 <strong>143052</strong> 表示 14:30:52)
+                </li>
+                <li>
+                    最新一份会复制为
+                    <code class="rounded bg-gray-100 px-1 py-0.5 text-xs dark:bg-gray-800">storage/app/reports/teacher-weekly-stats-latest.pdf</code>
+                    ,后台「打开 PDF」始终指向该文件。
+                </li>
+            </ul>
+        </x-filament::section>
+    </div>
+</x-filament-panels::page>

+ 22 - 0
resources/views/filament/widgets/dashboard-quick-links.blade.php

@@ -102,6 +102,28 @@
                     </div>
                     </div>
                 </a>
                 </a>
 
 
+                <a href="{{ url('/admin/teacher-weekly-stats-report') }}"
+                   class="group flex flex-col h-full rounded-xl border border-base-200 bg-base-100 shadow-sm hover:shadow-md transition duration-150 hover:-translate-y-1 overflow-hidden">
+                    <div class="p-5 flex flex-col gap-4 h-full">
+                        <div class="flex items-start justify-between gap-3">
+                            <div class="flex items-center gap-3">
+                                <div class="w-12 h-12 rounded-xl bg-success/10 text-success flex items-center justify-center">
+                                    <x-heroicon-o-document-text class="w-6 h-6" />
+                                </div>
+                                <div class="leading-tight">
+                                    <h3 class="text-base font-semibold text-base-content">老师周报统计</h3>
+                                    <p class="text-xs text-base-content/60">Teacher weekly stats PDF</p>
+                                </div>
+                            </div>
+                            <span class="badge badge-success badge-outline whitespace-nowrap">报表</span>
+                        </div>
+                        <p class="text-sm text-base-content/70 leading-relaxed flex-1">近 7 天组卷与学情分析套数,一键生成并打开 PDF。</p>
+                        <div>
+                            <span class="btn btn-outline btn-sm w-full">进入</span>
+                        </div>
+                    </div>
+                </a>
+
                 <a href="{{ url('/admin/mistake-book') }}"
                 <a href="{{ url('/admin/mistake-book') }}"
                    class="group flex flex-col h-full rounded-xl border border-base-200 bg-base-100 shadow-sm hover:shadow-md transition duration-150 hover:-translate-y-1 overflow-hidden">
                    class="group flex flex-col h-full rounded-xl border border-base-200 bg-base-100 shadow-sm hover:shadow-md transition duration-150 hover:-translate-y-1 overflow-hidden">
                     <div class="p-5 flex flex-col gap-4 h-full">
                     <div class="p-5 flex flex-col gap-4 h-full">

+ 4 - 0
routes/web.php

@@ -26,6 +26,10 @@ Route::get('/preview/exam/grading-server/{paper_id}', [\App\Http\Controllers\Exa
 
 
 Route::get('/admin/exam-analysis/pdf', [\App\Http\Controllers\ExamAnalysisPdfController::class, 'show'])->name('filament.admin.auth.exam-analysis.pdf');
 Route::get('/admin/exam-analysis/pdf', [\App\Http\Controllers\ExamAnalysisPdfController::class, 'show'])->name('filament.admin.auth.exam-analysis.pdf');
 
 
+Route::get('/admin/reports/teacher-weekly-stats/open', [\App\Http\Controllers\TeacherWeeklyStatsReportController::class, 'open'])
+    ->middleware(['web', 'auth'])
+    ->name('filament.admin.reports.teacher-weekly-stats.open');
+
 // 检查通知的路由
 // 检查通知的路由
 Route::get('/admin/question-management/check-notifications', [NotificationController::class, 'checkNotifications']);
 Route::get('/admin/question-management/check-notifications', [NotificationController::class, 'checkNotifications']);
 
 

+ 1 - 0
scripts/report_teacher_weekly_stats.php

@@ -5,6 +5,7 @@
  * 用法:
  * 用法:
  *   php scripts/report_teacher_weekly_stats.php
  *   php scripts/report_teacher_weekly_stats.php
  *   php scripts/report_teacher_weekly_stats.php > storage/app/reports/teacher-weekly-stats-$(date +%Y-%m-%d)_$(date +%H%M%S).md
  *   php scripts/report_teacher_weekly_stats.php > storage/app/reports/teacher-weekly-stats-$(date +%Y-%m-%d)_$(date +%H%M%S).md
+ *   (shell 里 %H%M%S 会展开为当前时分秒;PDF 见 scripts/report_teacher_weekly_stats_pdf.php)
  */
  */
 
 
 require __DIR__ . '/../vendor/autoload.php';
 require __DIR__ . '/../vendor/autoload.php';

+ 10 - 0
scripts/report_teacher_weekly_stats_pdf.php

@@ -6,6 +6,10 @@
  * 用法:
  * 用法:
  *   php scripts/report_teacher_weekly_stats_pdf.php
  *   php scripts/report_teacher_weekly_stats_pdf.php
  *   php scripts/report_teacher_weekly_stats_pdf.php /path/to/out.pdf
  *   php scripts/report_teacher_weekly_stats_pdf.php /path/to/out.pdf
+ *
+ * 输出:
+ *   - 带时间戳:teacher-weekly-stats-{Y-m-d}_{His}.pdf(His = 24 小时制的时-分-秒,如 143052)
+ *   - 固定别名:storage/app/reports/teacher-weekly-stats-latest.pdf(覆盖为最近一次生成,供后台一键打开)
  */
  */
 
 
 use League\CommonMark\Environment\Environment;
 use League\CommonMark\Environment\Environment;
@@ -65,4 +69,10 @@ $mpdf->Output($outPath, \Mpdf\Output\Destination::FILE);
 $mdPath = preg_replace('/\.pdf$/i', '.md', $outPath);
 $mdPath = preg_replace('/\.pdf$/i', '.md', $outPath);
 file_put_contents($mdPath, $markdown);
 file_put_contents($mdPath, $markdown);
 
 
+$latestPdf = storage_path('app/reports/teacher-weekly-stats-latest.pdf');
+$latestMd = storage_path('app/reports/teacher-weekly-stats-latest.md');
+@copy($outPath, $latestPdf);
+@copy($mdPath, $latestMd);
+
 fwrite(STDERR, "PDF: {$outPath}\nMD:  {$mdPath}\n");
 fwrite(STDERR, "PDF: {$outPath}\nMD:  {$mdPath}\n");
+fwrite(STDERR, "最新别名(一键打开): {$latestPdf}\n");