chore: 合并为单仓库,统一部署流程

- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 16:02:12 +08:00
1 parent 70983a12a5
commit 47d4679984
40 files changed
+13723 -34

No files matched your search

@@ -0,0 +1,304 @@
# 人气前五 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"
```