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,454 @@
|
||||
# Submission Flag Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add colored flag markers (red/blue/green/yellow) to submissions, settable by teachers/admins, visible to all users, with list filtering support.
|
||||
|
||||
**Architecture:** Add a nullable `flag` CharField to the Submission model. New PUT endpoint for setting/clearing flags with role check. Frontend shows flag icons in the table with a popover for admins to pick colors, plus a dropdown filter.
|
||||
|
||||
**Tech Stack:** Django + django-ninja (backend), Vue 3 + Naive UI + TypeScript (frontend)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `flag` field to Submission model
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/submission/models.py:1-13` (add FLAG_CHOICES and field)
|
||||
|
||||
**Step 1: Add FLAG_CHOICES and field to model**
|
||||
|
||||
In `api/submission/models.py`, add after the imports (before the class):
|
||||
|
||||
```python
|
||||
class FlagChoices(models.TextChoices):
|
||||
RED = "red", "值得展示"
|
||||
BLUE = "blue", "需要讲解"
|
||||
GREEN = "green", "优秀作品"
|
||||
YELLOW = "yellow", "需要改进"
|
||||
```
|
||||
|
||||
In the `Submission` class, add after the `conversation` field (line 33):
|
||||
|
||||
```python
|
||||
flag = models.CharField(
|
||||
max_length=10,
|
||||
choices=FlagChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name="标记",
|
||||
)
|
||||
```
|
||||
|
||||
**Step 2: Create and run migration**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd api && uv run manage.py makemigrations submission && uv run manage.py migrate
|
||||
```
|
||||
|
||||
Expected: Migration created and applied successfully.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add api/submission/models.py api/submission/migrations/
|
||||
git commit -m "feat: add flag field to Submission model"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add flag to API schemas and filter
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/submission/schemas.py`
|
||||
|
||||
**Step 1: Update SubmissionOut schema**
|
||||
|
||||
In `api/submission/schemas.py`, add to the `SubmissionOut` class fields (after line 27 `conversation_id`):
|
||||
|
||||
```python
|
||||
flag: Optional[str] = None
|
||||
```
|
||||
|
||||
**Step 2: Update SubmissionOut.list() static method**
|
||||
|
||||
Add `"flag": submission.flag,` to the return dict in `list()` (after the `conversation_id` line):
|
||||
|
||||
```python
|
||||
"flag": submission.flag,
|
||||
```
|
||||
|
||||
**Step 3: Update SubmissionOut.get() static method**
|
||||
|
||||
Add `"flag": submission.flag,` to the return dict in `get()` (after the `conversation_id` line):
|
||||
|
||||
```python
|
||||
"flag": submission.flag,
|
||||
```
|
||||
|
||||
**Step 4: Update SubmissionFilter**
|
||||
|
||||
Add flag filter field to `SubmissionFilter`:
|
||||
|
||||
```python
|
||||
class SubmissionFilter(Schema):
|
||||
task_id: Optional[int] = None
|
||||
task_type: Optional[Literal["tutorial", "challenge"]] = None
|
||||
username: Optional[str] = None
|
||||
flag: Optional[Literal["red", "blue", "green", "yellow"]] = None
|
||||
```
|
||||
|
||||
**Step 5: Add FlagIn schema**
|
||||
|
||||
Add at the end of `schemas.py`:
|
||||
|
||||
```python
|
||||
class FlagIn(Schema):
|
||||
flag: Optional[Literal["red", "blue", "green", "yellow"]] = None
|
||||
```
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add api/submission/schemas.py
|
||||
git commit -m "feat: add flag to submission schemas and filter"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add flag API endpoint and filter logic
|
||||
|
||||
**Files:**
|
||||
- Modify: `api/submission/api.py`
|
||||
|
||||
**Step 1: Add flag filter to list_submissions**
|
||||
|
||||
In `api/submission/api.py`, in the `list_submissions` function, add after the username filter block (after line 65):
|
||||
|
||||
```python
|
||||
if filters.flag:
|
||||
submissions = submissions.filter(flag=filters.flag)
|
||||
```
|
||||
|
||||
**Step 2: Add flag endpoint**
|
||||
|
||||
Import `FlagIn` in the imports from `.schemas`. Add the new endpoint after `update_score`:
|
||||
|
||||
```python
|
||||
@router.put("/{submission_id}/flag")
|
||||
@login_required
|
||||
def update_flag(request, submission_id: UUID, payload: FlagIn):
|
||||
"""
|
||||
设置或清除提交的标记(仅管理员和超级管理员可操作)
|
||||
"""
|
||||
if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN):
|
||||
raise HttpError(403, "没有权限")
|
||||
|
||||
submission = get_object_or_404(Submission, id=submission_id)
|
||||
submission.flag = payload.flag
|
||||
submission.save(update_fields=["flag"])
|
||||
return {"flag": submission.flag}
|
||||
```
|
||||
|
||||
Also add `RoleChoices` to the imports from `account.models`:
|
||||
|
||||
```python
|
||||
from account.models import RoleChoices
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add api/submission/api.py
|
||||
git commit -m "feat: add flag endpoint and filter logic"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update frontend types and API client
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/utils/type.ts`
|
||||
- Modify: `client/src/api.ts`
|
||||
- Modify: `client/src/store/submission.ts`
|
||||
|
||||
**Step 1: Add flag type to TypeScript interfaces**
|
||||
|
||||
In `client/src/utils/type.ts`, add the FlagType and update interfaces:
|
||||
|
||||
```typescript
|
||||
export type FlagType = "red" | "blue" | "green" | "yellow" | null
|
||||
```
|
||||
|
||||
Add `flag?: FlagType` to `SubmissionOut` (after `conversation_id`):
|
||||
|
||||
```typescript
|
||||
export interface SubmissionOut {
|
||||
id: string
|
||||
userid: number
|
||||
username: string
|
||||
task_display: number
|
||||
task_type: TASK_TYPE
|
||||
task_title: string
|
||||
score: number
|
||||
my_score: number
|
||||
conversation_id?: string
|
||||
flag?: FlagType
|
||||
created: Date
|
||||
modified: Date
|
||||
}
|
||||
```
|
||||
|
||||
Add `flag?: FlagType` to `SubmissionAll` (after `my_score`):
|
||||
|
||||
```typescript
|
||||
export interface SubmissionAll {
|
||||
id: string
|
||||
userid: number
|
||||
username: string
|
||||
task_id: number
|
||||
task_display: number
|
||||
task_type: TASK_TYPE
|
||||
task_title: string
|
||||
score: number
|
||||
my_score: number
|
||||
flag?: FlagType
|
||||
html: ""
|
||||
css: ""
|
||||
js: ""
|
||||
created: Date
|
||||
modified: Date
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add updateFlag to API client**
|
||||
|
||||
In `client/src/api.ts`, add to the `Submission` object:
|
||||
|
||||
```typescript
|
||||
async updateFlag(id: string, flag: FlagType) {
|
||||
const res = await http.put(`/submission/${id}/flag`, { flag })
|
||||
return res.data
|
||||
},
|
||||
```
|
||||
|
||||
Also add the import at the top:
|
||||
|
||||
```typescript
|
||||
import type { TutorialIn, ChallengeIn, FlagType } from "./utils/type"
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add client/src/utils/type.ts client/src/api.ts client/src/store/submission.ts
|
||||
git commit -m "feat: add flag type and API method to frontend"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add flag column and interaction to Submissions.vue
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/pages/Submissions.vue`
|
||||
|
||||
**Step 1: Add imports and flag config**
|
||||
|
||||
In the `<script setup>` section, add imports:
|
||||
|
||||
```typescript
|
||||
import { roleAdmin, roleSuper } from "../store/user"
|
||||
import type { FlagType } from "../utils/type"
|
||||
```
|
||||
|
||||
Add flag color config after the existing refs:
|
||||
|
||||
```typescript
|
||||
const FLAG_OPTIONS: { value: FlagType; color: string; label: string }[] = [
|
||||
{ value: "red", color: "#e03030", label: "值得展示" },
|
||||
{ value: "blue", color: "#2080f0", label: "需要讲解" },
|
||||
{ value: "green", color: "#18a058", label: "优秀作品" },
|
||||
{ value: "yellow", color: "#f0a020", label: "需要改进" },
|
||||
]
|
||||
|
||||
const isAdmin = computed(() => roleAdmin.value || roleSuper.value)
|
||||
```
|
||||
|
||||
**Step 2: Add flag update function**
|
||||
|
||||
```typescript
|
||||
async function updateFlag(row: SubmissionOut, flag: FlagType) {
|
||||
await Submission.updateFlag(row.id, flag)
|
||||
row.flag = flag
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Add flag column to columns array**
|
||||
|
||||
Add as the first column in the `columns` array (before the "时间" column):
|
||||
|
||||
```typescript
|
||||
{
|
||||
title: "标记",
|
||||
key: "flag",
|
||||
width: 50,
|
||||
render: (row) => {
|
||||
const flagOption = FLAG_OPTIONS.find((f) => f.value === row.flag)
|
||||
const flagIcon = h("span", {
|
||||
style: {
|
||||
display: "inline-block",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: flagOption ? flagOption.color : "transparent",
|
||||
border: flagOption ? "none" : "1px dashed #ccc",
|
||||
cursor: isAdmin.value ? "pointer" : "default",
|
||||
},
|
||||
})
|
||||
|
||||
if (!isAdmin.value) return flagIcon
|
||||
|
||||
return h(
|
||||
NPopover,
|
||||
{ trigger: "click" },
|
||||
{
|
||||
trigger: () => flagIcon,
|
||||
default: () =>
|
||||
h(NSpace, { vertical: true, size: "small" }, () => [
|
||||
...FLAG_OPTIONS.map((opt) =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => updateFlag(row, opt.value),
|
||||
},
|
||||
() =>
|
||||
h("span", { style: { display: "flex", alignItems: "center", gap: "6px" } }, [
|
||||
h("span", {
|
||||
style: {
|
||||
display: "inline-block",
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: opt.color,
|
||||
},
|
||||
}),
|
||||
opt.label,
|
||||
]),
|
||||
),
|
||||
),
|
||||
row.flag
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: "error",
|
||||
onClick: () => updateFlag(row, null),
|
||||
},
|
||||
() => "清除",
|
||||
)
|
||||
: null,
|
||||
]),
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
**Step 4: Add NPopover and NSpace to imports**
|
||||
|
||||
Update the naive-ui import:
|
||||
|
||||
```typescript
|
||||
import { NButton, NPopover, NSpace, type DataTableColumn } from "naive-ui"
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add client/src/pages/Submissions.vue
|
||||
git commit -m "feat: add flag column with color picker to submissions table"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Add flag filter dropdown
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/pages/Submissions.vue`
|
||||
|
||||
**Step 1: Add flag to query reactive object**
|
||||
|
||||
Update the `query` reactive:
|
||||
|
||||
```typescript
|
||||
const query = reactive({
|
||||
page: Number(route.params.page),
|
||||
username: route.query.username ?? "",
|
||||
flag: "" as string,
|
||||
})
|
||||
```
|
||||
|
||||
**Step 2: Add flag filter dropdown in template**
|
||||
|
||||
In the template, inside the `<n-flex align="center">` block (before the username input), add:
|
||||
|
||||
```html
|
||||
<n-select
|
||||
v-model:value="query.flag"
|
||||
style="width: 100px"
|
||||
clearable
|
||||
placeholder="标记"
|
||||
:options="[
|
||||
{ label: '红旗', value: 'red' },
|
||||
{ label: '蓝旗', value: 'blue' },
|
||||
{ label: '绿旗', value: 'green' },
|
||||
{ label: '黄旗', value: 'yellow' },
|
||||
]"
|
||||
/>
|
||||
```
|
||||
|
||||
**Step 3: Add watcher for flag filter**
|
||||
|
||||
Add a watch for the flag filter (alongside the existing username watcher):
|
||||
|
||||
```typescript
|
||||
watch(
|
||||
() => query.flag,
|
||||
() => {
|
||||
query.page = 1
|
||||
init()
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add client/src/pages/Submissions.vue
|
||||
git commit -m "feat: add flag filter dropdown to submissions list"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Manual verification
|
||||
|
||||
**Step 1: Start dev servers**
|
||||
|
||||
```bash
|
||||
cd api && uv run manage.py runserver &
|
||||
cd client && npm run start &
|
||||
```
|
||||
|
||||
**Step 2: Verify the following**
|
||||
|
||||
1. Submissions list shows the flag column
|
||||
2. As admin/super: clicking the dot opens a color picker popover
|
||||
3. Selecting a color sets the flag (colored dot appears)
|
||||
4. Clicking "清除" removes the flag
|
||||
5. As normal user: flag dot is visible but not clickable
|
||||
6. Flag filter dropdown filters the list correctly
|
||||
7. Page refresh preserves flag state (persisted in DB)
|
||||
|
||||
**Step 3: Final commit if any fixes needed**
|
||||
Reference in new issue
Block a user