update
This commit is contained in:
16
src/api.ts
16
src/api.ts
@@ -68,11 +68,6 @@ export const Account = {
|
||||
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[]> {
|
||||
const res = await http.get("/account/classes")
|
||||
return res.data
|
||||
@@ -226,17 +221,6 @@ export const Submission = {
|
||||
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 = {
|
||||
|
||||
@@ -19,14 +19,6 @@
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<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>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
@@ -39,7 +31,7 @@ import { ref, onMounted } from "vue"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { marked } from "marked"
|
||||
import { useRouter } from "vue-router"
|
||||
import { Challenge, Submission } from "../api"
|
||||
import { Challenge } from "../api"
|
||||
import { taskTab, taskId, challengeDisplay } from "../store/task"
|
||||
import { TASK_TYPE } from "../utils/const"
|
||||
import type { ChallengeSlim } from "../utils/type"
|
||||
@@ -48,16 +40,6 @@ const router = useRouter()
|
||||
const challenges = ref<ChallengeSlim[]>([])
|
||||
const currentChallenge = ref<ChallengeSlim | null>(null)
|
||||
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() {
|
||||
challenges.value = await Challenge.listDisplay()
|
||||
@@ -90,10 +72,7 @@ function back() {
|
||||
router.push({ name: "home-challenge-list" })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadList()
|
||||
await loadMyScores()
|
||||
})
|
||||
onMounted(loadList)
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
|
||||
@@ -45,12 +45,6 @@
|
||||
>
|
||||
<Icon :width="16" icon="lucide:list"></Icon>
|
||||
</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">
|
||||
<Icon :width="16" icon="lucide:edit"></Icon>
|
||||
</n-button>
|
||||
@@ -70,7 +64,7 @@ import { step } from "../store/tutorial"
|
||||
import { authed, roleSuper } from "../store/user"
|
||||
import { taskTab, challengeDisplay } from "../store/task"
|
||||
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 Tutorial from "./Tutorial.vue"
|
||||
|
||||
@@ -80,8 +74,6 @@ const tutorialRef = ref<InstanceType<typeof Tutorial>>()
|
||||
|
||||
defineEmits(["hide"])
|
||||
|
||||
const isLoggedIn = computed(() => localStorage.getItem(STORAGE_KEY.LOGIN) === "true")
|
||||
|
||||
const hideNav = computed(
|
||||
() =>
|
||||
taskTab.value !== TASK_TYPE.Tutorial ||
|
||||
|
||||
@@ -31,6 +31,7 @@ const emit = defineEmits<{
|
||||
select: [id: string]
|
||||
delete: [row: SubmissionOut, parentId: string]
|
||||
"show-chain": [conversationId: string]
|
||||
nominate: [row: SubmissionOut]
|
||||
}>()
|
||||
|
||||
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
|
||||
? [{
|
||||
title: "提示词",
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -178,6 +178,7 @@ const columns: DataTableColumn<SubmissionOut>[] = [
|
||||
onSelect: (id) => getSubmissionByID(id),
|
||||
onDelete: (r, parentId) => handleDelete(r, parentId),
|
||||
"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),
|
||||
}),
|
||||
},
|
||||
{
|
||||
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: "时间",
|
||||
key: "created",
|
||||
@@ -277,7 +259,17 @@ async function handleDelete(row: SubmissionOut, parentId: string) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -296,8 +288,12 @@ async function getSubmissionByID(id: string) {
|
||||
submission.value = await Submission.get(id)
|
||||
}
|
||||
|
||||
async function handleNominate(row: SubmissionOut) {
|
||||
async function handleNominateChild(row: SubmissionOut, parentId: string) {
|
||||
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) => {
|
||||
if (d.username === user.username && d.task_id === row.task_id) {
|
||||
d.nominated = d.id === row.id
|
||||
|
||||
@@ -81,7 +81,6 @@ const message = useMessage()
|
||||
const confirm = useDialog()
|
||||
|
||||
const list = ref<TutorialSlim[]>([])
|
||||
const content = ref("")
|
||||
const tutorial = reactive({
|
||||
display: 0,
|
||||
title: "",
|
||||
@@ -102,7 +101,6 @@ function createNew() {
|
||||
tutorial.title = ""
|
||||
tutorial.content = ""
|
||||
tutorial.is_public = false
|
||||
content.value = ""
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
@@ -114,7 +112,6 @@ async function submit() {
|
||||
tutorial.content = ""
|
||||
tutorial.is_public = false
|
||||
await getContent()
|
||||
content.value = ""
|
||||
} catch (error: any) {
|
||||
message.error(error.response.data.detail)
|
||||
}
|
||||
|
||||
@@ -25,23 +25,6 @@ const routes = [
|
||||
component: () => import("./pages/Submission.vue"),
|
||||
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",
|
||||
name: "dashboard",
|
||||
|
||||
Reference in New Issue
Block a user