|
|
@@ -209,6 +209,170 @@ $windowPrev = sprintf(
|
|
|
$generatedAt = $endCurrent->format('Y-m-d H:i:s');
|
|
|
$tz = (string) config('app.timezone', 'UTC');
|
|
|
|
|
|
+/**
|
|
|
+ * 将 [from, to) 均分为 7 段,返回每段内组卷数、学情去重套数。
|
|
|
+ *
|
|
|
+ * @return array{labels: list<string>, papers: list<int>, analysis: list<int>}
|
|
|
+ */
|
|
|
+$dailySlices = static function (\Carbon\Carbon $from, \Carbon\Carbon $toExclusive) use ($db): array {
|
|
|
+ $totalSec = max(1, $toExclusive->getTimestamp() - $from->getTimestamp());
|
|
|
+ $sliceSec = $totalSec / 7;
|
|
|
+ $labels = [];
|
|
|
+ $papers = [];
|
|
|
+ $analysis = [];
|
|
|
+ for ($i = 0; $i < 7; $i++) {
|
|
|
+ $sliceFrom = $from->copy()->addSeconds((int) floor($sliceSec * $i));
|
|
|
+ $sliceTo = $i < 6
|
|
|
+ ? $from->copy()->addSeconds((int) floor($sliceSec * ($i + 1)))
|
|
|
+ : $toExclusive;
|
|
|
+ $labels[] = $sliceFrom->format('n/j');
|
|
|
+ $papers[] = (int) $db::table('papers')
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
+ ->where('created_at', '>=', $sliceFrom)
|
|
|
+ ->where('created_at', '<', $sliceTo)
|
|
|
+ ->count();
|
|
|
+ $analysis[] = (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', '>=', $sliceFrom)
|
|
|
+ ->where('ear.created_at', '<', $sliceTo)
|
|
|
+ ->distinct()
|
|
|
+ ->count('ear.paper_id');
|
|
|
+ }
|
|
|
+
|
|
|
+ return ['labels' => $labels, 'papers' => $papers, 'analysis' => $analysis];
|
|
|
+};
|
|
|
+
|
|
|
+$curDaily = $dailySlices($startCurrent, $endCurrent);
|
|
|
+$prevDaily = $dailySlices($startPrev, $endPrev);
|
|
|
+
|
|
|
+$buildChartSvg = static function (array $curDaily, array $prevDaily): string {
|
|
|
+ $labels = $curDaily['labels'];
|
|
|
+ $pc = $curDaily['papers'];
|
|
|
+ $ac = $curDaily['analysis'];
|
|
|
+ $pp = $prevDaily['papers'];
|
|
|
+ $ap = $prevDaily['analysis'];
|
|
|
+ $maxY = max(1, ...$pc, ...$ac, ...$pp, ...$ap);
|
|
|
+ $W = 480;
|
|
|
+ $H = 200;
|
|
|
+ $padL = 44;
|
|
|
+ $padR = 12;
|
|
|
+ $padT = 16;
|
|
|
+ $padB = 36;
|
|
|
+ $gw = $W - $padL - $padR;
|
|
|
+ $gh = $H - $padT - $padB;
|
|
|
+ $n = 7;
|
|
|
+ $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;
|
|
|
+ };
|
|
|
+
|
|
|
+ $xAxisY = $padT + $gh;
|
|
|
+ $tickTxt = '';
|
|
|
+ for ($i = 0; $i < $n; $i++) {
|
|
|
+ $x = $xAt($i);
|
|
|
+ $lab = htmlspecialchars((string) ($labels[$i] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
+ $tickTxt .= sprintf(
|
|
|
+ '<text x="%.1f" y="%d" text-anchor="middle" font-size="9" fill="#374151" font-family="sun-exta,sans-serif">%s</text>',
|
|
|
+ $x,
|
|
|
+ $H - 10,
|
|
|
+ $lab
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ $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>',
|
|
|
+ 4,
|
|
|
+ $y + 3,
|
|
|
+ $v
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ $legend = '<g font-family="sun-exta,sans-serif" font-size="9">';
|
|
|
+ $lx = $padL + $gw - 168;
|
|
|
+ $ly = $padT + 4;
|
|
|
+ $items = [
|
|
|
+ ['#2563eb', '组卷·本周期', ''],
|
|
|
+ ['#ea580c', '学情·本周期', ''],
|
|
|
+ ['#93c5fd', '组卷·上周期', '6,4'],
|
|
|
+ ['#fdba74', '学情·上周期', '6,4'],
|
|
|
+ ];
|
|
|
+ foreach ($items as $idx => $it) {
|
|
|
+ $yy = $ly + $idx * 13;
|
|
|
+ $legend .= sprintf(
|
|
|
+ '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="2" %s/>',
|
|
|
+ $lx,
|
|
|
+ $yy,
|
|
|
+ $lx + 18,
|
|
|
+ $yy,
|
|
|
+ $it[0],
|
|
|
+ $it[2] !== '' ? 'stroke-dasharray="'.$it[2].'"' : ''
|
|
|
+ );
|
|
|
+ $legend .= sprintf(
|
|
|
+ '<text x="%d" y="%d" fill="#111">%s</text>',
|
|
|
+ $lx + 24,
|
|
|
+ $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:520px;">',
|
|
|
+ $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($pp, '#93c5fd', '6,4', 1.4);
|
|
|
+ $svg .= $lineWithDots($ap, '#fdba74', '6,4', 1.4);
|
|
|
+ $svg .= $lineWithDots($pc, '#2563eb', '', 1.8);
|
|
|
+ $svg .= $lineWithDots($ac, '#ea580c', '', 1.8);
|
|
|
+ $svg .= $tickTxt;
|
|
|
+ $svg .= $legend;
|
|
|
+ $svg .= '</svg>';
|
|
|
+
|
|
|
+ return $svg;
|
|
|
+};
|
|
|
+
|
|
|
+$chartSvg = $buildChartSvg($curDaily, $prevDaily);
|
|
|
+
|
|
|
echo "## 老师组卷与学情分析(近7天)\n\n";
|
|
|
echo "> 生成 {$generatedAt} · {$tz} · 本 {$windowCur} · 上 {$windowPrev}\n\n";
|
|
|
|
|
|
@@ -219,30 +383,43 @@ echo sprintf("| 组卷总套数 | %d | %d | %s |\n", $totalPapersCur, $totalPape
|
|
|
echo sprintf("| 学情分析套数(卷去重) | %d | %d | %s |\n", $totalAnalysisCur, $totalAnalysisPrev, $wowLineHtml($totalAnalysisCur, $totalAnalysisPrev));
|
|
|
echo sprintf("| 有组卷老师数 | %d | %d | %s |\n", $teachersCur, $teachersPrev, $wowLineHtml($teachersCur, $teachersPrev));
|
|
|
|
|
|
-echo "\n### 按老师\n\n";
|
|
|
+echo "\n### 近7段每日对比(本周期与上周期时间轴对齐)\n\n";
|
|
|
+echo '<div class="weekly-chart">';
|
|
|
+echo '<p style="font-size:9pt;color:#4b5563;margin:0 0 6px 0;">横轴为统计窗口均分的 7 段,数字为每段「组卷套数 / 学情分析套数(卷去重)」;实线本周期,虚线上一同期。</p>';
|
|
|
+echo $chartSvg;
|
|
|
+echo "</div>\n\n";
|
|
|
+
|
|
|
+echo "### 按老师\n\n";
|
|
|
|
|
|
-echo "| 排名 | 老师 | teacher_id | 组卷·本 | 组卷·上 | 组卷·环比 | 学情·本 | 学情·上 | 学情·环比 |\n";
|
|
|
-echo "| ---: | --- | --- | ---: | ---: | --- | ---: | ---: | --- |\n";
|
|
|
+echo '<table class="weekly-teacher-table">';
|
|
|
+echo '<colgroup>';
|
|
|
+echo '<col style="width:5%" /><col style="width:6%" /><col style="width:8%" />';
|
|
|
+echo '<col style="width:8%" /><col style="width:8%" /><col style="width:10%" />';
|
|
|
+echo '<col class="col-an" style="width:11%" /><col class="col-an" style="width:11%" /><col style="width:9%" />';
|
|
|
+echo '</colgroup>';
|
|
|
+echo '<thead><tr>';
|
|
|
+echo '<th>排名</th><th>老师</th><th>teacher_id</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) {
|
|
|
- $nm = str_replace(['|', "\n", '<', '>'], ['/', ' ', '', ''], $r['name']);
|
|
|
+ $nm = htmlspecialchars((string) $r['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
$pc = $r['papers'];
|
|
|
$pp = $r['papers_prev'];
|
|
|
$ac = $r['analysis_sets'];
|
|
|
$ap = $r['analysis_sets_prev'];
|
|
|
- echo sprintf(
|
|
|
- "| %d | %s | %s | %d | %d | %s | %d | %d | %s |\n",
|
|
|
- $i++,
|
|
|
- $nm,
|
|
|
- $r['teacher_id'],
|
|
|
- $pc,
|
|
|
- $pp,
|
|
|
- $compareCellHtml($pc, $pp),
|
|
|
- $ac,
|
|
|
- $ap,
|
|
|
- $compareCellHtml($ac, $ap)
|
|
|
- );
|
|
|
+ echo '<tr>';
|
|
|
+ echo '<td style="text-align:right">'.((string) $i++).'</td>';
|
|
|
+ echo '<td class="td-name">'.$nm.'</td>';
|
|
|
+ echo '<td>'.htmlspecialchars((string) $r['teacher_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').'</td>';
|
|
|
+ echo '<td style="text-align:right">'.((string) $pc).'</td>';
|
|
|
+ echo '<td style="text-align:right">'.((string) $pp).'</td>';
|
|
|
+ echo '<td>'.$compareCellHtml($pc, $pp).'</td>';
|
|
|
+ echo '<td style="text-align:right" class="td-an">'.((string) $ac).'</td>';
|
|
|
+ echo '<td style="text-align:right" class="td-an">'.((string) $ap).'</td>';
|
|
|
+ echo '<td>'.$compareCellHtml($ac, $ap).'</td>';
|
|
|
+ echo "</tr>\n";
|
|
|
}
|
|
|
+echo "</tbody></table>\n";
|
|
|
|
|
|
echo sprintf("\n本周期有组卷 **%d** 人。\n", count($rows));
|