Files
web/docs/superpowers/plans/2026-03-31-top-viewed-stats.md
xuyueandClaude Opus 5.5 47d4679984 chore: 合并为单仓库,统一部署流程
- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:02:12 +08:00

305 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 人气前五 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:** 在提交统计弹窗中新增"人气前五"区块,展示当前 task 下 view_count 最高的前 5 条提交,跟随班级筛选。
**Architecture:** 后端在现有 `get_task_stats` 端点中新增 `top_viewed` 查询,前端在 `TaskStatsModal` 底部新增对应 UI 区块。全程复用已有数据流,不新增 API 路由。
**Tech Stack:** Python/Django-Ninja (backend schema + query), TypeScript (type definition), Vue 3 + Naive UI (frontend component)
---
### Task 1: 后端 Schema — 新增 `TopViewedItem`,更新 `TaskStatsOut`
**Files:**
- Modify: `api/submission/schemas.py:134-151`
- [ ] **Step 1: 在 `FlagStats` 之前插入 `TopViewedItem` class**
在 `api/submission/schemas.py` 第 134 行(`class FlagStats` 之前)插入:
```python
class TopViewedItem(Schema):
username: str
classname: str
view_count: int
submission_id: str
```
- [ ] **Step 2: 在 `TaskStatsOut` 末尾新增 `top_viewed` 字段**
将 `TaskStatsOut`(当前第 141-151 行)更新为:
```python
class TaskStatsOut(Schema):
submitted_count: int
unsubmitted_count: int
average_score: Optional[float]
unrated_count: int
unsubmitted_users: list[UserTag]
unrated_users: list[UserTag]
submission_count_distribution: SubmissionCountBucket
flag_stats: FlagStats
classes: list[str]
top_viewed: list[TopViewedItem]
```
- [ ] **Step 3: 验证 Schema 无语法错误**
```bash
cd /home/xuyue/Projects/webpreview/api
uv run python -c "from submission.schemas import TaskStatsOut, TopViewedItem; print('OK')"
```
Expected output: `OK`
- [ ] **Step 4: Commit**
```bash
git add api/submission/schemas.py
git commit -m "feat: add TopViewedItem schema and top_viewed field to TaskStatsOut"
```
---
### Task 2: 后端查询 — 在 `get_task_stats` 中计算并返回 `top_viewed`
**Files:**
- Modify: `api/submission/api.py:13-23` (imports)
- Modify: `api/submission/api.py:306-330` (flag_stats 之后 → return 之前,以及 return 语句)
- [ ] **Step 1: 在 import 中新增 `TopViewedItem`**
将 `api/submission/api.py` 顶部的 import(第 13-23 行)更新为:
```python
from .schemas import (
FlagIn,
FlagStats,
SubmissionCountBucket,
SubmissionFilter,
SubmissionIn,
SubmissionOut,
RatingScoreIn,
TaskStatsOut,
TopViewedItem,
UserTag,
)
```
- [ ] **Step 2: 在 `flag_stats =` 赋值之后、`return TaskStatsOut(...)` 之前插入 top_viewed 查询**
在 `api/submission/api.py` 第 318 行(`return TaskStatsOut(` 之前)插入:
```python
# Top 5 submissions by view_count (within filtered student_ids)
top_viewed_qs = (
Submission.objects
.filter(task=task, user_id__in=student_ids)
.select_related("user")
.order_by("-view_count")[:5]
)
top_viewed = [
TopViewedItem(
username=s.user.username,
classname=s.user.classname,
view_count=s.view_count,
submission_id=str(s.id),
)
for s in top_viewed_qs
]
```
- [ ] **Step 3: 在 `return TaskStatsOut(...)` 中新增 `top_viewed=top_viewed`**
将返回语句(当前第 320-330 行)更新为:
```python
return TaskStatsOut(
submitted_count=submitted_count,
unsubmitted_count=unsubmitted_count,
average_score=average_score,
unrated_count=unrated_count,
unsubmitted_users=unsubmitted_users,
unrated_users=unrated_users,
submission_count_distribution=SubmissionCountBucket(**dist),
flag_stats=flag_stats,
classes=all_classes,
top_viewed=top_viewed,
)
```
- [ ] **Step 4: 启动开发服务器,验证接口返回**
```bash
cd /home/xuyue/Projects/webpreview/api
uv run manage.py runserver
```
另开终端,用已有管理员账号 cookie 或 session 测试(替换 `<task_id>` 和 `<session>`):
```bash
curl -s -H "Cookie: sessionid=<session>" \
http://127.0.0.1:8000/api/submission/stats/<task_id> \
| python3 -m json.tool | grep -A 20 '"top_viewed"'
```
Expected: 看到 `"top_viewed": [...]`,每项含 `username`、`classname`、`view_count`、`submission_id`。
- [ ] **Step 5: Commit**
```bash
git add api/submission/api.py
git commit -m "feat: query top 5 by view_count in get_task_stats"
```
---
### Task 3: 前端类型 — 新增 `TopViewedItem` 接口,更新 `TaskStatsOut`
**Files:**
- Modify: `client/src/utils/type.ts:136-148`
- [ ] **Step 1: 在 `TaskStatsOut` 之前插入 `TopViewedItem` 接口**
在 `client/src/utils/type.ts` 第 136 行(`export interface TaskStatsOut` 之前)插入:
```ts
export interface TopViewedItem {
username: string
classname: string
view_count: number
submission_id: string
}
```
- [ ] **Step 2: 在 `TaskStatsOut` 末尾新增 `top_viewed` 字段**
将 `TaskStatsOut`(当前第 137-148 行)更新为:
```ts
export interface TaskStatsOut {
submitted_count: number
unsubmitted_count: number
average_score: number | null
unrated_count: number
unsubmitted_users: UserTag[]
unrated_users: UserTag[]
submission_count_distribution: SubmissionCountBucket
score_distribution: ScoreBucket
flag_stats: FlagStats
classes: string[]
top_viewed: TopViewedItem[]
}
```
- [ ] **Step 3: Commit**
```bash
git add client/src/utils/type.ts
git commit -m "feat: add TopViewedItem type and top_viewed to TaskStatsOut"
```
---
### Task 4: 前端 UI — 在 `TaskStatsModal` 中新增"人气前五"区块
**Files:**
- Modify: `client/src/components/TaskStatsModal.vue`
- [ ] **Step 1: 在模板的"标记统计"区块(`<!-- 标记统计 -->`,第 282 行附近)之后新增"人气前五"区块**
找到 `</n-spin>` 之前的 `<!-- 标记统计 -->` 区块末尾的 `</div>`(第 325 行附近),在它后面、`</n-spin>` 之前插入:
```html
<!-- 人气前五 -->
<div style="margin-top: 12px">
<div
style="
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
color: #333;
"
>
人气前五
</div>
<div
v-if="stats.top_viewed.length === 0"
style="color: #aaa; font-size: 12px"
>
暂无
</div>
<div v-else style="display: flex; flex-direction: column; gap: 6px">
<div
v-for="(item, i) in stats.top_viewed"
:key="item.submission_id"
style="
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border: 1px solid #eee;
border-radius: 6px;
cursor: pointer;
background: #fafafa;
"
@click="viewSubmission(item.submission_id)"
>
<span
:style="{
width: '20px',
height: '20px',
borderRadius: '50%',
background: i < 3 ? ['#f0a020', '#888', '#a07040'][i] : '#ddd',
color: '#fff',
fontSize: '11px',
fontWeight: '700',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: '0',
}"
>{{ i + 1 }}</span>
<span style="flex: 1; font-size: 13px; color: #333">
{{ displayName(item.username, item.classname) }}
</span>
<span style="font-size: 12px; color: #2080f0; font-weight: 600">
{{ item.view_count }} 次
</span>
</div>
</div>
</div>
```
- [ ] **Step 2: 在 `<script>` 中确认 `TopViewedItem` 类型已被 `TaskStatsOut` 携带(无需额外 import)**
打开 `client/src/components/TaskStatsModal.vue`,确认顶部 import 行:
```ts
import type { TaskStatsOut } from "../utils/type"
```
`TopViewedItem` 作为 `TaskStatsOut.top_viewed` 的元素类型,无需单独导入。
- [ ] **Step 3: 启动前端开发服务器,打开任意 task 的提交统计弹窗**
```bash
cd /home/xuyue/Projects/webpreview/client
npm run start
```
在浏览器中打开 Dashboard,进入任意 tutorial/challenge,点击"提交统计",向下滚动确认:
- 出现"人气前五"标题
- 列表按 view_count 降序排列,最多 5 项
- 点击某项会在新标签打开对应提交详情
- 切换班级筛选后列表随之更新
- [ ] **Step 4: Commit**
```bash
git add client/src/components/TaskStatsModal.vue
git commit -m "feat: add top-viewed-5 section to TaskStatsModal"
```