update
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-10-23 10:54:55 +08:00
parent ee59039968
commit 9e7fbd8ff2
6 changed files with 71 additions and 591 deletions

View File

@@ -338,25 +338,6 @@ export function joinProblemSet(problemSetId: number) {
})
}
export function getProblemSetSubmissions(
problemSetId: number,
params: {
problem_id?: string
result?: string
language?: string
offset?: number
limit?: number
} = {},
) {
return http.get(`problemset/${problemSetId}/submissions`, {
params,
})
}
export function getProblemSetStatistics(problemSetId: number) {
return http.get(`problemset/${problemSetId}/statistics`)
}
export function updateProblemSetProgress(
problemSetId: number,
problemId: number,

View File

@@ -24,7 +24,8 @@ const problemStore = useProblemStore()
const { problem } = storeToRefs(problemStore)
const route = useRoute()
const contestID = <string>route.params.contestID ?? ""
const problemSetID = computed(() => route.params.problemSetID as string || "")
const problemSetId = <string>route.params.problemSetId ?? ""
console.log(problemSetId, "problemSetId")
const [commentPanel] = useToggle()
const { isDesktop } = useBreakpoints()
@@ -100,8 +101,8 @@ async function submit() {
if (contestID) {
data.contest_id = parseInt(contestID)
}
if (problemSetID.value) {
data.problemset_id = parseInt(problemSetID.value)
if (problemSetId) {
data.problemset_id = parseInt(problemSetId)
}
// 2. 提交代码到后端
@@ -123,19 +124,20 @@ watch(
problem.value!.my_status = 0
// 2. 更新题单进度
if (problemSetID.value) {
if (problemSetId) {
console.log(problemSetId, "problemSetId")
try {
await updateProblemSetProgress(Number(problemSetID.value), problem.value!.id)
await updateProblemSetProgress(Number(problemSetId), problem.value!.id)
} catch (error) {
console.error("更新题单进度失败:", error)
}
}
// 3. 放烟花(随机效果)
// 3. 放烟花
celebrate()
// 4. 显示评价框(非比赛模式)
if (!contestID) {
// 4. 显示评价框
if (!contestID && !problemSetId) {
showCommentPanelDelayed()
}
},

View File

@@ -1,189 +0,0 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { getProblemSetStatistics } from "oj/api"
import { ProblemSetStatistics } from "utils/types"
import { useBreakpoints } from "shared/composables/breakpoints"
import { NTag } from "naive-ui"
import { h } from "vue"
interface Props {
problemSetId: number
}
const props = defineProps<Props>()
const { isDesktop } = useBreakpoints()
const message = useMessage()
const statistics = ref<ProblemSetStatistics | null>(null)
const loading = ref(false)
// 结果状态映射
const resultStatusMap: Record<string, { text: string; type: string }> = {
"-2": { text: "编译错误", type: "error" },
"-1": { text: "答案错误", type: "error" },
"0": { text: "通过", type: "success" },
"1": { text: "时间超限", type: "warning" },
"2": { text: "时间超限", type: "warning" },
"3": { text: "内存超限", type: "warning" },
"4": { text: "运行时错误", type: "error" },
"5": { text: "系统错误", type: "error" },
"6": { text: "等待中", type: "info" },
"7": { text: "评测中", type: "info" },
"8": { text: "部分通过", type: "warning" },
}
async function loadStatistics() {
loading.value = true
try {
const res = await getProblemSetStatistics(props.problemSetId)
statistics.value = res.data
} catch (err: any) {
message.error("加载统计信息失败:" + (err.data || "未知错误"))
} finally {
loading.value = false
}
}
onMounted(() => {
loadStatistics()
})
</script>
<template>
<n-flex vertical size="large" v-if="statistics">
<!-- 总体统计 -->
<n-card title="总体统计">
<n-grid :cols="isDesktop ? 4 : 2" :x-gap="16" :y-gap="16">
<n-grid-item>
<n-statistic label="总提交数" :value="statistics.total_submissions">
<template #prefix>
<Icon icon="streamline-emojis:file" />
</template>
</n-statistic>
</n-grid-item>
<n-grid-item>
<n-statistic label="通过数" :value="statistics.accepted_submissions">
<template #prefix>
<Icon icon="streamline-emojis:check-mark" />
</template>
</n-statistic>
</n-grid-item>
<n-grid-item>
<n-statistic label="通过率" :value="`${statistics.acceptance_rate}%`">
<template #prefix>
<Icon icon="streamline-emojis:chart-increasing" />
</template>
</n-statistic>
</n-grid-item>
<n-grid-item>
<n-statistic label="完成进度" :value="`${statistics.progress.progress_percentage.toFixed(1)}%`">
<template #prefix>
<Icon icon="streamline-emojis:target" />
</template>
</n-statistic>
</n-grid-item>
</n-grid>
</n-card>
<!-- 进度信息 -->
<n-card title="题单进度">
<n-flex vertical size="medium">
<n-flex justify="space-between" align="center">
<n-text>完成题目数</n-text>
<n-text strong>
{{ statistics.progress.completed_problems_count }} / {{ statistics.progress.total_problems_count }}
</n-text>
</n-flex>
<n-progress
:percentage="statistics.progress.progress_percentage"
:height="8"
:border-radius="4"
/>
<n-flex justify="space-between" align="center">
<n-text>总得分</n-text>
<n-text strong>{{ statistics.progress.total_score }} </n-text>
</n-flex>
</n-flex>
</n-card>
<!-- 题目统计 -->
<n-card title="题目统计">
<n-data-table
:columns="[
{
title: '题目',
key: 'problem_title',
minWidth: 200,
},
{
title: '总提交',
key: 'total_submissions',
width: 100,
},
{
title: '通过',
key: 'accepted_submissions',
width: 100,
},
{
title: '状态',
key: 'is_completed',
width: 100,
render: (row) => {
return h(
NTag,
{
type: row.is_completed ? 'success' : 'default',
},
() => row.is_completed ? '已完成' : '未完成'
)
},
},
]"
:data="Object.values(statistics.problem_stats)"
:pagination="false"
/>
</n-card>
<!-- 语言统计 -->
<n-card title="语言统计" v-if="Object.keys(statistics.language_stats).length > 0">
<n-flex vertical size="small">
<n-flex
v-for="(count, language) in statistics.language_stats"
:key="language"
justify="space-between"
align="center"
>
<n-text>{{ language }}</n-text>
<n-text strong>{{ count }} </n-text>
</n-flex>
</n-flex>
</n-card>
<!-- 结果统计 -->
<n-card title="结果统计" v-if="Object.keys(statistics.result_stats).length > 0">
<n-flex vertical size="small">
<n-flex
v-for="(count, result) in statistics.result_stats"
:key="result"
justify="space-between"
align="center"
>
<n-tag
:type="(resultStatusMap[String(result)]?.type as any) || 'default'"
size="small"
>
{{ resultStatusMap[String(result)]?.text || '未知' }}
</n-tag>
<n-text strong>{{ count }} </n-text>
</n-flex>
</n-flex>
</n-card>
</n-flex>
<n-empty v-else-if="!loading" description="暂无统计信息" />
<n-spin v-else size="large" />
</template>
<style scoped></style>

View File

@@ -1,252 +0,0 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { getProblemSetSubmissions } from "oj/api"
import { parseTime } from "utils/functions"
import { ProblemSetSubmission } from "utils/types"
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import { renderTableTitle } from "utils/renders"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import Pagination from "shared/components/Pagination.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { usePagination } from "shared/composables/pagination"
import SubmissionDetail from "oj/submission/detail.vue"
import { NButton } from "naive-ui"
interface Props {
problemSetId: number
}
const props = defineProps<Props>()
const { isDesktop } = useBreakpoints()
const message = useMessage()
const router = useRouter()
// 弹框状态管理
const [codePanelVisible, toggleCodePanel] = useToggle(false)
const submissionID = ref("")
const problemID = ref("")
// 显示代码弹框
function showCodePanel(id: string, problem: string) {
submissionID.value = id
problemID.value = problem
toggleCodePanel(true)
}
// 使用分页 composable
const { query, clearQuery } = usePagination({
problem_id: "",
result: "",
language: "",
})
const submissions = ref<ProblemSetSubmission[]>([])
const total = ref(0)
const loading = ref(false)
const columns = computed(() => {
const baseColumns = [
{
title: renderTableTitle("提交时间", "noto:seven-oclock"),
key: "submit_time",
minWidth: 200,
render: (row: ProblemSetSubmission) => parseTime(row.submit_time, "YYYY-MM-DD HH:mm:ss"),
},
{
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
key: "id",
minWidth: 200,
render: (row: ProblemSetSubmission) => {
return h(
NButton,
{
text: true,
type: "info",
onClick: () => {
showCodePanel(row.submission, row.problem.toString())
},
},
() => row.submission.slice(0, 12),
)
},
},
{
title: renderTableTitle("状态", "streamline-emojis:panda-face"),
key: "result",
minWidth: 140,
render: (row: ProblemSetSubmission) => h(SubmissionResultTag, { result: row.result as any }),
},
{
title: renderTableTitle("题目", "streamline-emojis:blossom"),
key: "problem_title",
minWidth: 300,
render: (row: ProblemSetSubmission) => {
return h(
NButton,
{
text: true,
type: "primary",
onClick: () => {
router.push(`/problem/${row.problem_id}`)
},
},
() => `${row.problem_id} ${row.problem_title}`,
)
},
},
{
title: renderTableTitle("语言", "streamline-emojis:globe-showing-europe-africa"),
key: "language",
minWidth: 120,
render: (row: ProblemSetSubmission) => LANGUAGE_SHOW_VALUE[row.language as keyof typeof LANGUAGE_SHOW_VALUE] || row.language,
},
]
if (isDesktop.value) {
baseColumns.push(
{
title: renderTableTitle("代码长度", "streamline-emojis:file"),
key: "code_length",
minWidth: 100,
render: (row: ProblemSetSubmission) => `${row.code_length} 字符`,
},
{
title: renderTableTitle("执行时间", "streamline-emojis:stopwatch"),
key: "execution_time",
minWidth: 100,
render: (row: ProblemSetSubmission) => `${row.execution_time}ms`,
},
{
title: renderTableTitle("内存使用", "streamline-emojis:brain"),
key: "memory_usage",
minWidth: 100,
render: (row: ProblemSetSubmission) => `${row.memory_usage}KB`,
}
)
}
return baseColumns
})
async function loadSubmissions() {
loading.value = true
try {
const res = await getProblemSetSubmissions(props.problemSetId, {
...query,
offset: query.limit * (query.page - 1),
})
submissions.value = res.data.results
total.value = res.data.total
} catch (err: any) {
message.error("加载提交记录失败:" + (err.data || "未知错误"))
} finally {
loading.value = false
}
}
function search() {
query.page = 1
loadSubmissions()
}
function clear() {
clearQuery()
search()
}
onMounted(() => {
loadSubmissions()
})
watch(query, loadSubmissions)
</script>
<template>
<n-flex vertical size="large">
<!-- 搜索表单 -->
<n-form :show-feedback="false" inline label-placement="left">
<n-form-item label="题目ID">
<n-input
v-model:value="query.problem_id"
placeholder="输入题目ID"
clearable
@keyup.enter="search"
/>
</n-form-item>
<n-form-item label="结果">
<n-select
v-model:value="query.result"
placeholder="选择结果"
clearable
:options="[
{ label: '通过', value: '0' },
{ label: '答案错误', value: '-1' },
{ label: '编译错误', value: '-2' },
{ label: '时间超限', value: '1' },
{ label: '内存超限', value: '3' },
{ label: '运行时错误', value: '4' },
{ label: '系统错误', value: '5' },
{ label: '等待中', value: '6' },
{ label: '评测中', value: '7' },
{ label: '部分通过', value: '8' },
]"
/>
</n-form-item>
<n-form-item label="语言">
<n-select
v-model:value="query.language"
placeholder="选择语言"
clearable
:options="[
{ label: 'C', value: 'C' },
{ label: 'C++', value: 'C++' },
{ label: 'Java', value: 'Java' },
{ label: 'Python', value: 'Python' },
{ label: 'JavaScript', value: 'JavaScript' },
]"
/>
</n-form-item>
<n-form-item>
<n-button @click="search" type="primary">搜索</n-button>
</n-form-item>
<n-form-item>
<n-button @click="clear" quaternary>重置</n-button>
</n-form-item>
</n-form>
<!-- 提交记录表格 -->
<n-data-table
v-if="submissions.length > 0"
:bordered="false"
:columns="columns"
:data="submissions"
:loading="loading"
/>
<n-empty v-else-if="!loading" description="暂无提交记录" />
<!-- 分页 -->
<Pagination
:total="total"
v-model:limit="query.limit"
v-model:page="query.page"
/>
<!-- 代码详情弹框 -->
<n-modal
v-model:show="codePanelVisible"
preset="card"
:style="{ maxWidth: isDesktop && '70vw', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
title="代码详情"
>
<SubmissionDetail
:problemID="problemID"
:submissionID="submissionID"
hideList
/>
</n-modal>
</n-flex>
</template>
<style scoped></style>

View File

@@ -5,11 +5,9 @@ import {
getProblemSetProblems,
joinProblemSet,
} from "../api"
import { parseTime, getTagColor, getACRate } from "utils/functions"
import { getTagColor, getACRate } from "utils/functions"
import { ProblemSet, ProblemSetProblem } from "utils/types"
import ProblemStatus from "../problem/components/ProblemStatus.vue"
import ProblemSetSubmissions from "./components/ProblemSetSubmissions.vue"
import ProblemSetStatistics from "./components/ProblemSetStatistics.vue"
import { DIFFICULTY } from "utils/constants"
import { useBreakpoints } from "shared/composables/breakpoints"
@@ -39,23 +37,6 @@ async function refreshProblemSetDetail() {
}
}
// 标签页状态
const activeTab = ref("problems")
const tabOptions = computed(() => {
const options = [
{ label: "题目列表", value: "problems" }
]
if (isJoined.value) {
options.push(
{ label: "提交记录", value: "submissions" },
{ label: "统计信息", value: "statistics" }
)
}
return options
})
function getDifficultyTag(difficulty: string) {
const difficultyMap: Record<
string,
@@ -138,10 +119,14 @@ watch(
() => route.path,
(newPath, oldPath) => {
// 如果从题单题目页面返回到题单详情页面,刷新题单详情
if (oldPath?.includes('/problem/') && newPath === `/problemset/${problemSetId.value}` && isJoined.value) {
if (
oldPath?.includes("/problem/") &&
newPath === `/problemset/${problemSetId.value}` &&
isJoined.value
) {
refreshProblemSetDetail()
}
}
},
)
</script>
@@ -198,76 +183,66 @@ watch(
</n-flex>
</n-card>
<!-- 标签页 -->
<n-tabs v-model:value="activeTab" type="segment">
<n-tab-pane name="problems" tab="题目列表">
<n-grid :cols="isDesktop ? 4 : 1" :x-gap="16" :y-gap="16">
<n-grid-item
v-for="(problemSetProblem, index) in problems"
:key="problemSetProblem.id"
>
<n-card
hoverable
@click="goToProblem(problemSetProblem.problem._id)"
style="cursor: pointer"
>
<n-flex>
<!-- 题目列表 -->
<n-grid :cols="isDesktop ? 4 : 1" :x-gap="16" :y-gap="16">
<n-grid-item
v-for="(problemSetProblem, index) in problems"
:key="problemSetProblem.id"
>
<n-card
hoverable
@click="goToProblem(problemSetProblem.problem._id)"
style="cursor: pointer"
>
<n-flex>
<n-flex align="center">
<n-h2 style="margin: 0 20px 0 0">#{{ index + 1 }}. </n-h2>
<n-flex vertical style="flex: 1">
<n-flex align="center">
<n-h2 style="margin: 0 20px 0 0">#{{ index + 1 }}. </n-h2>
<n-flex vertical style="flex: 1">
<n-flex align="center">
<n-tag
:type="getTagColor(problemSetProblem.problem.difficulty)"
size="small"
>
{{ DIFFICULTY[problemSetProblem.problem.difficulty] }}
</n-tag>
<n-h4 style="margin: 0">
{{ problemSetProblem.problem.title }}
</n-h4>
</n-flex>
<n-flex align="center" size="small">
<n-text type="info">
分数{{ problemSetProblem.score }}
</n-text>
<n-text v-if="!problemSetProblem.is_required">选做</n-text>
<n-text depth="3" style="font-size: 12px">
通过率
{{
getACRate(
problemSetProblem.problem.accepted_number,
problemSetProblem.problem.submission_number,
)
}}
</n-text>
</n-flex>
</n-flex>
<n-tag
:type="getTagColor(problemSetProblem.problem.difficulty)"
size="small"
>
{{ DIFFICULTY[problemSetProblem.problem.difficulty] }}
</n-tag>
<n-h4 style="margin: 0">
{{ problemSetProblem.problem.title }}
</n-h4>
</n-flex>
<n-flex align="center">
<ProblemStatus
:status="getProblemStatus(problemSetProblem.problem.my_status)"
/>
<n-icon>
<Icon icon="streamline-emojis:right-arrow" />
</n-icon>
<n-flex align="center" size="small">
<n-text type="info">
分数{{ problemSetProblem.score }}
</n-text>
<n-text v-if="!problemSetProblem.is_required"
>选做</n-text
>
<n-text depth="3" style="font-size: 12px">
通过率
{{
getACRate(
problemSetProblem.problem.accepted_number,
problemSetProblem.problem.submission_number,
)
}}
</n-text>
</n-flex>
</n-flex>
</n-card>
</n-grid-item>
</n-grid>
</n-tab-pane>
</n-flex>
<n-tab-pane v-if="isJoined" name="submissions" tab="提交记录">
<ProblemSetSubmissions :problemSetId="problemSetId" />
</n-tab-pane>
<n-tab-pane v-if="isJoined" name="statistics" tab="统计信息">
<ProblemSetStatistics :problemSetId="problemSetId" />
</n-tab-pane>
</n-tabs>
<n-flex align="center">
<ProblemStatus
:status="getProblemStatus(problemSetProblem.problem.my_status)"
/>
<n-icon>
<Icon icon="streamline-emojis:right-arrow" />
</n-icon>
</n-flex>
</n-flex>
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>

View File

@@ -604,41 +604,4 @@ export interface DetailsData {
export type Grade = "S" | "A" | "B" | "C"
// 题单提交记录相关类型
export interface ProblemSetSubmission {
id: number
problem: number
problem_id: number
problem_title: string
submission: string
result: number
result_text: string
score: number
language: string
code_length: number
execution_time: number
memory_usage: number
submit_time: string
}
export interface ProblemSetStatistics {
total_submissions: number
accepted_submissions: number
acceptance_rate: number
problem_stats: {
[problemId: string]: {
problem_title: string
total_submissions: number
accepted_submissions: number
is_completed: boolean
}
}
language_stats: { [language: string]: number }
result_stats: { [result: number]: number }
progress: {
completed_problems_count: number
total_problems_count: number
progress_percentage: number
total_score: number
}
}