# 学生档案报告前端预览 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 将档案报告从后端 Puppeteer 生成 PDF 改为后端生成 HTML、前端新窗口预览、浏览器打印出 PDF。

**Architecture:** 后端新增 `/archive/:studentId/report-html` 返回 HTML 字符串（复用现有 `buildHtml()` 方法），前端 `fetch` 后在新窗口渲染。移除 Puppeteer 依赖和原 PDF 下载端点。

**Tech Stack:** NestJS 11 + TypeORM + React 19 + Ant Design 6

## Global Constraints

- 复用现有 `buildHtml()` / `buildCover()` / `css()` 等 HTML 构造方法，不做任何样式改动
- 所有写操作记录日志
- 前端遵循现有页面模式
- Docker 镜像需移除 Chromium 相关依赖

---

### Task 1: 后端新增 report-html 接口

**Files:**
- Modify: `apps/server/src/archive/archive-report.service.ts`
- Modify: `apps/server/src/archive/archive.controller.ts`

**Interfaces:**
- Produces: `ArchiveReportService.generateReportHtml(studentId: number): Promise<string>`
- Produces: `GET /archive/:studentId/report-html` → `{ html: string }`

- [ ] **Step 1: 在 ArchiveReportService 新增 generateReportHtml 方法**

在 `archive-report.service.ts` 的 `generateReport` 方法之后，新增：

```typescript
async generateReportHtml(studentId: number): Promise<string> {
  const [student, profile, enrollments, exams, learnings, result, attendances] =
    await Promise.all([
      this.studentRepo.findOne({ where: { id: studentId } }),
      this.profileRepo.findOne({ where: { studentId } }),
      this.enrollmentRepo.find({ where: { studentId }, order: { startDate: 'ASC' } }),
      this.examRepo.find({ where: { studentId }, order: { examDate: 'ASC' } }),
      this.learningRepo.find({ where: { studentId }, order: { recordDate: 'DESC' } }),
      this.resultRepo.findOne({ where: { studentId } }),
      this.attendanceRepo.find({ where: { studentId }, order: { attendanceDate: 'ASC' } }),
    ]);

  if (!student) throw new Error('学生不存在');

  const data: ReportData = {
    student,
    profile,
    enrollments,
    exams,
    learnings,
    result,
    attendances,
  };

  return this.buildHtml(data);
}
```

- [ ] **Step 2: 在 ArchiveController 新增 report-html 端点**

在 `archive.controller.ts` 中，`generateReport` 方法之后新增：

```typescript
@Get(':studentId/report-html')
@RequirePermission('student:view')
async getReportHtml(
  @Param('studentId') studentId: string,
  @Request() req: AuthenticatedRequest,
) {
  const { ipAddress, userAgent } = extractRequestInfo(req);
  await this.logService.log({
    userId: req.user?.id,
    username: req.user?.username,
    module: 'archive',
    action: 'preview_report',
    targetId: +studentId,
    targetType: 'student',
    ipAddress,
    userAgent,
  });
  const html = await this.reportService.generateReportHtml(+studentId);
  return { html };
}
```

- [ ] **Step 3: 验证后端接口**

```bash
# 启动后端后测试
curl -H "Authorization: Bearer <token>" http://localhost:3000/api/archive/1/report-html
```

预期返回 `{ "html": "<!DOCTYPE html>..." }`，HTML 内容完整。

- [ ] **Step 4: Commit**

```bash
git add apps/server/src/archive/archive-report.service.ts apps/server/src/archive/archive.controller.ts
git commit -m "feat: add GET /archive/:studentId/report-html endpoint"
```

---

### Task 2: 前端按钮改为预览报告

**Files:**
- Modify: `apps/admin/src/pages/StudentProfile/index.tsx`

**Interfaces:**
- Consumes: `GET /archive/:studentId/report-html` → `{ html: string }`

- [ ] **Step 1: 修改按钮行为**

将 `handleDownloadReport` 替换为 `handlePreviewReport`，打开新窗口渲染 HTML：

```typescript
const handlePreviewReport = async () => {
  const token = localStorage.getItem('token');
  const res = await fetch(`/api/archive/${studentId}/report-html`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const { html } = await res.json();
  const w = window.open('', '_blank');
  if (w) {
    w.document.write(html);
    w.document.close();
  }
};
```

将按钮文本和图标改为预览：

```tsx
extra={
  <PermissionButton
    permission="student:view"
    type="primary"
    icon={<EyeOutlined />}
    onClick={handlePreviewReport}
  >
    预览报告
  </PermissionButton>
}
```

需要在文件顶部 import 添加 `EyeOutlined`（已有 `DownloadOutlined` 可删除）。

- [ ] **Step 2: 浏览器验证**

```bash
# 启动前端 dev server
cd apps/admin && npm run dev
```

1. 打开学生档案页面
2. 点击「预览报告」按钮
3. 确认新窗口打开完整报告，样式正确
4. 新窗口 Ctrl+P → 确认打印预览分页正常

- [ ] **Step 3: Commit**

```bash
git add apps/admin/src/pages/StudentProfile/index.tsx
git commit -m "feat: change report button from download to preview"
```

---

### Task 3: 移除 Puppeteer 和旧 PDF 下载端点

**Files:**
- Modify: `apps/server/src/archive/archive-report.service.ts`
- Modify: `apps/server/src/archive/archive.controller.ts`
- Modify: `apps/server/package.json`
- Modify: `apps/server/Dockerfile`

**Interfaces:**
- Removes: `ArchiveReportService.generateReport(studentId, res)` — Puppeteer PDF 生成
- Removes: `GET /archive/:studentId/report` — PDF 下载端点
- Removes: `puppeteer` npm 依赖

- [ ] **Step 1: 删除 generateReport 方法**

在 `archive-report.service.ts` 中删除 `generateReport(res: Response)` 方法（第 36-85 行），包括方法内所有 Puppeteer 相关逻辑。

同步删除文件顶部的两个不再需要的 import：

```typescript
// 删除这两行
import puppeteer from 'puppeteer';
import { Response } from 'express';
```

- [ ] **Step 2: 删除旧的 report 端点**

在 `archive.controller.ts` 中删除 `GET /archive/:studentId/report` 的 `generateReport` 方法（第 343-362 行）。

同步删除 `@Res` 装饰器的 import（检查 `@Res` 是否被其他地方使用，如果只在 `generateReport` 中使用，则一并移除）。

- [ ] **Step 3: 移除 puppeteer 依赖**

```bash
cd apps/server && npm uninstall puppeteer
```

- [ ] **Step 4: 清理 Dockerfile**

读取 `apps/server/Dockerfile`，移除 Chromium/ Puppeteer 相关依赖安装。常见需要移除的：
- `chromium` / `chromium-browser` 等包
- Puppeteer 相关环境变量如 `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`

- [ ] **Step 5: 验证构建**

```bash
cd apps/server && npm run build
```

确认编译通过，无 puppeteer 相关 import 错误。

- [ ] **Step 6: Commit**

```bash
git add apps/server/src/archive/archive-report.service.ts apps/server/src/archive/archive.controller.ts apps/server/package.json apps/server/package-lock.json apps/server/Dockerfile
git commit -m "refactor: remove Puppeteer, use frontend browser print for PDF"
```

---

### Task 4: 端到端验证

- [ ] **Step 1: 启动完整环境**

```bash
docker compose up -d
```

- [ ] **Step 2: 验证功能**

1. 登录系统 → 学生列表 → 进入某个学生档案
2. 点击「预览报告」→ 新窗口打开
3. 检查报告完整性：封面、基础信息、考试成绩总览（含 SVG 趋势图）、出勤记录（含 SVG 柱状图）、文化课明细、学情记录与录取归档
4. 新窗口 Ctrl+P → 另存为 PDF
5. 确认 PDF 内容与预览一致

- [ ] **Step 3: 验证无回归**

- 基础档案 Tab CRUD 正常
- 报读记录/考试成绩/学情记录/录取结果 增删改正常
- 附件上传/删除正常

---

## Self-Review

**1. Spec coverage:**
- [x] 新增 `/report-html` 端点 → Task 1 Step 2
- [x] 复用现有 buildHtml → Task 1 Step 1
- [x] 删除 Puppeteer 和旧端点 → Task 3
- [x] 前端按钮改为预览 → Task 2
- [x] 验收标准全部覆盖 → Task 4

**2. Placeholder scan:** 无 TBD/TODO/占位符。

**3. Type consistency:** `generateReportHtml` 签名在三处一致：Service 定义、Controller 调用、接口文档。
