@@ -294,7 +294,7 @@ export function getProblemSetList(
|
||||
difficulty = "",
|
||||
status = "",
|
||||
) {
|
||||
return http.get("admin/problemset/", {
|
||||
return http.get("admin/problemset", {
|
||||
params: {
|
||||
paging: true,
|
||||
offset,
|
||||
@@ -307,7 +307,7 @@ export function getProblemSetList(
|
||||
}
|
||||
|
||||
export function getProblemSetDetail(id: number) {
|
||||
return http.get(`admin/problemset/${id}/`)
|
||||
return http.get(`admin/problemset/${id}`)
|
||||
}
|
||||
|
||||
export function createProblemSet(data: {
|
||||
@@ -316,7 +316,7 @@ export function createProblemSet(data: {
|
||||
difficulty: string
|
||||
status: string
|
||||
}) {
|
||||
return http.post("admin/problemset/", data)
|
||||
return http.post("admin/problemset", data)
|
||||
}
|
||||
|
||||
export function editProblemSet(data: {
|
||||
@@ -327,85 +327,107 @@ export function editProblemSet(data: {
|
||||
status?: string
|
||||
visible?: boolean
|
||||
}) {
|
||||
return http.put("admin/problemset/", data)
|
||||
return http.put("admin/problemset", data)
|
||||
}
|
||||
|
||||
export function deleteProblemSet(id: number) {
|
||||
return http.delete("admin/problemset/", { params: { id } })
|
||||
return http.delete("admin/problemset", { params: { id } })
|
||||
}
|
||||
|
||||
export function toggleProblemSetVisible(id: number) {
|
||||
return http.put("admin/problemset/visible/", { id })
|
||||
return http.put("admin/problemset/visible", { id })
|
||||
}
|
||||
|
||||
export function updateProblemSetStatus(id: number, status: string) {
|
||||
return http.put("admin/problemset/status/", { id, status })
|
||||
return http.put("admin/problemset/status", { id, status })
|
||||
}
|
||||
|
||||
// 题单题目管理 API
|
||||
export function getProblemSetProblems(problemSetId: number) {
|
||||
return http.get(`admin/problemset/${problemSetId}/problems/`)
|
||||
return http.get(`admin/problemset/${problemSetId}/problems`)
|
||||
}
|
||||
|
||||
export function addProblemToSet(problemSetId: number, data: {
|
||||
export function addProblemToSet(
|
||||
problemSetId: number,
|
||||
data: {
|
||||
problem_id: string
|
||||
order?: number
|
||||
is_required?: boolean
|
||||
score?: number
|
||||
hint?: string
|
||||
}) {
|
||||
return http.post(`admin/problemset/${problemSetId}/problems/`, data)
|
||||
},
|
||||
) {
|
||||
return http.post(`admin/problemset/${problemSetId}/problems`, data)
|
||||
}
|
||||
|
||||
export function editProblemInSet(problemSetId: number, problemSetProblemId: number, data: {
|
||||
export function editProblemInSet(
|
||||
problemSetId: number,
|
||||
problemSetProblemId: number,
|
||||
data: {
|
||||
order?: number
|
||||
is_required?: boolean
|
||||
score?: number
|
||||
hint?: string
|
||||
}) {
|
||||
return http.put(`admin/problemset/${problemSetId}/problems/${problemSetProblemId}/`, data)
|
||||
},
|
||||
) {
|
||||
return http.put(
|
||||
`admin/problemset/${problemSetId}/problems/${problemSetProblemId}`,
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
export function removeProblemFromSet(problemSetId: number, problemSetProblemId: number) {
|
||||
return http.delete(`admin/problemset/${problemSetId}/problems/${problemSetProblemId}/`)
|
||||
export function removeProblemFromSet(
|
||||
problemSetId: number,
|
||||
problemSetProblemId: number,
|
||||
) {
|
||||
return http.delete(
|
||||
`admin/problemset/${problemSetId}/problems/${problemSetProblemId}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 题单奖章管理 API
|
||||
export function getProblemSetBadges(problemSetId: number) {
|
||||
return http.get(`admin/problemset/${problemSetId}/badges/`)
|
||||
return http.get(`admin/problemset/${problemSetId}/badges`)
|
||||
}
|
||||
|
||||
export function createProblemSetBadge(problemSetId: number, data: {
|
||||
export function createProblemSetBadge(
|
||||
problemSetId: number,
|
||||
data: {
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
condition_type: string
|
||||
condition_value: number
|
||||
level?: number
|
||||
}) {
|
||||
return http.post(`admin/problemset/${problemSetId}/badges/`, data)
|
||||
},
|
||||
) {
|
||||
return http.post(`admin/problemset/${problemSetId}/badges`, data)
|
||||
}
|
||||
|
||||
export function editProblemSetBadge(problemSetId: number, badgeId: number, data: {
|
||||
export function editProblemSetBadge(
|
||||
problemSetId: number,
|
||||
badgeId: number,
|
||||
data: {
|
||||
name?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
condition_type?: string
|
||||
condition_value?: number
|
||||
level?: number
|
||||
}) {
|
||||
return http.put(`admin/problemset/${problemSetId}/badges/${badgeId}/`, data)
|
||||
},
|
||||
) {
|
||||
return http.put(`admin/problemset/${problemSetId}/badges/${badgeId}`, data)
|
||||
}
|
||||
|
||||
export function deleteProblemSetBadge(problemSetId: number, badgeId: number) {
|
||||
return http.delete(`admin/problemset/${problemSetId}/badges/${badgeId}/`)
|
||||
return http.delete(`admin/problemset/${problemSetId}/badges/${badgeId}`)
|
||||
}
|
||||
|
||||
// 题单进度管理 API
|
||||
export function getProblemSetProgress(problemSetId: number) {
|
||||
return http.get(`admin/problemset/${problemSetId}/progress/`)
|
||||
return http.get(`admin/problemset/${problemSetId}/progress`)
|
||||
}
|
||||
|
||||
export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
||||
return http.delete(`admin/problemset/${problemSetId}/progress/${userId}/`)
|
||||
return http.delete(`admin/problemset/${problemSetId}/progress/${userId}`)
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ const options = computed<ChartOptions<"bar" | "line">>(() => {
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx: TooltipItem<"bar">) => {
|
||||
label: (ctx: TooltipItem<"bar" | "line">) => {
|
||||
const dsLabel = ctx.dataset.label || ""
|
||||
if ((ctx.dataset as any).yAxisID === "y1") {
|
||||
const idx = Number(ctx.parsed.y)
|
||||
@@ -170,7 +170,7 @@ const options = computed<ChartOptions<"bar" | "line">>(() => {
|
||||
}
|
||||
return `${dsLabel}: ${ctx.formattedValue}`
|
||||
},
|
||||
footer: (items: TooltipItem<"bar">[]) => {
|
||||
footer: (items: TooltipItem<"bar" | "line">[]) => {
|
||||
const barItems = items.filter(
|
||||
(item) => (item.dataset as any).yAxisID === "y",
|
||||
)
|
||||
|
||||
@@ -125,12 +125,12 @@ const data = computed<ChartData<"line">>(() => {
|
||||
})
|
||||
|
||||
// 图表配置
|
||||
const options = computed<ChartOptions<"line">>(() => {
|
||||
const options = computed(() => {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: "index",
|
||||
mode: "index" as const,
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
@@ -142,8 +142,8 @@ const options = computed<ChartOptions<"line">>(() => {
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
position: "left",
|
||||
type: "linear" as const,
|
||||
position: "left" as const,
|
||||
title: {
|
||||
display: true,
|
||||
text: "平均提交次数(次/题)",
|
||||
@@ -159,8 +159,8 @@ const options = computed<ChartOptions<"line">>(() => {
|
||||
},
|
||||
},
|
||||
y1: {
|
||||
type: "linear",
|
||||
position: "right",
|
||||
type: "linear" as const,
|
||||
position: "right" as const,
|
||||
min: 0,
|
||||
max: 100,
|
||||
title: {
|
||||
|
||||
@@ -303,3 +303,71 @@ export function getCurrentProblemFlowchartSubmission(problemId: number) {
|
||||
params: { problem_id: problemId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 题单相关API ====================
|
||||
|
||||
export function getProblemSetList(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
keyword = "",
|
||||
difficulty = "",
|
||||
status = "",
|
||||
) {
|
||||
return http.get("problemset", {
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
difficulty,
|
||||
status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getProblemSetDetail(id: number) {
|
||||
return http.get(`problemset/${id}`)
|
||||
}
|
||||
|
||||
export function getProblemSetProblems(problemSetId: number) {
|
||||
return http.get(`problemset/${problemSetId}/problems`)
|
||||
}
|
||||
|
||||
export function joinProblemSet(problemSetId: number) {
|
||||
return http.post("problemset/progress", {
|
||||
problemset_id: problemSetId,
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
data: {
|
||||
problem_id: number
|
||||
status: string
|
||||
score?: number
|
||||
submit_time?: string
|
||||
}
|
||||
) {
|
||||
return http.put("problemset/progress", {
|
||||
problemset_id: problemSetId,
|
||||
...data,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { getComment, submitCode } from "oj/api"
|
||||
import { getComment, submitCode, updateProblemSetProgress } from "oj/api"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useFireworks } from "oj/problem/composables/useFireworks"
|
||||
@@ -24,6 +24,7 @@ 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 [commentPanel] = useToggle()
|
||||
|
||||
const { isDesktop } = useBreakpoints()
|
||||
@@ -112,16 +113,31 @@ async function submit() {
|
||||
// ==================== AC庆祝效果 ====================
|
||||
watch(
|
||||
() => submission.value?.result,
|
||||
(result) => {
|
||||
async (result) => {
|
||||
if (result !== SubmissionStatus.accepted) return
|
||||
|
||||
// 1. 刷新题目状态
|
||||
problem.value!.my_status = 0
|
||||
|
||||
// 2. 放烟花(随机效果)
|
||||
// 2. 更新题单进度(如果来自题单)
|
||||
if (problemsetID.value) {
|
||||
try {
|
||||
await updateProblemSetProgress(Number(problemsetID.value), {
|
||||
problem_id: problem.value!.id,
|
||||
status: "completed",
|
||||
score: 100, // 通过得满分
|
||||
submit_time: new Date().toISOString(),
|
||||
})
|
||||
console.log(`[ProblemSet] 题单进度已更新: problemset=${problemsetID.value}, problem=${problem.value!.id}`)
|
||||
} catch (error) {
|
||||
console.error("更新题单进度失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 放烟花(随机效果)
|
||||
celebrate()
|
||||
|
||||
// 3. 显示评价框(非比赛模式)
|
||||
// 4. 显示评价框(非比赛模式)
|
||||
if (!contestID) {
|
||||
showCommentPanelDelayed()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "shared/composables/websocket"
|
||||
|
||||
// API 和状态管理
|
||||
import { getCurrentProblemFlowchartSubmission, submitFlowchart } from "oj/api"
|
||||
import { getCurrentProblemFlowchartSubmission, submitFlowchart, updateProblemSetProgress } from "oj/api"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
@@ -43,6 +43,10 @@ const { problem } = toRefs(problemStore)
|
||||
const { isDesktop } = useBreakpoints()
|
||||
const { convertToMermaid } = useMermaidConverter()
|
||||
const { renderError, renderFlowchart } = useMermaid()
|
||||
const route = useRoute()
|
||||
|
||||
// 获取题单ID
|
||||
const problemsetID = computed(() => route.params.problemSetId as string || "")
|
||||
|
||||
// 状态管理
|
||||
const rendering = ref(false)
|
||||
@@ -62,7 +66,7 @@ const evaluation = ref<Evaluation>({
|
||||
|
||||
// ==================== WebSocket 相关函数 ====================
|
||||
// 处理 WebSocket 消息
|
||||
const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
|
||||
const handleWebSocketMessage = async (data: FlowchartEvaluationUpdate) => {
|
||||
console.log("收到流程图评分更新:", data)
|
||||
|
||||
if (data.type === "flowchart_evaluation_completed") {
|
||||
@@ -72,6 +76,21 @@ const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
|
||||
grade: data.grade || "",
|
||||
}
|
||||
message.success(`流程图评分完成!得分: ${data.score}分 (${data.grade}级)`)
|
||||
|
||||
// 更新题单进度(如果来自题单)
|
||||
if (problemsetID.value) {
|
||||
try {
|
||||
await updateProblemSetProgress(Number(problemsetID.value), {
|
||||
problem_id: problem.value!.id,
|
||||
status: "completed",
|
||||
score: data.score || 0,
|
||||
submit_time: new Date().toISOString(),
|
||||
})
|
||||
console.log(`[ProblemSet] 题单进度已更新: problemset=${problemsetID.value}, problem=${problem.value!.id}, score=${data.score}`)
|
||||
} catch (error) {
|
||||
console.error("更新题单进度失败:", error)
|
||||
}
|
||||
}
|
||||
} else if (data.type === "flowchart_evaluation_failed") {
|
||||
console.log("处理评分失败消息")
|
||||
loading.value = false
|
||||
|
||||
@@ -33,16 +33,21 @@ const ProblemFlowchart = defineAsyncComponent(
|
||||
interface Props {
|
||||
problemID: string
|
||||
contestID?: string
|
||||
problemSetId?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contestID: "",
|
||||
problemSetId: "",
|
||||
})
|
||||
|
||||
const errMsg = ref("无数据")
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 从路由参数中获取题单ID
|
||||
const problemsetID = computed(() => props.problemSetId || route.params.problemSetId as string)
|
||||
|
||||
const problemStore = useProblemStore()
|
||||
const screenModeStore = useScreenModeStore()
|
||||
const { problem } = storeToRefs(problemStore)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { NSpace, NTag } from "naive-ui"
|
||||
import { NFlex, NTag } from "naive-ui"
|
||||
import { useRouteQuery } from "@vueuse/router"
|
||||
import { getProblemList, getRandomProblemID } from "oj/api"
|
||||
import { getTagColor } from "utils/functions"
|
||||
@@ -180,7 +180,7 @@ const baseColumns: DataTableColumn<ProblemFiltered>[] = [
|
||||
key: "tags",
|
||||
width: 260,
|
||||
render: (row) =>
|
||||
h(NSpace, () => row.tags.map((t) => h(NTag, { key: t }, () => t))),
|
||||
h(NFlex, () => row.tags.map((t) => h(NTag, { key: t }, () => t))),
|
||||
},
|
||||
{
|
||||
title: renderTableTitle("出题者", "streamline-emojis:man-raising-hand-2"),
|
||||
|
||||
189
src/oj/problemset/components/ProblemSetStatistics.vue
Normal file
189
src/oj/problemset/components/ProblemSetStatistics.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<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>
|
||||
252
src/oj/problemset/components/ProblemSetSubmissions.vue
Normal file
252
src/oj/problemset/components/ProblemSetSubmissions.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<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>
|
||||
274
src/oj/problemset/detail.vue
Normal file
274
src/oj/problemset/detail.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import {
|
||||
getProblemSetDetail,
|
||||
getProblemSetProblems,
|
||||
joinProblemSet,
|
||||
} from "../api"
|
||||
import { parseTime, 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"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const { isDesktop } = useBreakpoints()
|
||||
|
||||
const problemSetId = computed(() => Number(route.params.problemSetId))
|
||||
|
||||
const problemSet = ref<ProblemSet | null>(null)
|
||||
const problems = ref<ProblemSetProblem[]>([])
|
||||
const isJoined = ref(false)
|
||||
const isJoining = ref(false)
|
||||
|
||||
// 刷新题单详情的函数
|
||||
async function refreshProblemSetDetail() {
|
||||
try {
|
||||
const res = await getProblemSetDetail(problemSetId.value)
|
||||
problemSet.value = res.data
|
||||
// 更新加入状态
|
||||
isJoined.value = res.data.user_progress?.is_joined || false
|
||||
console.log(`[ProblemSet] 题单详情已刷新: ${problemSet.value?.title}`)
|
||||
} catch (err: any) {
|
||||
console.error("刷新题单详情失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 标签页状态
|
||||
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,
|
||||
{ type: "success" | "warning" | "error" | "default"; text: string }
|
||||
> = {
|
||||
Easy: { type: "success", text: "简单" },
|
||||
Medium: { type: "warning", text: "中等" },
|
||||
Hard: { type: "error", text: "困难" },
|
||||
}
|
||||
return difficultyMap[difficulty] || { type: "default", text: "未知" }
|
||||
}
|
||||
|
||||
function goToProblem(problemId: string) {
|
||||
// 使用新的路由结构:/problemset/{id}/problem/{problem_id}
|
||||
router.push(`/problemset/${problemSetId.value}/problem/${problemId}`)
|
||||
}
|
||||
|
||||
function getProblemStatus(
|
||||
myStatus: number | null | undefined,
|
||||
): "not_test" | "passed" | "failed" {
|
||||
if (myStatus === null || myStatus === undefined) {
|
||||
return "not_test"
|
||||
} else if (myStatus === 0) {
|
||||
return "passed"
|
||||
} else {
|
||||
return "failed"
|
||||
}
|
||||
}
|
||||
|
||||
function getProgressPercentage() {
|
||||
if (!problemSet.value) return 0
|
||||
return Math.round(
|
||||
(problemSet.value.completed_count / problemSet.value.problems_count) * 100,
|
||||
)
|
||||
}
|
||||
|
||||
async function loadProblemSetDetail() {
|
||||
try {
|
||||
const res = await getProblemSetDetail(problemSetId.value)
|
||||
problemSet.value = res.data
|
||||
// 更新加入状态
|
||||
isJoined.value = res.data.user_progress?.is_joined || false
|
||||
} catch (err: any) {
|
||||
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProblems() {
|
||||
try {
|
||||
const res = await getProblemSetProblems(problemSetId.value)
|
||||
problems.value = res.data
|
||||
} catch (err: any) {
|
||||
message.error("加载题目列表失败:" + (err.data || "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleJoinProblemSet() {
|
||||
if (isJoining.value) return
|
||||
|
||||
isJoining.value = true
|
||||
try {
|
||||
await joinProblemSet(problemSetId.value)
|
||||
isJoined.value = true
|
||||
message.success("成功加入题单!")
|
||||
// 不需要重新加载详情,因为我们已经更新了本地状态
|
||||
// await loadProblemSetDetail()
|
||||
} catch (err: any) {
|
||||
message.error("加入题单失败:" + (err.data || "未知错误"))
|
||||
} finally {
|
||||
isJoining.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadProblemSetDetail(), loadProblems()])
|
||||
})
|
||||
|
||||
// 监听路由变化,当从题单题目页面返回时刷新题单详情
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath, oldPath) => {
|
||||
// 如果从题单题目页面返回到题单详情页面,刷新题单详情
|
||||
if (oldPath?.includes('/problem/') && newPath === `/problemset/${problemSetId.value}` && isJoined.value) {
|
||||
refreshProblemSetDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="problemSet">
|
||||
<!-- 题单信息头部 -->
|
||||
<n-card style="margin-bottom: 24px">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-flex align="center">
|
||||
<n-tag type="warning" v-if="problemSet.status === 'archived'">
|
||||
已归档
|
||||
</n-tag>
|
||||
<n-tag :type="getDifficultyTag(problemSet.difficulty).type">
|
||||
{{ getDifficultyTag(problemSet.difficulty).text }}
|
||||
</n-tag>
|
||||
<n-h2 style="margin: 0">{{ problemSet.title }}</n-h2>
|
||||
<n-tooltip trigger="hover" v-if="problemSet.description">
|
||||
<template #trigger>
|
||||
<Icon width="20" icon="emojione:information" />
|
||||
</template>
|
||||
{{ problemSet.description }}
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
|
||||
<n-flex align="center">
|
||||
<n-flex align="center">
|
||||
<n-text strong>完成进度</n-text>
|
||||
<n-text>
|
||||
{{ problemSet.completed_count }} / {{ problemSet.problems_count }}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
<n-progress
|
||||
:percentage="getProgressPercentage()"
|
||||
:height="8"
|
||||
:border-radius="4"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<n-button
|
||||
v-if="!isJoined"
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isJoining"
|
||||
@click="handleJoinProblemSet"
|
||||
>
|
||||
加入题单
|
||||
</n-button>
|
||||
<n-tag v-else type="success" size="large">
|
||||
<template #icon>
|
||||
<Icon icon="material-symbols:check-circle" />
|
||||
</template>
|
||||
已加入
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</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-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-flex>
|
||||
|
||||
<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>
|
||||
</n-tab-pane>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { useRouteQuery } from "@vueuse/router"
|
||||
import { getProblemSetList } from "../api"
|
||||
import { parseTime } from "utils/functions"
|
||||
import { ProblemSetList } from "utils/types"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { usePagination } from "shared/composables/pagination"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
|
||||
const router = useRouter()
|
||||
const { isDesktop } = useBreakpoints()
|
||||
|
||||
const total = ref(0)
|
||||
const problemSets = ref<ProblemSetList[]>([])
|
||||
|
||||
interface ProblemSetQuery {
|
||||
keyword: string
|
||||
difficulty: string
|
||||
status: string
|
||||
}
|
||||
|
||||
// 使用分页 composable
|
||||
const { query, clearQuery } = usePagination<ProblemSetQuery>({
|
||||
keyword: useRouteQuery("keyword", "").value,
|
||||
difficulty: useRouteQuery("difficulty", "").value,
|
||||
status: useRouteQuery("status", "").value,
|
||||
})
|
||||
|
||||
const difficultyOptions = [
|
||||
{ label: "全部", value: "" },
|
||||
{ label: "简单", value: "Easy" },
|
||||
{ label: "中等", value: "Medium" },
|
||||
{ label: "困难", value: "Hard" },
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "全部", value: "" },
|
||||
{ label: "活跃", value: "active" },
|
||||
{ label: "已归档", value: "archived" },
|
||||
]
|
||||
|
||||
async function listProblemSets() {
|
||||
if (query.page < 1) query.page = 1
|
||||
const offset = (query.page - 1) * query.limit
|
||||
const res = await getProblemSetList(
|
||||
offset,
|
||||
query.limit,
|
||||
query.keyword,
|
||||
query.difficulty,
|
||||
query.status,
|
||||
)
|
||||
total.value = res.data.total
|
||||
problemSets.value = res.data.results
|
||||
}
|
||||
|
||||
function getDifficultyTag(difficulty: string) {
|
||||
const difficultyMap: Record<
|
||||
string,
|
||||
{ type: "success" | "warning" | "error" | "default"; text: string }
|
||||
> = {
|
||||
Easy: { type: "success", text: "简单" },
|
||||
Medium: { type: "warning", text: "中等" },
|
||||
Hard: { type: "error", text: "困难" },
|
||||
}
|
||||
return difficultyMap[difficulty] || { type: "default", text: "未知" }
|
||||
}
|
||||
|
||||
function goToProblemSet(problemSetId: number) {
|
||||
router.push(`/problemset/${problemSetId}`)
|
||||
}
|
||||
|
||||
function getConditionText(
|
||||
conditionType: string,
|
||||
conditionValue: number,
|
||||
): string {
|
||||
const conditionMap: Record<string, string> = {
|
||||
all_problems: "完成所有题目",
|
||||
problem_count: `完成 ${conditionValue} 道题目`,
|
||||
score: `达到 ${conditionValue} 分`,
|
||||
}
|
||||
return conditionMap[conditionType] || "未知条件"
|
||||
}
|
||||
|
||||
function getProgressColor(percentage: number) {
|
||||
if (percentage >= 80) return "#18a058" // 绿色
|
||||
if (percentage >= 50) return "#f0a020" // 橙色
|
||||
return "#d03050" // 红色
|
||||
}
|
||||
|
||||
onMounted(listProblemSets)
|
||||
|
||||
// 监听搜索关键词变化(防抖)
|
||||
watchDebounced(() => query.keyword, listProblemSets, {
|
||||
debounce: 500,
|
||||
maxWait: 1000,
|
||||
})
|
||||
|
||||
// 监听其他查询条件变化
|
||||
watch(
|
||||
() => [query.page, query.limit, query.difficulty, query.status],
|
||||
listProblemSets,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-flex vertical size="large">
|
||||
<n-space>
|
||||
<n-space align="center">
|
||||
<n-text>难度:</n-text>
|
||||
<n-select
|
||||
v-model:value="query.difficulty"
|
||||
:options="difficultyOptions"
|
||||
placeholder="选择难度"
|
||||
style="width: 120px"
|
||||
clearable
|
||||
/>
|
||||
</n-space>
|
||||
<n-space align="center">
|
||||
<n-text>状态:</n-text>
|
||||
<n-select
|
||||
v-model:value="query.status"
|
||||
:options="statusOptions"
|
||||
placeholder="选择状态"
|
||||
style="width: 120px"
|
||||
clearable
|
||||
/>
|
||||
</n-space>
|
||||
<n-input
|
||||
v-model:value="query.keyword"
|
||||
placeholder="搜索题单..."
|
||||
clearable
|
||||
@clear="clearQuery"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</n-space>
|
||||
|
||||
<n-grid :cols="isDesktop ? 4 : 1" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item v-for="problemSet in problemSets" :key="problemSet.id">
|
||||
<n-card
|
||||
hoverable
|
||||
@click="goToProblemSet(problemSet.id)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<template #header>
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-text strong>{{ problemSet.title }}</n-text>
|
||||
<n-tag :type="getDifficultyTag(problemSet.difficulty).type">
|
||||
{{ getDifficultyTag(problemSet.difficulty).text }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-flex vertical size="large">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-flex>
|
||||
<Icon width="20" icon="streamline-emojis:blossom" />
|
||||
<n-text>{{ problemSet.problems_count }} 道题目</n-text>
|
||||
</n-flex>
|
||||
|
||||
<n-flex align="center" size="small">
|
||||
<n-tag type="warning" v-if="problemSet.status === 'archived'">
|
||||
已归档
|
||||
</n-tag>
|
||||
<n-tag v-if="problemSet.user_progress?.is_joined" type="success" size="small">
|
||||
<template #icon>
|
||||
<Icon icon="material-symbols:check-circle" width="12" />
|
||||
</template>
|
||||
已加入
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 用户进度显示 -->
|
||||
<div v-if="problemSet.user_progress?.is_joined">
|
||||
<n-flex align="center" justify="space-between" style="margin-bottom: 8px">
|
||||
<n-text depth="3" style="font-size: 12px">
|
||||
我的进度: {{ problemSet.user_progress.completed_count }} / {{ problemSet.user_progress.total_count }}
|
||||
</n-text>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="Math.round(problemSet.user_progress.progress_percentage)"
|
||||
:height="4"
|
||||
:border-radius="2"
|
||||
style="width: 100px"
|
||||
:color="getProgressColor(problemSet.user_progress.progress_percentage)"
|
||||
/>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<!-- 奖章显示 -->
|
||||
<div v-if="problemSet.badges && problemSet.badges.length > 0">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-text depth="3">
|
||||
创建于
|
||||
{{ parseTime(problemSet.create_time, "YYYY-MM-DD") }}
|
||||
</n-text>
|
||||
<n-flex>
|
||||
<n-tooltip
|
||||
v-for="badge in problemSet.badges"
|
||||
:key="badge.id"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-image
|
||||
:src="badge.icon"
|
||||
:alt="badge.name"
|
||||
width="24"
|
||||
height="24"
|
||||
object-fit="cover"
|
||||
/>
|
||||
</template>
|
||||
<n-flex vertical size="small">
|
||||
<span style="font-weight: bold"
|
||||
>徽章: {{ badge.name }}</span
|
||||
>
|
||||
<span>
|
||||
获取条件:
|
||||
{{
|
||||
getConditionText(
|
||||
badge.condition_type,
|
||||
badge.condition_value,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</n-flex>
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:limit="query.limit"
|
||||
v-model:page="query.page"
|
||||
/>
|
||||
</n-flex>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -100,6 +100,24 @@ export const ojs: RouteRecordRaw = {
|
||||
path: "flowchart",
|
||||
component: () => import("oj/flowchart/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "problemset",
|
||||
component: () => import("oj/problemset/list.vue"),
|
||||
name: "problemsets",
|
||||
},
|
||||
{
|
||||
path: "problemset/:problemSetId",
|
||||
component: () => import("oj/problemset/detail.vue"),
|
||||
props: true,
|
||||
name: "problemset",
|
||||
},
|
||||
{
|
||||
path: "problemset/:problemSetId/problem/:problemID",
|
||||
component: () => import("oj/problem/detail.vue"),
|
||||
props: true,
|
||||
name: "problemset problem",
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,11 @@ const menus = computed<MenuOption[]>(() => [
|
||||
key: "problem",
|
||||
icon: renderIcon("streamline-emojis:blossom"),
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: "/problemset" }, { default: () => "题单" }),
|
||||
key: "problemset",
|
||||
icon: renderIcon("streamline-emojis:green-book"),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(RouterLink, { to: "/submission" }, { default: () => "提交" }),
|
||||
|
||||
@@ -194,6 +194,13 @@ export interface ProblemSet {
|
||||
visible: boolean
|
||||
problems_count: number
|
||||
completed_count: number
|
||||
user_progress: {
|
||||
is_joined: boolean
|
||||
progress_percentage: number
|
||||
completed_count: number
|
||||
total_count: number
|
||||
is_completed: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProblemSetList {
|
||||
@@ -213,6 +220,7 @@ export interface ProblemSetList {
|
||||
total_count: number
|
||||
is_completed: boolean
|
||||
}
|
||||
badges: ProblemSetBadge[]
|
||||
}
|
||||
|
||||
export interface ProblemSetProblem {
|
||||
@@ -594,3 +602,42 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user