|
@@ -0,0 +1,693 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 按「自然日」统计:近 N 个自然日(今日为 0 点—生成时刻)vs 紧邻的前 N 个完整自然日;exam_analysis_results 按 paper_id 去重计套。
|
|
|
|
|
+ * 按老师:学案/分析/学生数为「本 / 上」并列;保留学案·环比、学情·环比。
|
|
|
|
|
+ * 用法:
|
|
|
|
|
+ * php scripts/report_teacher_weekly_stats.php
|
|
|
|
|
+ * TEACHER_WEEKLY_REPORT_DAYS=14 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
|
|
|
|
|
+ *
|
|
|
|
|
+ * 环境变量 / artisan:TEACHER_WEEKLY_REPORT_DAYS(默认 7)= 本/上周期各包含几个自然日。时区:config app.timezone。
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+require __DIR__ . '/../vendor/autoload.php';
|
|
|
|
|
+
|
|
|
|
|
+$app = require_once __DIR__ . '/../bootstrap/app.php';
|
|
|
|
|
+$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
|
|
|
|
+
|
|
|
|
|
+$tz = (string) config('app.timezone', 'UTC');
|
|
|
|
|
+
|
|
|
|
|
+$reportPeriodDays = 7;
|
|
|
|
|
+$envDays = getenv('TEACHER_WEEKLY_REPORT_DAYS');
|
|
|
|
|
+if ($envDays !== false && $envDays !== '') {
|
|
|
|
|
+ $reportPeriodDays = max(1, min(366, (int) $envDays));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 本周期:自「今日 0 点」往回数第 N 天 0 点起,至当前时刻。上周期:紧邻的前 N 个完整自然日。 */
|
|
|
|
|
+$endCurrent = now()->timezone($tz);
|
|
|
|
|
+$todayStart = $endCurrent->copy()->startOfDay();
|
|
|
|
|
+$startCurrent = $todayStart->copy()->subDays($reportPeriodDays - 1);
|
|
|
|
|
+$startPrev = $todayStart->copy()->subDays(2 * $reportPeriodDays - 1);
|
|
|
|
|
+$endPrev = $todayStart->copy()->subDays($reportPeriodDays - 1);
|
|
|
|
|
+
|
|
|
|
|
+$db = \Illuminate\Support\Facades\DB::class;
|
|
|
|
|
+
|
|
|
|
|
+$sumPapers = static function ($from, $toExclusive) use ($db) {
|
|
|
|
|
+ $q = $db::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $from);
|
|
|
|
|
+ if ($toExclusive !== null) {
|
|
|
|
|
+ $q->where('created_at', '<', $toExclusive);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (int) $q->count();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 学情分析:以卷子为单位,同一 paper_id 在区间内多条记录只计 1 */
|
|
|
|
|
+$countDistinctAnalysisPapers = static function ($from, $toExclusive) use ($db) {
|
|
|
|
|
+ $q = $db::table('exam_analysis_results as ear')
|
|
|
|
|
+ ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
|
|
|
+ ->whereNotNull('p.teacher_id')
|
|
|
|
|
+ ->where('p.teacher_id', '!=', '')
|
|
|
|
|
+ ->where('ear.created_at', '>=', $from);
|
|
|
|
|
+ if ($toExclusive !== null) {
|
|
|
|
|
+ $q->where('ear.created_at', '<', $toExclusive);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (int) $q->distinct()->count('ear.paper_id');
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+$countActiveTeachers = static function ($from, $toExclusive) use ($db) {
|
|
|
|
|
+ $q = $db::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $from);
|
|
|
|
|
+ if ($toExclusive !== null) {
|
|
|
|
|
+ $q->where('created_at', '<', $toExclusive);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (int) $q->distinct()->count('teacher_id');
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+$totalPapersCur = $sumPapers($startCurrent, $endCurrent);
|
|
|
|
|
+$totalPapersPrev = $sumPapers($startPrev, $endPrev);
|
|
|
|
|
+$totalAnalysisCur = $countDistinctAnalysisPapers($startCurrent, $endCurrent);
|
|
|
|
|
+$totalAnalysisPrev = $countDistinctAnalysisPapers($startPrev, $endPrev);
|
|
|
|
|
+$teachersCur = $countActiveTeachers($startCurrent, $endCurrent);
|
|
|
|
|
+$teachersPrev = $countActiveTeachers($startPrev, $endPrev);
|
|
|
|
|
+
|
|
|
|
|
+$wowLine = static function (int $cur, int $prev): string {
|
|
|
|
|
+ $delta = $cur - $prev;
|
|
|
|
|
+ if ($prev === 0) {
|
|
|
|
|
+ if ($cur === 0) {
|
|
|
|
|
+ return '0';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return sprintf('+%d(上周期0)', $delta);
|
|
|
|
|
+ }
|
|
|
|
|
+ $pct = round(($delta / $prev) * 100, 2);
|
|
|
|
|
+ $sign = $delta >= 0 ? '+' : '';
|
|
|
|
|
+ $dir = match (true) {
|
|
|
|
|
+ $delta > 0 => '↑',
|
|
|
|
|
+ $delta < 0 => '↓',
|
|
|
|
|
+ default => '→',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return sprintf('%s%d(%s%.2f%%)%s', $sign, $delta, $sign, $pct, $dir);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 总量表环比列:正增长着色(学案蓝 / 学情橙 / 其余绿) */
|
|
|
|
|
+$wowLineHtml = static function (int $cur, int $prev, string $posColor = '#16a34a') use ($wowLine): string {
|
|
|
|
|
+ $plain = $wowLine($cur, $prev);
|
|
|
|
|
+ if ($cur > $prev) {
|
|
|
|
|
+ return '<span style="color:'.$posColor.';font-weight:600;">'.$plain.'</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $plain;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 环比列:正增长用 $posColor(学案蓝 / 学情橙 / 默认绿) */
|
|
|
|
|
+$compareCellHtml = static function (int $cur, int $prev, string $posColor = '#16a34a'): string {
|
|
|
|
|
+ $d = $cur - $prev;
|
|
|
|
|
+ if ($d === 0) {
|
|
|
|
|
+ return '0';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($prev === 0) {
|
|
|
|
|
+ if ($cur === 0) {
|
|
|
|
|
+ return '0';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return '<span style="color:'.$posColor.';font-weight:600;">+'.$d.'(上0)</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+ $pct = round(($d / $prev) * 100, 1);
|
|
|
|
|
+ $sign = $d > 0 ? '+' : '';
|
|
|
|
|
+ $text = sprintf('%s%d(%s%.1f%%)', $sign, $d, $sign, $pct);
|
|
|
|
|
+ if ($d > 0) {
|
|
|
|
|
+ return '<span style="color:'.$posColor.';font-weight:600;">'.$text.'</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $text;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 「本 / 上」并列:本>上时本侧用强调色(与上方左/右图折线一致);「 / 」与上周期数字固定深灰黑 */
|
|
|
|
|
+$slashPairAccentHtml = static function (int $cur, int $prev, string $accent): string {
|
|
|
|
|
+ $rest = '<span style="color:#111827;font-weight:normal;"> / '.$prev.'</span>';
|
|
|
|
|
+ if ($cur > $prev) {
|
|
|
|
|
+ return '<span style="color:'.$accent.';font-weight:700;">'.$cur.'</span>'.$rest;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return '<span style="color:#111827;font-weight:normal;">'.$cur.'</span>'.$rest;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 与每日对比图一致:学案蓝、学情橙、学生绿 */
|
|
|
|
|
+$colorPapers = '#2563eb';
|
|
|
|
|
+$colorAnalysis = '#ea580c';
|
|
|
|
|
+$colorStudents = '#16a34a';
|
|
|
|
|
+
|
|
|
|
|
+$byTeacher = \Illuminate\Support\Facades\DB::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $startCurrent)
|
|
|
|
|
+ ->where('created_at', '<', $endCurrent)
|
|
|
|
|
+ ->selectRaw('teacher_id, COUNT(*) as paper_count')
|
|
|
|
|
+ ->groupBy('teacher_id')
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+// 本时间窗内学情:按 ear.paper_id 去重后归到 papers.teacher_id
|
|
|
|
|
+$analysisByTeacher = \Illuminate\Support\Facades\DB::table('exam_analysis_results as ear')
|
|
|
|
|
+ ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
|
|
|
+ ->whereNotNull('p.teacher_id')
|
|
|
|
|
+ ->where('p.teacher_id', '!=', '')
|
|
|
|
|
+ ->where('ear.created_at', '>=', $startCurrent)
|
|
|
|
|
+ ->where('ear.created_at', '<', $endCurrent)
|
|
|
|
|
+ ->selectRaw('p.teacher_id, COUNT(DISTINCT ear.paper_id) AS paper_set_count')
|
|
|
|
|
+ ->groupBy('p.teacher_id')
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+$analysisMap = [];
|
|
|
|
|
+foreach ($analysisByTeacher as $r) {
|
|
|
|
|
+ $analysisMap[(string) $r->teacher_id] = (int) $r->paper_set_count;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$paperMap = [];
|
|
|
|
|
+foreach ($byTeacher as $r) {
|
|
|
|
|
+ $paperMap[(string) $r->teacher_id] = (int) $r->paper_count;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$byTeacherPrev = $db::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $startPrev)
|
|
|
|
|
+ ->where('created_at', '<', $endPrev)
|
|
|
|
|
+ ->selectRaw('teacher_id, COUNT(*) as paper_count')
|
|
|
|
|
+ ->groupBy('teacher_id')
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+$analysisByTeacherPrev = $db::table('exam_analysis_results as ear')
|
|
|
|
|
+ ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
|
|
|
+ ->whereNotNull('p.teacher_id')
|
|
|
|
|
+ ->where('p.teacher_id', '!=', '')
|
|
|
|
|
+ ->where('ear.created_at', '>=', $startPrev)
|
|
|
|
|
+ ->where('ear.created_at', '<', $endPrev)
|
|
|
|
|
+ ->selectRaw('p.teacher_id, COUNT(DISTINCT ear.paper_id) AS paper_set_count')
|
|
|
|
|
+ ->groupBy('p.teacher_id')
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+$paperMapPrev = [];
|
|
|
|
|
+foreach ($byTeacherPrev as $r) {
|
|
|
|
|
+ $paperMapPrev[(string) $r->teacher_id] = (int) $r->paper_count;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$analysisMapPrev = [];
|
|
|
|
|
+foreach ($analysisByTeacherPrev as $r) {
|
|
|
|
|
+ $analysisMapPrev[(string) $r->teacher_id] = (int) $r->paper_set_count;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 学生数:组卷 ∪ 学情,student_id 合并去重(按老师、时间窗) */
|
|
|
|
|
+$studentUnionSql = <<<'SQL'
|
|
|
|
|
+SELECT u.teacher_id, COUNT(DISTINCT u.student_id) AS c
|
|
|
|
|
+FROM (
|
|
|
|
|
+ SELECT teacher_id, student_id FROM papers
|
|
|
|
|
+ WHERE teacher_id IS NOT NULL AND teacher_id != ''
|
|
|
|
|
+ AND student_id IS NOT NULL AND student_id != ''
|
|
|
|
|
+ AND created_at >= ? AND created_at < ?
|
|
|
|
|
+ UNION
|
|
|
|
|
+ SELECT p.teacher_id, ear.student_id
|
|
|
|
|
+ FROM exam_analysis_results ear
|
|
|
|
|
+ INNER JOIN papers p ON p.paper_id = ear.paper_id
|
|
|
|
|
+ WHERE p.teacher_id IS NOT NULL AND p.teacher_id != ''
|
|
|
|
|
+ AND ear.student_id IS NOT NULL AND ear.student_id != ''
|
|
|
|
|
+ AND ear.created_at >= ? AND ear.created_at < ?
|
|
|
|
|
+) u
|
|
|
|
|
+GROUP BY u.teacher_id
|
|
|
|
|
+SQL;
|
|
|
|
|
+
|
|
|
|
|
+$studentUnionCurRows = $db::select($studentUnionSql, [$startCurrent, $endCurrent, $startCurrent, $endCurrent]);
|
|
|
|
|
+$studentUnionPrevRows = $db::select($studentUnionSql, [$startPrev, $endPrev, $startPrev, $endPrev]);
|
|
|
|
|
+
|
|
|
|
|
+$studentUnionCurMap = [];
|
|
|
|
|
+foreach ($studentUnionCurRows as $row) {
|
|
|
|
|
+ $studentUnionCurMap[(string) $row->teacher_id] = (int) $row->c;
|
|
|
|
|
+}
|
|
|
|
|
+$studentUnionPrevMap = [];
|
|
|
|
|
+foreach ($studentUnionPrevRows as $row) {
|
|
|
|
|
+ $studentUnionPrevMap[(string) $row->teacher_id] = (int) $row->c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$names = \Illuminate\Support\Facades\DB::table('teachers')->pluck('name', 'teacher_id');
|
|
|
|
|
+$nameStrMap = [];
|
|
|
|
|
+foreach ($names as $tid => $nm) {
|
|
|
|
|
+ $nameStrMap[(string) $tid] = $nm;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+$rows = [];
|
|
|
|
|
+foreach ($paperMap as $tid => $paperCount) {
|
|
|
|
|
+ $rows[] = [
|
|
|
|
|
+ 'teacher_id' => $tid,
|
|
|
|
|
+ 'name' => (string) ($nameStrMap[$tid] ?? $tid),
|
|
|
|
|
+ 'papers' => $paperCount,
|
|
|
|
|
+ 'analysis_sets' => (int) ($analysisMap[$tid] ?? 0),
|
|
|
|
|
+ 'papers_prev' => (int) ($paperMapPrev[$tid] ?? 0),
|
|
|
|
|
+ 'analysis_sets_prev' => (int) ($analysisMapPrev[$tid] ?? 0),
|
|
|
|
|
+ ];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+usort($rows, static fn ($a, $b) => $b['papers'] <=> $a['papers']);
|
|
|
|
|
+
|
|
|
|
|
+$windowCur = sprintf(
|
|
|
|
|
+ '%s ~ %s',
|
|
|
|
|
+ $startCurrent->format('Y-m-d H:i:s'),
|
|
|
|
|
+ $endCurrent->format('Y-m-d H:i:s')
|
|
|
|
|
+);
|
|
|
|
|
+$windowPrev = sprintf(
|
|
|
|
|
+ '%s ~ %s',
|
|
|
|
|
+ $startPrev->format('Y-m-d H:i:s'),
|
|
|
|
|
+ $endPrev->format('Y-m-d H:i:s')
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+$generatedAt = $endCurrent->format('Y-m-d H:i:s');
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 按自然日切段(app.timezone):每天一格;本周期末段为「今日 0 点—当前时刻」。
|
|
|
|
|
+ * 学案:当日创建的组卷数,各日之和 = 窗口总量。
|
|
|
|
|
+ * 学情:该自然日内有 analysis 记录的卷去重(同日多条只计 1);跨日同一卷可在多日重复出现,故各日之和可不等于上方「窗口内卷仅计一次」的总量。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return array{labels: list<string>, papers: list<int>, analysis: list<int>}
|
|
|
|
|
+ */
|
|
|
|
|
+$dailyNaturalSlices = static function (string $which) use ($db, $reportPeriodDays, $todayStart, $endCurrent): array {
|
|
|
|
|
+ $n = $reportPeriodDays;
|
|
|
|
|
+ $labels = [];
|
|
|
|
|
+ $papers = [];
|
|
|
|
|
+ $analysis = [];
|
|
|
|
|
+
|
|
|
|
|
+ $countAnalysisSetsInRange = static function ($from, $toExclusive) use ($db): int {
|
|
|
|
|
+ return (int) $db::table('exam_analysis_results as ear')
|
|
|
|
|
+ ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
|
|
|
+ ->whereNotNull('p.teacher_id')
|
|
|
|
|
+ ->where('p.teacher_id', '!=', '')
|
|
|
|
|
+ ->where('ear.created_at', '>=', $from)
|
|
|
|
|
+ ->where('ear.created_at', '<', $toExclusive)
|
|
|
|
|
+ ->distinct()
|
|
|
|
|
+ ->count('ear.paper_id');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if ($which === 'current') {
|
|
|
|
|
+ for ($i = 0; $i < $n; $i++) {
|
|
|
|
|
+ $dayStart = $todayStart->copy()->subDays($n - 1 - $i);
|
|
|
|
|
+ $dayEnd = ($i < $n - 1) ? $dayStart->copy()->addDay() : $endCurrent;
|
|
|
|
|
+ $labels[] = $dayStart->format('j');
|
|
|
|
|
+ $papers[] = (int) $db::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $dayStart)
|
|
|
|
|
+ ->where('created_at', '<', $dayEnd)
|
|
|
|
|
+ ->count();
|
|
|
|
|
+ $analysis[] = $countAnalysisSetsInRange($dayStart, $dayEnd);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ for ($i = 0; $i < $n; $i++) {
|
|
|
|
|
+ $dayStart = $todayStart->copy()->subDays(2 * $n - 1 - $i);
|
|
|
|
|
+ $dayEnd = $dayStart->copy()->addDay();
|
|
|
|
|
+ $labels[] = $dayStart->format('j');
|
|
|
|
|
+ $papers[] = (int) $db::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $dayStart)
|
|
|
|
|
+ ->where('created_at', '<', $dayEnd)
|
|
|
|
|
+ ->count();
|
|
|
|
|
+ $analysis[] = $countAnalysisSetsInRange($dayStart, $dayEnd);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ['labels' => $labels, 'papers' => $papers, 'analysis' => $analysis];
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+$curDaily = $dailyNaturalSlices('current');
|
|
|
|
|
+$prevDaily = $dailyNaturalSlices('prev');
|
|
|
|
|
+
|
|
|
|
|
+/** 左侧:学案(组卷)套数;右侧:学情分析套数(卷去重)。横轴:本周期日 / 上周期同序号日。 */
|
|
|
|
|
+$buildDualChartsHtml = static function (array $curDaily, array $prevDaily): string {
|
|
|
|
|
+ $axisCur = $curDaily['labels'];
|
|
|
|
|
+ $axisPrev = $prevDaily['labels'];
|
|
|
|
|
+ $pc = $curDaily['papers'];
|
|
|
|
|
+ $ac = $curDaily['analysis'];
|
|
|
|
|
+ $pp = $prevDaily['papers'];
|
|
|
|
|
+ $ap = $prevDaily['analysis'];
|
|
|
|
|
+
|
|
|
|
|
+ $halfSvg = static function (
|
|
|
|
|
+ array $labelsCur,
|
|
|
|
|
+ array $labelsPrev,
|
|
|
|
|
+ array $cur,
|
|
|
|
|
+ array $prev,
|
|
|
|
|
+ string $strokeCur,
|
|
|
|
|
+ string $strokePrev,
|
|
|
|
|
+ string $legCur,
|
|
|
|
|
+ string $legPrev
|
|
|
|
|
+ ): string {
|
|
|
|
|
+ $maxY = max(1, ...$cur, ...$prev);
|
|
|
|
|
+ $W = 296;
|
|
|
|
|
+ $H = 256;
|
|
|
|
|
+ $padL = 42;
|
|
|
|
|
+ /** 右侧、顶部留白:数值标签 text-anchor=middle 时不依赖末列特殊锚点 */
|
|
|
|
|
+ $padR = 62;
|
|
|
|
|
+ $padT = 72;
|
|
|
|
|
+ $padB = 56;
|
|
|
|
|
+ $gw = $W - $padL - $padR;
|
|
|
|
|
+ $gh = $H - $padT - $padB;
|
|
|
|
|
+ $n = max(1, count($labelsCur));
|
|
|
|
|
+ $xAt = static function (int $i) use ($padL, $gw, $n): float {
|
|
|
|
|
+ return $padL + ($n <= 1 ? $gw / 2 : $gw * $i / ($n - 1));
|
|
|
|
|
+ };
|
|
|
|
|
+ $yAt = static function (int $v) use ($padT, $gh, $maxY): float {
|
|
|
|
|
+ return $padT + $gh - ($v / $maxY) * $gh;
|
|
|
|
|
+ };
|
|
|
|
|
+ $lineWithDots = static function (array $vals, string $stroke, string $dash, float $sw = 1.6) use ($xAt, $yAt, $n): string {
|
|
|
|
|
+ $pts = [];
|
|
|
|
|
+ $circles = '';
|
|
|
|
|
+ for ($i = 0; $i < $n; $i++) {
|
|
|
|
|
+ $x = $xAt($i);
|
|
|
|
|
+ $y = $yAt((int) $vals[$i]);
|
|
|
|
|
+ $pts[] = round($x, 1).','.round($y, 1);
|
|
|
|
|
+ $circles .= sprintf(
|
|
|
|
|
+ '<circle cx="%.1f" cy="%.1f" r="3.2" fill="%s" stroke="#fff" stroke-width="1"/>',
|
|
|
|
|
+ $x,
|
|
|
|
|
+ $y,
|
|
|
|
|
+ $stroke
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ $poly = implode(' ', $pts);
|
|
|
|
|
+ $dashAttr = $dash !== '' ? ' stroke-dasharray="'.$dash.'"' : '';
|
|
|
|
|
+
|
|
|
|
|
+ return '<polyline fill="none" stroke="'.$stroke.'" stroke-width="'.$sw.'"'.$dashAttr.' points="'.$poly.'" />'.$circles;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /** 标签:每列只放一条「本/上」,y = 最高点y - 固定偏移(以 dominant-baseline=middle 作为中心y) */
|
|
|
|
|
+ $labelFillCur = $strokeCur === '#2563eb' ? '#1d4ed8' : '#c2410c';
|
|
|
|
|
+ $labelFillPrev = $strokePrev === '#93c5fd' ? '#475569' : '#78716c';
|
|
|
|
|
+ $valueLabels = '<g font-family="sun-exta,sans-serif">';
|
|
|
|
|
+ $labelOffset = 22; // 标签中心到点的固定上移像素
|
|
|
|
|
+ for ($vi = 0; $vi < $n; $vi++) {
|
|
|
|
|
+ $xv = $xAt($vi);
|
|
|
|
|
+ $yCv = $yAt((int) $cur[$vi]);
|
|
|
|
|
+ $yPv = $yAt((int) $prev[$vi]);
|
|
|
|
|
+ $vC = (int) $cur[$vi];
|
|
|
|
|
+ $vP = (int) $prev[$vi];
|
|
|
|
|
+ $yUpper = min($yCv, $yPv);
|
|
|
|
|
+ // 允许跑到绘图区上方的留白里;只在极端情况下防止贴到 SVG 顶边
|
|
|
|
|
+ $baseY = max(14, $yUpper - $labelOffset);
|
|
|
|
|
+ $valueLabels .= sprintf(
|
|
|
|
|
+ '<text x="%.1f" y="%.1f" text-anchor="middle" dominant-baseline="middle" font-size="9" font-family="sun-exta,sans-serif"><tspan fill="%s">%d</tspan><tspan fill="#94a3b8">/</tspan><tspan fill="%s">%d</tspan></text>',
|
|
|
|
|
+ $xv,
|
|
|
|
|
+ $baseY,
|
|
|
|
|
+ $labelFillCur,
|
|
|
|
|
+ $vC,
|
|
|
|
|
+ $labelFillPrev,
|
|
|
|
|
+ $vP
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ $valueLabels .= '</g>';
|
|
|
|
|
+
|
|
|
|
|
+ $xAxisY = $padT + $gh;
|
|
|
|
|
+ $tickTxt = '';
|
|
|
|
|
+ $axisRowY = $H - 48;
|
|
|
|
|
+ for ($i = 0; $i < $n; $i++) {
|
|
|
|
|
+ $x = $xAt($i);
|
|
|
|
|
+ $lc = htmlspecialchars((string) ($labelsCur[$i] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $lp = htmlspecialchars((string) ($labelsPrev[$i] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $tickTxt .= sprintf(
|
|
|
|
|
+ '<text x="%.1f" y="%d" text-anchor="middle" font-size="8" font-family="sun-exta,sans-serif"><tspan fill="#111827">%s</tspan><tspan fill="#94a3b8">/</tspan><tspan fill="#475569">%s</tspan></text>',
|
|
|
|
|
+ $x,
|
|
|
|
|
+ $axisRowY,
|
|
|
|
|
+ $lc,
|
|
|
|
|
+ $lp
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $yTick = '';
|
|
|
|
|
+ for ($k = 0; $k <= 4; $k++) {
|
|
|
|
|
+ $v = (int) round($maxY * $k / 4);
|
|
|
|
|
+ $y = $yAt($v);
|
|
|
|
|
+ $yTick .= sprintf(
|
|
|
|
|
+ '<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#e5e7eb" stroke-width="1"/>',
|
|
|
|
|
+ $padL,
|
|
|
|
|
+ $y,
|
|
|
|
|
+ $padL + $gw,
|
|
|
|
|
+ $y
|
|
|
|
|
+ );
|
|
|
|
|
+ $yTick .= sprintf(
|
|
|
|
|
+ '<text x="%d" y="%.1f" font-size="8" fill="#6b7280" font-family="sun-exta,sans-serif">%d</text>',
|
|
|
|
|
+ 2,
|
|
|
|
|
+ $y + 3,
|
|
|
|
|
+ $v
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $legend = '<g font-family="sun-exta,sans-serif" font-size="9">';
|
|
|
|
|
+ $lx = $padL + 4;
|
|
|
|
|
+ $ly = $H - 26;
|
|
|
|
|
+ $items = [
|
|
|
|
|
+ [$strokeCur, $legCur, ''],
|
|
|
|
|
+ [$strokePrev, $legPrev, '6,4'],
|
|
|
|
|
+ ];
|
|
|
|
|
+ foreach ($items as $idx => $it) {
|
|
|
|
|
+ $yy = $ly + $idx * 12;
|
|
|
|
|
+ $legend .= sprintf(
|
|
|
|
|
+ '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="2" %s/>',
|
|
|
|
|
+ $lx,
|
|
|
|
|
+ $yy,
|
|
|
|
|
+ $lx + 16,
|
|
|
|
|
+ $yy,
|
|
|
|
|
+ $it[0],
|
|
|
|
|
+ $it[2] !== '' ? 'stroke-dasharray="'.$it[2].'"' : ''
|
|
|
|
|
+ );
|
|
|
|
|
+ $legend .= sprintf(
|
|
|
|
|
+ '<text x="%d" y="%d" fill="#111">%s</text>',
|
|
|
|
|
+ $lx + 20,
|
|
|
|
|
+ $yy + 4,
|
|
|
|
|
+ htmlspecialchars($it[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ $legend .= '</g>';
|
|
|
|
|
+
|
|
|
|
|
+ $svg = sprintf(
|
|
|
|
|
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" width="100%%" height="auto" style="max-width:100%%;">',
|
|
|
|
|
+ $W,
|
|
|
|
|
+ $H
|
|
|
|
|
+ );
|
|
|
|
|
+ $svg .= sprintf('<rect x="0" y="0" width="%d" height="%d" fill="#fafafa"/>', $W, $H);
|
|
|
|
|
+ $svg .= $yTick;
|
|
|
|
|
+ $svg .= sprintf('<line x1="%d" y1="%.1f" x2="%.1f" y2="%.1f" stroke="#9ca3af" stroke-width="1"/>', $padL, $xAxisY, $padL + $gw, $xAxisY);
|
|
|
|
|
+ $svg .= $lineWithDots($prev, $strokePrev, '6,4', 1.4);
|
|
|
|
|
+ $svg .= $lineWithDots($cur, $strokeCur, '', 1.8);
|
|
|
|
|
+ $svg .= $valueLabels;
|
|
|
|
|
+ $svg .= $tickTxt;
|
|
|
|
|
+ $svg .= $legend;
|
|
|
|
|
+ $svg .= '</svg>';
|
|
|
|
|
+
|
|
|
|
|
+ return $svg;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ $left = $halfSvg($axisCur, $axisPrev, $pc, $pp, '#2563eb', '#93c5fd', '学案·本周期', '学案·上周期');
|
|
|
|
|
+ $right = $halfSvg($axisCur, $axisPrev, $ac, $ap, '#ea580c', '#fdba74', '学情·本周期', '学情·上周期');
|
|
|
|
|
+
|
|
|
|
|
+ return '<table class="weekly-chart-pair" style="width:100%;border-collapse:collapse;margin:0;"><tr>'
|
|
|
|
|
+ .'<td style="width:50%;vertical-align:top;padding:2px 5px 2px 0;">'.$left.'</td>'
|
|
|
|
|
+ .'<td style="width:50%;vertical-align:top;padding:2px 0 2px 5px;">'.$right.'</td>'
|
|
|
|
|
+ .'</tr></table>';
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+$chartSvg = $buildDualChartsHtml($curDaily, $prevDaily);
|
|
|
|
|
+
|
|
|
|
|
+echo sprintf("## 老师组卷与学情分析(近%d个自然日)\n\n", $reportPeriodDays);
|
|
|
|
|
+echo "> 生成 {$generatedAt} · {$tz} · 本 {$windowCur} · 上 {$windowPrev}\n";
|
|
|
|
|
+echo sprintf("> 口径:自然日边界(config timezone);今日段为当日 0:00—生成时刻;上周期为紧邻的前 **%d** 个完整自然日。\n\n", $reportPeriodDays);
|
|
|
|
|
+
|
|
|
|
|
+echo "### 总量\n\n";
|
|
|
|
|
+echo "| 指标 | 本周期 | 上周期 | 环比 |\n";
|
|
|
|
|
+echo "| :---: | :---: | :---: | :---: |\n";
|
|
|
|
|
+echo sprintf("| 组卷总套数 | %d | %d | %s |\n", $totalPapersCur, $totalPapersPrev, $wowLineHtml($totalPapersCur, $totalPapersPrev, $colorPapers));
|
|
|
|
|
+echo sprintf("| 学情分析套数(卷去重) | %d | %d | %s |\n", $totalAnalysisCur, $totalAnalysisPrev, $wowLineHtml($totalAnalysisCur, $totalAnalysisPrev, $colorAnalysis));
|
|
|
|
|
+echo sprintf("| 有组卷老师数 | %d | %d | %s |\n", $teachersCur, $teachersPrev, $wowLineHtml($teachersCur, $teachersPrev, $colorStudents));
|
|
|
|
|
+
|
|
|
|
|
+echo "### 逐日对比(左:学案 · 右:学情)\n\n";
|
|
|
|
|
+echo '<div class="weekly-chart">';
|
|
|
|
|
+echo $chartSvg;
|
|
|
|
|
+echo "</div>\n\n";
|
|
|
|
|
+
|
|
|
|
|
+// 老师列表汇总:先给出增/降总体倾向,再给出异常清单
|
|
|
|
|
+$calcPct = static function (int $delta, int $prev): ?float {
|
|
|
|
|
+ if ($prev <= 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return round(($delta / $prev) * 100, 1);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+$fmtDeltaWithPct = static function (int $delta, int $prev): string {
|
|
|
|
|
+ $sign = $delta > 0 ? '+' : '';
|
|
|
|
|
+ $pct = $prev > 0 ? sprintf('(%s%.1f%%)', $sign, ($delta / $prev) * 100) : '(上0)';
|
|
|
|
|
+ return $sign.$delta.$pct;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+$paperUp = 0; $paperDown = 0; $paperFlat = 0;
|
|
|
|
|
+$analysisUp = 0; $analysisDown = 0; $analysisFlat = 0;
|
|
|
|
|
+$studentUp = 0; $studentDown = 0; $studentFlat = 0;
|
|
|
|
|
+$sumPaperDelta = 0; $sumAnalysisDelta = 0; $sumStudentDelta = 0;
|
|
|
|
|
+$anomalies = [];
|
|
|
|
|
+
|
|
|
|
|
+foreach ($rows as $r) {
|
|
|
|
|
+ $tidKey = (string) $r['teacher_id'];
|
|
|
|
|
+ $name = (string) $r['name'];
|
|
|
|
|
+ $papersCur = (int) $r['papers'];
|
|
|
|
|
+ $papersPrev = (int) $r['papers_prev'];
|
|
|
|
|
+ $analysisCur = (int) $r['analysis_sets'];
|
|
|
|
|
+ $analysisPrev = (int) $r['analysis_sets_prev'];
|
|
|
|
|
+ $studentCur = (int) ($studentUnionCurMap[$tidKey] ?? 0);
|
|
|
|
|
+ $studentPrev = (int) ($studentUnionPrevMap[$tidKey] ?? 0);
|
|
|
|
|
+
|
|
|
|
|
+ $paperDelta = $papersCur - $papersPrev;
|
|
|
|
|
+ $analysisDelta = $analysisCur - $analysisPrev;
|
|
|
|
|
+ $studentDelta = $studentCur - $studentPrev;
|
|
|
|
|
+ $sumPaperDelta += $paperDelta;
|
|
|
|
|
+ $sumAnalysisDelta += $analysisDelta;
|
|
|
|
|
+ $sumStudentDelta += $studentDelta;
|
|
|
|
|
+
|
|
|
|
|
+ if ($paperDelta > 0) { $paperUp++; } elseif ($paperDelta < 0) { $paperDown++; } else { $paperFlat++; }
|
|
|
|
|
+ if ($analysisDelta > 0) { $analysisUp++; } elseif ($analysisDelta < 0) { $analysisDown++; } else { $analysisFlat++; }
|
|
|
|
|
+ if ($studentDelta > 0) { $studentUp++; } elseif ($studentDelta < 0) { $studentDown++; } else { $studentFlat++; }
|
|
|
|
|
+
|
|
|
|
|
+ $paperPct = $calcPct($paperDelta, $papersPrev);
|
|
|
|
|
+ $analysisPct = $calcPct($analysisDelta, $analysisPrev);
|
|
|
|
|
+ $studentPct = $calcPct($studentDelta, $studentPrev);
|
|
|
|
|
+
|
|
|
|
|
+ // 异常口径(用于快速巡检):
|
|
|
|
|
+ // 1) 学案/学情明显下滑;2) 学案有量但学情为 0;3) 学情明显高于学案;
|
|
|
|
|
+ // 4) 学案与学情方向背离;5) 覆盖学生数异常波动。
|
|
|
|
|
+ if (
|
|
|
|
|
+ ($papersPrev >= 20 && ($paperDelta <= -20 || ($paperPct !== null && $paperPct <= -50.0))) ||
|
|
|
|
|
+ ($analysisPrev >= 15 && ($analysisDelta <= -15 || ($analysisPct !== null && $analysisPct <= -50.0))) ||
|
|
|
|
|
+ ($papersCur >= 10 && $analysisCur === 0) ||
|
|
|
|
|
+ ($analysisCur >= 10 && $analysisCur > $papersCur + 5) ||
|
|
|
|
|
+ (($paperDelta > 0 && $analysisDelta < 0 && abs($analysisDelta) >= 5) ||
|
|
|
|
|
+ ($paperDelta < 0 && $analysisDelta > 0 && abs($paperDelta) >= 5)) ||
|
|
|
|
|
+ (abs($studentDelta) >= 10 || ($studentPct !== null && abs($studentPct) >= 100.0 && $studentPrev >= 5))
|
|
|
|
|
+ ) {
|
|
|
|
|
+ $severity = abs($paperDelta) + abs($analysisDelta) + abs($studentDelta);
|
|
|
|
|
+ if ($papersCur >= 10 && $analysisCur === 0) { $severity += 20; }
|
|
|
|
|
+ if ($analysisCur >= 10 && $analysisCur > $papersCur + 5) { $severity += 12; }
|
|
|
|
|
+
|
|
|
|
|
+ $reason = [];
|
|
|
|
|
+ if ($papersPrev >= 20 && ($paperDelta <= -20 || ($paperPct !== null && $paperPct <= -50.0))) { $reason[] = '学案下滑'; }
|
|
|
|
|
+ if ($analysisPrev >= 15 && ($analysisDelta <= -15 || ($analysisPct !== null && $analysisPct <= -50.0))) { $reason[] = '学情下滑'; }
|
|
|
|
|
+ if ($papersCur >= 10 && $analysisCur === 0) { $reason[] = '学案有量但学情为0'; }
|
|
|
|
|
+ if ($analysisCur >= 10 && $analysisCur > $papersCur + 5) { $reason[] = '学情高于学案'; }
|
|
|
|
|
+ if (($paperDelta > 0 && $analysisDelta < 0) || ($paperDelta < 0 && $analysisDelta > 0)) { $reason[] = '学案学情背离'; }
|
|
|
|
|
+ if (abs($studentDelta) >= 10 || ($studentPct !== null && abs($studentPct) >= 100.0 && $studentPrev >= 5)) { $reason[] = '学生覆盖波动'; }
|
|
|
|
|
+
|
|
|
|
|
+ $anomalies[] = [
|
|
|
|
|
+ 'severity' => $severity,
|
|
|
|
|
+ 'teacher_id' => $tidKey,
|
|
|
|
|
+ 'name' => $name,
|
|
|
|
|
+ 'papers_cur' => $papersCur,
|
|
|
|
|
+ 'papers_prev' => $papersPrev,
|
|
|
|
|
+ 'analysis_cur' => $analysisCur,
|
|
|
|
|
+ 'analysis_prev' => $analysisPrev,
|
|
|
|
|
+ 'student_cur' => $studentCur,
|
|
|
|
|
+ 'student_prev' => $studentPrev,
|
|
|
|
|
+ 'paper_delta_text' => $fmtDeltaWithPct($paperDelta, $papersPrev),
|
|
|
|
|
+ 'analysis_delta_text' => $fmtDeltaWithPct($analysisDelta, $analysisPrev),
|
|
|
|
|
+ 'student_delta_text' => $fmtDeltaWithPct($studentDelta, $studentPrev),
|
|
|
|
|
+ 'reason' => implode(' / ', array_values(array_unique($reason))),
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+usort($anomalies, static fn ($a, $b) => $b['severity'] <=> $a['severity']);
|
|
|
|
|
+
|
|
|
|
|
+$paperTrend = $paperUp > $paperDown ? '学案整体偏增长' : ($paperUp < $paperDown ? '学案整体偏下降' : '学案整体增降持平');
|
|
|
|
|
+$analysisTrend = $analysisUp > $analysisDown ? '学情整体偏增长' : ($analysisUp < $analysisDown ? '学情整体偏下降' : '学情整体增降持平');
|
|
|
|
|
+$studentTrend = $studentUp > $studentDown ? '学生覆盖整体偏增长' : ($studentUp < $studentDown ? '学生覆盖整体偏下降' : '学生覆盖整体增降持平');
|
|
|
|
|
+
|
|
|
|
|
+echo "### 老师列表汇总(增降对比)\n\n";
|
|
|
|
|
+echo "| 维度 | 增长老师数 | 下降老师数 | 持平老师数 | 判断 |\n";
|
|
|
|
|
+echo "| :--- | ---: | ---: | ---: | :--- |\n";
|
|
|
|
|
+echo sprintf("| 学案(组卷) | %d | %d | %d | %s |\n", $paperUp, $paperDown, $paperFlat, $paperTrend);
|
|
|
|
|
+echo sprintf("| 学情(分析) | %d | %d | %d | %s |\n", $analysisUp, $analysisDown, $analysisFlat, $analysisTrend);
|
|
|
|
|
+echo sprintf("| 学生覆盖(去重) | %d | %d | %d | %s |\n\n", $studentUp, $studentDown, $studentFlat, $studentTrend);
|
|
|
|
|
+
|
|
|
|
|
+echo "### 异常数据(学案本/上超过100,文字清单)\n\n";
|
|
|
|
|
+$highVolumeAnomalies = array_values(array_filter(
|
|
|
|
|
+ $anomalies,
|
|
|
|
|
+ static fn (array $a): bool => ((int) ($a['papers_cur'] ?? 0) > 100) || ((int) ($a['papers_prev'] ?? 0) > 100)
|
|
|
|
|
+));
|
|
|
|
|
+if ($highVolumeAnomalies === []) {
|
|
|
|
|
+ echo "本周期未命中“学案本/上超过100”的异常老师。\n\n";
|
|
|
|
|
+} else {
|
|
|
|
|
+ $display = array_slice($highVolumeAnomalies, 0, 30);
|
|
|
|
|
+ $idx = 1;
|
|
|
|
|
+ foreach ($display as $a) {
|
|
|
|
|
+ $nameEsc = htmlspecialchars((string) $a['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $tidEsc = htmlspecialchars((string) $a['teacher_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $reasonEsc = htmlspecialchars((string) $a['reason'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $line = sprintf(
|
|
|
|
|
+ '%d) %s(%s):学案 %d/%d(%s);学情 %d/%d(%s);学生 %d/%d(%s);异常:%s',
|
|
|
|
|
+ $idx++,
|
|
|
|
|
+ $nameEsc,
|
|
|
|
|
+ $tidEsc,
|
|
|
|
|
+ (int) $a['papers_cur'],
|
|
|
|
|
+ (int) $a['papers_prev'],
|
|
|
|
|
+ (string) $a['paper_delta_text'],
|
|
|
|
|
+ (int) $a['analysis_cur'],
|
|
|
|
|
+ (int) $a['analysis_prev'],
|
|
|
|
|
+ (string) $a['analysis_delta_text'],
|
|
|
|
|
+ (int) $a['student_cur'],
|
|
|
|
|
+ (int) $a['student_prev'],
|
|
|
|
|
+ (string) $a['student_delta_text'],
|
|
|
|
|
+ $reasonEsc
|
|
|
|
|
+ );
|
|
|
|
|
+ echo '- '.$line."\n";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (count($highVolumeAnomalies) > 30) {
|
|
|
|
|
+ echo sprintf("\n> 仅展示前 30 条,共 %d 条。\n", count($highVolumeAnomalies));
|
|
|
|
|
+ }
|
|
|
|
|
+ echo "\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+echo sprintf("### 按老师 本周期有组卷 **%d** 人。\n\n", count($rows));
|
|
|
|
|
+
|
|
|
|
|
+echo '<table class="weekly-teacher-table">';
|
|
|
|
|
+echo '<colgroup>';
|
|
|
|
|
+echo '<col style="width:3%" /><col class="col-name" style="width:18mm;max-width:18mm;" />';
|
|
|
|
|
+echo '<col class="col-slash" style="width:11%" /><col style="width:14%" />';
|
|
|
|
|
+echo '<col class="col-slash" style="width:11%" /><col style="width:14%" />';
|
|
|
|
|
+echo '<col class="col-slash" style="width:11%" />';
|
|
|
|
|
+echo '</colgroup>';
|
|
|
|
|
+echo '<thead><tr>';
|
|
|
|
|
+echo '<th>排名</th><th>老师</th><th>学案数量</th><th>学案·环比</th><th>分析数量</th><th>学情·环比</th><th>学生数</th>';
|
|
|
|
|
+echo "</tr></thead>\n<tbody>\n";
|
|
|
|
|
+
|
|
|
|
|
+$i = 1;
|
|
|
|
|
+foreach ($rows as $r) {
|
|
|
|
|
+ $tidKey = (string) $r['teacher_id'];
|
|
|
|
|
+ $nmRaw = (string) $r['name'];
|
|
|
|
|
+ $nmEsc = htmlspecialchars($nmRaw, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $tidEsc = htmlspecialchars($tidKey, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $nameWithId = $nmEsc.' <span class="teacher-id">('.$tidEsc.')</span>';
|
|
|
|
|
+ $pc = $r['papers'];
|
|
|
|
|
+ $pp = $r['papers_prev'];
|
|
|
|
|
+ $ac = $r['analysis_sets'];
|
|
|
|
|
+ $ap = $r['analysis_sets_prev'];
|
|
|
|
|
+ $stuC = (int) ($studentUnionCurMap[$tidKey] ?? 0);
|
|
|
|
|
+ $stuP = (int) ($studentUnionPrevMap[$tidKey] ?? 0);
|
|
|
|
|
+ echo '<tr>';
|
|
|
|
|
+ echo '<td>'.((string) $i++).'</td>';
|
|
|
|
|
+ echo '<td class="td-name">'.$nameWithId.'</td>';
|
|
|
|
|
+ echo '<td class="td-slash td-slash-papers" title="本周期 / 上周期:组卷套数">'.$slashPairAccentHtml($pc, $pp, $colorPapers).'</td>';
|
|
|
|
|
+ echo '<td class="td-wow-papers">'.$compareCellHtml($pc, $pp, $colorPapers).'</td>';
|
|
|
|
|
+ echo '<td class="td-slash td-slash-analysis" title="本周期 / 上周期:学情分析套数(卷去重)">'.$slashPairAccentHtml($ac, $ap, $colorAnalysis).'</td>';
|
|
|
|
|
+ echo '<td class="td-wow-analysis">'.$compareCellHtml($ac, $ap, $colorAnalysis).'</td>';
|
|
|
|
|
+ echo '<td class="td-slash td-slash-stu td-stu" title="本周期 / 上周期:组卷∪学情学生合并去重">'.$slashPairAccentHtml($stuC, $stuP, $colorStudents).'</td>';
|
|
|
|
|
+ echo "</tr>\n";
|
|
|
|
|
+}
|
|
|
|
|
+echo "</tbody></table>\n";
|