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

8.9 KiB
Raw Permalink Blame History

人气前五 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 之前)插入:

class TopViewedItem(Schema):
    username: str
    classname: str
    view_count: int
    submission_id: str
  • Step 2: 在 TaskStatsOut 末尾新增 top_viewed 字段

将 TaskStatsOut(当前第 141-151 行)更新为:

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 无语法错误
cd /home/xuyue/Projects/webpreview/api
uv run python -c "from submission.schemas import TaskStatsOut, TopViewedItem; print('OK')"

Expected output: OK

  • Step 4: Commit
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 行)更新为:

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( 之前)插入:

    # 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 行)更新为:

    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: 启动开发服务器,验证接口返回
cd /home/xuyue/Projects/webpreview/api
uv run manage.py runserver

另开终端,用已有管理员账号 cookie 或 session 测试(替换 <task_id> 和 <session>):

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
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 之前)插入:

export interface TopViewedItem {
  username: string
  classname: string
  view_count: number
  submission_id: string
}
  • Step 2: 在 TaskStatsOut 末尾新增 top_viewed 字段

将 TaskStatsOut(当前第 137-148 行)更新为:

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
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> 之前插入:

          <!-- 人气前五 -->
          <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 行:

import type { TaskStatsOut } from "../utils/type"

TopViewedItem 作为 TaskStatsOut.top_viewed 的元素类型,无需单独导入。

  • Step 3: 启动前端开发服务器,打开任意 task 的提交统计弹窗
cd /home/xuyue/Projects/webpreview/client
npm run start

在浏览器中打开 Dashboard,进入任意 tutorial/challenge,点击"提交统计",向下滚动确认:

  • 出现"人气前五"标题

  • 列表按 view_count 降序排列,最多 5 项

  • 点击某项会在新标签打开对应提交详情

  • 切换班级筛选后列表随之更新

  • Step 4: Commit

git add client/src/components/TaskStatsModal.vue
git commit -m "feat: add top-viewed-5 section to TaskStatsModal"