Compare commits

...

4 Commits

Author SHA1 Message Date
016e070fb9 fix: use null instead of empty string for flag filter initial value
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled
Axios sends empty string as query param which fails django-ninja
Literal validation. Null is omitted from params entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:15:35 +08:00
0ee8b0d6ea Add flag filter dropdown to submissions page
Allows filtering submissions by flag color using a select dropdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:29:00 +08:00
334b2d77b1 Add flag column with admin popover to submissions table
Admins can click the flag dot to set/clear colored flags on submissions.
Non-admins see the flag indicator as read-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:26:16 +08:00
b4bfc7706c Add FlagType and updateFlag API method to frontend
Add FlagType to type definitions and flag field to SubmissionOut/SubmissionAll
interfaces. Add updateFlag method to Submission API client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:25:37 +08:00
3 changed files with 113 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
import axios from "axios" import axios from "axios"
import { router } from "./router" import { router } from "./router"
import type { TutorialIn, ChallengeIn } from "./utils/type" import type { TutorialIn, ChallengeIn, FlagType } from "./utils/type"
import { BASE_URL, STORAGE_KEY } from "./utils/const" import { BASE_URL, STORAGE_KEY } from "./utils/const"
const http = axios.create({ const http = axios.create({
@@ -167,6 +167,11 @@ export const Submission = {
const res = await http.put(`/submission/${id}/score`, { score }) const res = await http.put(`/submission/${id}/score`, { score })
return res.data return res.data
}, },
async updateFlag(id: string, flag: FlagType) {
const res = await http.put(`/submission/${id}/flag`, { flag })
return res.data
},
} }
export const Prompt = { export const Prompt = {

View File

@@ -7,6 +7,18 @@
返回首页 返回首页
</n-button> </n-button>
<n-flex align="center"> <n-flex align="center">
<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' },
]"
/>
<div> <div>
<n-input <n-input
style="width: 120px" style="width: 120px"
@@ -113,7 +125,7 @@
</n-modal> </n-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NButton, type DataTableColumn } from "naive-ui" import { NButton, NPopover, NSpace, type DataTableColumn } from "naive-ui"
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from "vue" import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from "vue"
import { Submission, Prompt } from "../api" import { Submission, Prompt } from "../api"
import type { SubmissionOut } from "../utils/type" import type { SubmissionOut } from "../utils/type"
@@ -128,6 +140,8 @@ import { step } from "../store/tutorial"
import { html as eHtml, css as eCss, js as eJs } from "../store/editors" import { html as eHtml, css as eCss, js as eJs } from "../store/editors"
import { TASK_TYPE } from "../utils/const" import { TASK_TYPE } from "../utils/const"
import { goHome } from "../utils/helper" import { goHome } from "../utils/helper"
import { roleAdmin, roleSuper } from "../store/user"
import type { FlagType } from "../utils/type"
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -137,6 +151,7 @@ const count = ref(0)
const query = reactive({ const query = reactive({
page: Number(route.params.page), page: Number(route.params.page),
username: route.query.username ?? "", username: route.query.username ?? "",
flag: null as string | null,
}) })
const html = computed(() => submission.value.html) const html = computed(() => submission.value.html)
@@ -149,6 +164,15 @@ const chainMessages = ref<{ id: number; role: string; content: string; code_html
const chainLoading = ref(false) const chainLoading = ref(false)
const selectedRound = ref(0) const selectedRound = ref(0)
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)
const chainRounds = computed(() => { const chainRounds = computed(() => {
const messages = chainMessages.value const messages = chainMessages.value
const rounds: { question: string; html: string | null; css: string | null; js: string | null }[] = [] const rounds: { question: string; html: string | null; css: string | null; js: string | null }[] = []
@@ -177,6 +201,11 @@ const selectedPageHtml = computed(() => {
return `<!DOCTYPE html><html><head><meta charset="utf-8">${style}</head><body>${round.html}${script}</body></html>` return `<!DOCTYPE html><html><head><meta charset="utf-8">${style}</head><body>${round.html}${script}</body></html>`
}) })
async function updateFlag(row: SubmissionOut, flag: FlagType) {
await Submission.updateFlag(row.id, flag)
row.flag = flag
}
async function showChain(conversationId: string) { async function showChain(conversationId: string) {
chainLoading.value = true chainLoading.value = true
chainModal.value = true chainModal.value = true
@@ -191,6 +220,72 @@ async function showChain(conversationId: string) {
} }
const columns: DataTableColumn<SubmissionOut>[] = [ const columns: DataTableColumn<SubmissionOut>[] = [
{
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,
block: true,
type: "error",
onClick: () => updateFlag(row, null),
},
() => "清除",
)
: null,
]),
},
)
},
},
{ {
title: "时间", title: "时间",
key: "created", key: "created",
@@ -291,6 +386,13 @@ watchDebounced(
}, },
{ debounce: 500, maxWait: 1000 }, { debounce: 500, maxWait: 1000 },
) )
watch(
() => query.flag,
() => {
query.page = 1
init()
},
)
onMounted(init) onMounted(init)
onUnmounted(() => { onUnmounted(() => {
submission.value = { submission.value = {

View File

@@ -14,6 +14,8 @@ export function getRole(role: Role) {
}[role] }[role]
} }
export type FlagType = "red" | "blue" | "green" | "yellow" | null
export interface TutorialSlim { export interface TutorialSlim {
display: number display: number
title: string title: string
@@ -64,6 +66,7 @@ export interface SubmissionOut {
score: number score: number
my_score: number my_score: number
conversation_id?: string conversation_id?: string
flag?: FlagType
created: Date created: Date
modified: Date modified: Date
} }
@@ -78,6 +81,7 @@ export interface SubmissionAll {
task_title: string task_title: string
score: number score: number
my_score: number my_score: number
flag?: FlagType
html: "" html: ""
css: "" css: ""
js: "" js: ""