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:
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"
|
||||
```
|
||||
Reference in new issue
Block a user