update
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled

This commit is contained in:
2026-03-18 18:40:15 +08:00
parent dd52e3e1f9
commit 4e95a2fad0
11 changed files with 41 additions and 422 deletions

View File

@@ -68,11 +68,6 @@ export const Account = {
return res.data return res.data
}, },
async leaderboard() {
const res = await http.get("/account/leaderboard")
return res.data as { rank: number; username: string; total_score: number }[]
},
async listClasses(): Promise<string[]> { async listClasses(): Promise<string[]> {
const res = await http.get("/account/classes") const res = await http.get("/account/classes")
return res.data return res.data
@@ -226,17 +221,6 @@ export const Submission = {
return res.data as { nominated: boolean } return res.data as { nominated: boolean }
}, },
async myScores() {
const res = await http.get("/submission/my-scores")
return res.data as {
task_id: number
task_display: number
task_title: string
score: number
created: string
}[]
},
} }
export const Prompt = { export const Prompt = {

View File

@@ -19,14 +19,6 @@
</template> </template>
<template #header-extra> <template #header-extra>
<n-tag type="warning" size="small">{{ item.score }}</n-tag> <n-tag type="warning" size="small">{{ item.score }}</n-tag>
<n-tag
v-if="myScoreMap.get(item.display)"
type="success"
size="small"
style="margin-left: 4px"
>
得分 {{ myScoreMap.get(item.display)!.toFixed(1) }}
</n-tag>
</template> </template>
</n-card> </n-card>
</n-gi> </n-gi>
@@ -39,7 +31,7 @@ import { ref, onMounted } from "vue"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { marked } from "marked" import { marked } from "marked"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import { Challenge, Submission } from "../api" import { Challenge } from "../api"
import { taskTab, taskId, challengeDisplay } from "../store/task" import { taskTab, taskId, challengeDisplay } from "../store/task"
import { TASK_TYPE } from "../utils/const" import { TASK_TYPE } from "../utils/const"
import type { ChallengeSlim } from "../utils/type" import type { ChallengeSlim } from "../utils/type"
@@ -48,16 +40,6 @@ const router = useRouter()
const challenges = ref<ChallengeSlim[]>([]) const challenges = ref<ChallengeSlim[]>([])
const currentChallenge = ref<ChallengeSlim | null>(null) const currentChallenge = ref<ChallengeSlim | null>(null)
const content = ref("") const content = ref("")
const myScoreMap = ref<Map<number, number>>(new Map())
async function loadMyScores() {
try {
const scores = await Submission.myScores()
myScoreMap.value = new Map(scores.map((s) => [s.task_display, s.score]))
} catch {
// 未登录时忽略
}
}
async function loadList() { async function loadList() {
challenges.value = await Challenge.listDisplay() challenges.value = await Challenge.listDisplay()
@@ -90,10 +72,7 @@ function back() {
router.push({ name: "home-challenge-list" }) router.push({ name: "home-challenge-list" })
} }
onMounted(async () => { onMounted(loadList)
await loadList()
await loadMyScores()
})
</script> </script>
<style scoped> <style scoped>
.container { .container {

View File

@@ -45,12 +45,6 @@
> >
<Icon :width="16" icon="lucide:list"></Icon> <Icon :width="16" icon="lucide:list"></Icon>
</n-button> </n-button>
<!-- <n-button text @click="$router.push({ name: 'leaderboard' })">
<Icon :width="16" icon="lucide:trophy" />
</n-button>
<n-button text v-if="isLoggedIn" @click="$router.push({ name: 'my-scores' })">
<Icon :width="16" icon="lucide:bar-chart-2" />
</n-button> -->
<n-button text v-if="roleSuper" @click="edit"> <n-button text v-if="roleSuper" @click="edit">
<Icon :width="16" icon="lucide:edit"></Icon> <Icon :width="16" icon="lucide:edit"></Icon>
</n-button> </n-button>
@@ -70,7 +64,7 @@ import { step } from "../store/tutorial"
import { authed, roleSuper } from "../store/user" import { authed, roleSuper } from "../store/user"
import { taskTab, challengeDisplay } from "../store/task" import { taskTab, challengeDisplay } from "../store/task"
import { useRoute, useRouter } from "vue-router" import { useRoute, useRouter } from "vue-router"
import { TASK_TYPE, STORAGE_KEY } from "../utils/const" import { TASK_TYPE } from "../utils/const"
import Challenge from "./Challenge.vue" import Challenge from "./Challenge.vue"
import Tutorial from "./Tutorial.vue" import Tutorial from "./Tutorial.vue"
@@ -80,8 +74,6 @@ const tutorialRef = ref<InstanceType<typeof Tutorial>>()
defineEmits(["hide"]) defineEmits(["hide"])
const isLoggedIn = computed(() => localStorage.getItem(STORAGE_KEY.LOGIN) === "true")
const hideNav = computed( const hideNav = computed(
() => () =>
taskTab.value !== TASK_TYPE.Tutorial || taskTab.value !== TASK_TYPE.Tutorial ||

View File

@@ -31,6 +31,7 @@ const emit = defineEmits<{
select: [id: string] select: [id: string]
delete: [row: SubmissionOut, parentId: string] delete: [row: SubmissionOut, parentId: string]
"show-chain": [conversationId: string] "show-chain": [conversationId: string]
nominate: [row: SubmissionOut]
}>() }>()
const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge) const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge)
@@ -66,6 +67,25 @@ const subColumns = computed((): DataTableColumn<SubmissionOut>[] => [
]) ])
}, },
}, },
{
title: "排名",
key: "nominated",
width: 60,
render: (r: SubmissionOut) => {
if (r.username !== user.username) {
return r.nominated ? h("span", { style: { color: "#f0a020" } }, "🏅") : null
}
return h(
NButton,
{
text: true,
title: r.nominated ? "已参与排名点击可重新提名" : "参与排名",
onClick: (e: Event) => { e.stopPropagation(); emit("nominate", r) },
},
() => (r.nominated ? "🏅" : ""),
)
},
},
...(isChallenge.value ...(isChallenge.value
? [{ ? [{
title: "提示词", title: "提示词",

View File

@@ -1,46 +0,0 @@
<template>
<div class="container">
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
<n-button secondary @click="$router.back()">返回</n-button>
<span style="font-weight: bold; font-size: 18px">排行榜</span>
<div style="width: 60px" />
</n-flex>
<n-data-table :columns="columns" :data="data" :loading="loading" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from "vue"
import { Account } from "../api"
import type { DataTableColumn } from "naive-ui"
const data = ref<{ rank: number; username: string; total_score: number }[]>([])
const loading = ref(false)
const columns: DataTableColumn<(typeof data.value)[0]>[] = [
{ title: "排名", key: "rank", width: 70 },
{ title: "用户名", key: "username" },
{
title: "总分",
key: "total_score",
render: (row) => h("span", { style: "font-weight: bold" }, row.total_score.toFixed(2)),
},
]
onMounted(async () => {
loading.value = true
try {
data.value = await Account.leaderboard()
} finally {
loading.value = false
}
})
</script>
<style scoped>
.container {
max-width: 600px;
margin: 40px auto;
padding: 0 16px;
}
</style>

View File

@@ -1,64 +0,0 @@
<template>
<div class="container">
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
<n-button secondary @click="$router.back()">返回</n-button>
<span style="font-weight: bold; font-size: 18px">我的成绩</span>
<div style="width: 60px" />
</n-flex>
<n-empty v-if="!loading && !data.length" description="暂无评分记录" />
<n-data-table v-else :columns="columns" :data="data" :loading="loading" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from "vue"
import { Submission } from "../api"
import type { DataTableColumn } from "naive-ui"
import { parseTime } from "../utils/helper"
type MyScore = {
task_id: number
task_display: number
task_title: string
score: number
created: string
}
const data = ref<MyScore[]>([])
const loading = ref(false)
const columns: DataTableColumn<MyScore>[] = [
{ title: "题号", key: "task_display", width: 70 },
{ title: "标题", key: "task_title" },
{
title: "最高得分",
key: "score",
render: (row) =>
row.score > 0
? h("span", { style: { fontWeight: "bold", color: "#18a058" } }, row.score.toFixed(2))
: h("span", { style: { color: "#999" } }, "未评分"),
},
{
title: "提交时间",
key: "created",
render: (row) => parseTime(row.created, "YYYY-MM-DD HH:mm"),
},
]
onMounted(async () => {
loading.value = true
try {
data.value = await Submission.myScores()
} finally {
loading.value = false
}
})
</script>
<style scoped>
.container {
max-width: 700px;
margin: 40px auto;
padding: 0 16px;
}
</style>

View File

@@ -1,222 +0,0 @@
<template>
<n-split class="container" direction="horizontal" :default-size="0.333" :min="0.2" :max="0.8">
<template #1>
<n-flex vertical style="height: 100%; padding-right: 10px">
<n-flex justify="space-between" align="center">
<n-button secondary @click="$router.back()">返回</n-button>
<span style="font-weight: bold; font-size: 16px">排名榜</span>
<div style="width: 60px" />
</n-flex>
<n-tabs v-model:value="activeZone" type="line" animated @update:value="onZoneChange">
<n-tab v-for="zone in ZONES" :key="zone.key" :name="zone.key">
<n-flex align="center" :style="{ gap: '4px' }">
<span>{{ zone.label }}</span>
<n-badge
v-if="counts[zone.key] !== undefined"
:value="counts[zone.key]"
:max="999"
:show-zero="true"
style="margin-left: 4px"
/>
</n-flex>
</n-tab>
</n-tabs>
<n-empty
v-if="!loading && data.length === 0"
description="该区暂无提交"
style="margin: auto"
/>
<n-data-table
v-else
striped
:columns="columns"
:data="data"
:loading="loading"
:row-props="rowProps"
:row-class-name="rowClassName"
/>
<n-pagination
v-model:page="page"
:page-size="PAGE_SIZE"
:item-count="counts[activeZone] ?? 0"
simple
style="align-self: flex-end"
/>
</n-flex>
</template>
<template #2>
<div style="height: 100%; padding-left: 10px">
<Preview
v-if="selectedSubmission.id"
:html="selectedSubmission.html"
:css="selectedSubmission.css"
:js="selectedSubmission.js"
:submission-id="selectedSubmission.id"
@after-score="afterScore"
/>
</div>
</template>
</n-split>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch, h } from "vue"
import type { DataTableColumn } from "naive-ui"
import { Submission } from "../api"
import type { SubmissionOut } from "../utils/type"
import { parseTime } from "../utils/helper"
import Preview from "../components/Preview.vue"
import { submission as submissionStore } from "../store/submission"
const PAGE_SIZE = 10
interface Zone {
key: string
label: string
params: Record<string, unknown>
}
const ZONES: Zone[] = [
{
key: "top",
label: "🏆 精华",
params: { score_min: 4.5, ordering: "-score" },
},
{
key: "good",
label: "⭐ 优秀",
params: { score_min: 3.5, score_max_exclusive: 4.5, ordering: "-score" },
},
{
key: "normal",
label: "📝 普通",
params: { score_min: 0.001, score_max_exclusive: 3.5, ordering: "-score" },
},
{
key: "unrated",
label: "⏳ 待评",
params: { score_lt_threshold: 0.001, ordering: "-created" },
},
]
const activeZone = ref("top")
const page = ref(1)
const data = ref<SubmissionOut[]>([])
const loading = ref(false)
const counts = reactive<Record<string, number>>({})
const selectedSubmission = submissionStore
const columns: DataTableColumn<SubmissionOut>[] = [
{
title: "#",
key: "rank",
width: 45,
render: (_, index) => (page.value - 1) * PAGE_SIZE + index + 1,
},
{
title: "得分",
key: "score",
width: 65,
render: (row) =>
row.score > 0
? h("span", { style: { fontWeight: "bold" } }, row.score.toFixed(2))
: h("span", { style: { color: "#999" } }, "—"),
},
{ title: "提交者", key: "username", width: 80, render: (row) => row.username },
{ title: "任务", key: "task_title", render: (row) => row.task_title },
{
title: "时间",
key: "created",
width: 110,
render: (row) => parseTime(row.created, "YYYY-MM-DD HH:mm:ss"),
},
]
function rowProps(row: SubmissionOut) {
return {
style: { cursor: "pointer" },
onClick: () => loadSubmission(row.id),
}
}
function rowClassName(row: SubmissionOut) {
return submissionStore.value.id === row.id ? "row-active" : ""
}
async function loadSubmission(id: string) {
submissionStore.value = await Submission.get(id)
}
function afterScore() {
data.value = data.value.map((d) => {
if (d.id === submissionStore.value.id) {
d.my_score = submissionStore.value.my_score
}
return d
})
}
function currentZone(): Zone {
return ZONES.find((z) => z.key === activeZone.value)!
}
async function fetchPage() {
loading.value = true
try {
const zone = currentZone()
const res = await Submission.list({
page: page.value,
task_type: "challenge",
nominated: true,
...zone.params,
} as Parameters<typeof Submission.list>[0])
data.value = res.items
counts[zone.key] = res.count
} finally {
loading.value = false
}
}
async function fetchAllCounts() {
await Promise.all(
ZONES.map(async (zone) => {
const res = await Submission.list({
page: 1,
task_type: "challenge",
nominated: true,
...zone.params,
} as Parameters<typeof Submission.list>[0])
counts[zone.key] = res.count
}),
)
}
function onZoneChange() {
page.value = 1
fetchPage()
}
watch(page, fetchPage)
onMounted(async () => {
await fetchAllCounts()
await fetchPage()
})
</script>
<style scoped>
.container {
padding: 10px;
box-sizing: border-box;
height: calc(100% - 43px);
width: 100%;
}
:deep(.row-active td) {
background-color: rgba(24, 160, 80, 0.1) !important;
}
</style>

View File

@@ -178,6 +178,7 @@ const columns: DataTableColumn<SubmissionOut>[] = [
onSelect: (id) => getSubmissionByID(id), onSelect: (id) => getSubmissionByID(id),
onDelete: (r, parentId) => handleDelete(r, parentId), onDelete: (r, parentId) => handleDelete(r, parentId),
"onShow-chain": (id) => showChain(id), "onShow-chain": (id) => showChain(id),
onNominate: (r) => handleNominateChild(r, row.id),
}), }),
}, },
{ {
@@ -191,25 +192,6 @@ const columns: DataTableColumn<SubmissionOut>[] = [
"onUpdate:flag": (flag: FlagType) => updateFlag(row, flag), "onUpdate:flag": (flag: FlagType) => updateFlag(row, flag),
}), }),
}, },
{
title: "排名",
key: "nominated",
width: 60,
render: (row) => {
if (row.username !== user.username) {
return row.nominated ? h("span", { style: { color: "#f0a020" } }, "🏅") : null
}
return h(
NButton,
{
text: true,
title: row.nominated ? "已参与排名点击可重新提名" : "参与排名",
onClick: (e: Event) => { e.stopPropagation(); handleNominate(row) },
},
() => (row.nominated ? "🏅" : ""),
)
},
},
{ {
title: "时间", title: "时间",
key: "created", key: "created",
@@ -277,7 +259,17 @@ async function handleDelete(row: SubmissionOut, parentId: string) {
} }
function rowProps(row: SubmissionOut) { function rowProps(row: SubmissionOut) {
return { style: { cursor: "pointer" }, onClick: () => getSubmissionByID(row.id) } return {
style: { cursor: "pointer" },
onClick: () => {
getSubmissionByID(row.id)
handleExpand(
expandedKeys.value.includes(row.id)
? expandedKeys.value.filter((k) => k !== row.id)
: [...expandedKeys.value, row.id],
)
},
}
} }
function rowClassName(row: SubmissionOut) { function rowClassName(row: SubmissionOut) {
@@ -296,8 +288,12 @@ async function getSubmissionByID(id: string) {
submission.value = await Submission.get(id) submission.value = await Submission.get(id)
} }
async function handleNominate(row: SubmissionOut) { async function handleNominateChild(row: SubmissionOut, parentId: string) {
await Submission.nominate(row.id) await Submission.nominate(row.id)
const items = expandedData.get(parentId)
if (items) {
expandedData.set(parentId, items.map((d) => ({ ...d, nominated: d.id === row.id })))
}
data.value = data.value.map((d) => { data.value = data.value.map((d) => {
if (d.username === user.username && d.task_id === row.task_id) { if (d.username === user.username && d.task_id === row.task_id) {
d.nominated = d.id === row.id d.nominated = d.id === row.id

View File

@@ -81,7 +81,6 @@ const message = useMessage()
const confirm = useDialog() const confirm = useDialog()
const list = ref<TutorialSlim[]>([]) const list = ref<TutorialSlim[]>([])
const content = ref("")
const tutorial = reactive({ const tutorial = reactive({
display: 0, display: 0,
title: "", title: "",
@@ -102,7 +101,6 @@ function createNew() {
tutorial.title = "" tutorial.title = ""
tutorial.content = "" tutorial.content = ""
tutorial.is_public = false tutorial.is_public = false
content.value = ""
} }
async function submit() { async function submit() {
@@ -114,7 +112,6 @@ async function submit() {
tutorial.content = "" tutorial.content = ""
tutorial.is_public = false tutorial.is_public = false
await getContent() await getContent()
content.value = ""
} catch (error: any) { } catch (error: any) {
message.error(error.response.data.detail) message.error(error.response.data.detail)
} }

View File

@@ -25,23 +25,6 @@ const routes = [
component: () => import("./pages/Submission.vue"), component: () => import("./pages/Submission.vue"),
props: true, props: true,
}, },
{
path: "/leaderboard",
name: "leaderboard",
component: () => import("./pages/Leaderboard.vue"),
},
{
path: "/ranking",
name: "ranking",
component: () => import("./pages/Ranking.vue"),
meta: { auth: true },
},
{
path: "/my-scores",
name: "my-scores",
component: () => import("./pages/MyScores.vue"),
meta: { auth: true },
},
{ {
path: "/dashboard", path: "/dashboard",
name: "dashboard", name: "dashboard",

View File

@@ -10,5 +10,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] "include": ["src/**/*.ts", "src/**/*.vue"]
} }