update
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-06-05 09:03:38 -06:00
parent 324e85d2c0
commit f9d7c2ff92
18 changed files with 335 additions and 164 deletions

View File

@@ -1,7 +1,6 @@
import { DIFFICULTY } from "utils/constants"
import { getACRate } from "utils/functions"
import http from "utils/http"
import {
import { filterResult } from "oj/transforms"
import type {
Exercise,
Problem,
Submission,
@@ -9,31 +8,6 @@ import {
SubmitCodePayload,
} from "utils/types"
function filterResult(result: Problem) {
const newResult = {
id: result.id,
_id: result._id,
title: result.title,
difficulty: DIFFICULTY[result.difficulty],
tags: result.tags,
submission: result.submission_number,
rate: getACRate(result.accepted_number, result.submission_number),
status: "",
author: result.created_by.username,
allow_flowchart: result.allow_flowchart,
show_flowchart: result.show_flowchart,
has_ast_rules: result.has_ast_rules,
}
if (result.my_status === null || result.my_status === undefined) {
newResult.status = "not_test"
} else if (result.my_status === 0) {
newResult.status = "passed"
} else {
newResult.status = "failed"
}
return newResult
}
export function getWebsiteConfig() {
return http.get("website")
}
@@ -43,17 +17,9 @@ export async function getProblemList(
limit = 10,
searchParams: any = {},
) {
let params: any = {
paging: true,
offset,
limit,
}
Object.keys(searchParams).forEach((element) => {
if (searchParams[element]) {
params[element] = searchParams[element]
}
const res = await http.get<{ results: Problem[]; total: number }>("problem", {
params: { paging: true, offset, limit, ...searchParams },
})
const res = await http.get("problem", { params })
return {
results: res.data.results.map(filterResult),
total: res.data.total,
@@ -203,7 +169,7 @@ export function checkContestPassword(contestID: string, password: string) {
}
export async function getContestProblems(contestID: string) {
const res = await http.get("contest/problem", {
const res = await http.get<Problem[]>("contest/problem", {
params: { contest_id: contestID },
})
return res.data.map(filterResult)
@@ -460,7 +426,7 @@ export function getProblemSetUserProgress(
}
export async function getExercises(tutorialId: number): Promise<Exercise[]> {
const res = await http.get("exercises", {
const res = await http.get<Exercise[]>("exercises", {
params: { tutorial_id: tutorialId },
})
return res.data

View File

@@ -164,7 +164,8 @@ async function analyzeWithAI() {
aiController = controller
const timeRangeLabel =
timeRangeOptions.find((o) => o.value === duration.value)?.label ?? "全部时间"
timeRangeOptions.find((o) => o.value === duration.value)?.label ??
"全部时间"
showAIModal.value = true
aiContent.value = ""
@@ -195,7 +196,11 @@ async function analyzeWithAI() {
if (event === "end" && !hasStarted) aiLoading.value = false
},
onMessage(payload) {
const parsed = payload as { type?: string; content?: string; message?: string }
const parsed = payload as {
type?: string
content?: string
message?: string
}
if (parsed.type === "delta" && parsed.content) {
if (!hasStarted) {
hasStarted = true
@@ -1176,7 +1181,6 @@ const radarChartOptions = {
</n-gi>
</n-grid>
</n-card>
</template>
<!-- 对比表格 -->
@@ -1185,10 +1189,7 @@ const radarChartOptions = {
title="对比表格"
style="margin-top: 20px"
>
<n-data-table
:data="comparisons"
:columns="tableColumns"
/>
<n-data-table :data="comparisons" :columns="tableColumns" />
</n-card>
</n-flex>
</n-card>

View File

@@ -85,16 +85,14 @@ function inputWidth(idx: number): string {
</script>
<template>
<n-card
style="margin: 16px 0; border: 1.5px solid var(--n-border-color)"
>
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
<template #header>
<n-tag type="warning" :bordered="false"
>练一练 · 代码填空</n-tag
>
<n-tag type="warning" :bordered="false">练一练 · 代码填空</n-tag>
</template>
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">{{ data.question }}</p>
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">
{{ data.question }}
</p>
<pre
:style="{
@@ -147,11 +145,7 @@ function inputWidth(idx: number): string {
/>
<n-space style="margin-top: 12px" :size="8">
<n-button
type="warning"
:disabled="allCorrect"
@click="submit"
>
<n-button type="warning" :disabled="allCorrect" @click="submit">
提交
</n-button>
<n-button @click="reset">重置</n-button>

View File

@@ -63,9 +63,7 @@ function optionType(idx: number): "default" | "primary" | "success" {
</script>
<template>
<n-card
style="margin: 16px 0; border: 1.5px solid var(--n-border-color)"
>
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
<template #header>
<n-space align="center" :size="8">
<n-tag type="success" :bordered="false">
@@ -74,7 +72,9 @@ function optionType(idx: number): "default" | "primary" | "success" {
</n-space>
</template>
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">{{ data.question }}</p>
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">
{{ data.question }}
</p>
<n-space vertical :size="8">
<n-button

View File

@@ -101,16 +101,14 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
</script>
<template>
<n-card
style="margin: 16px 0; border: 1.5px solid var(--n-border-color)"
>
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
<template #header>
<n-tag type="info" :bordered="false"
>练一练 · 代码排序</n-tag
>
<n-tag type="info" :bordered="false">练一练 · 代码排序</n-tag>
</template>
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">{{ data.question }}</p>
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">
{{ data.question }}
</p>
<n-space vertical :size="6">
<div
@@ -157,11 +155,7 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
/>
<n-space style="margin-top: 12px" :size="8">
<n-button
type="info"
:disabled="submitted && allCorrect"
@click="submit"
>
<n-button type="info" :disabled="submitted && allCorrect" @click="submit">
提交
</n-button>
<n-button @click="reset">重置</n-button>

View File

@@ -1,7 +1,12 @@
<template>
<div class="learn-container">
<!-- 桌面端布局 -->
<n-grid :cols="5" :x-gap="16" v-if="tutorial.id && isDesktop" class="learn-grid">
<n-grid
:cols="5"
:x-gap="16"
v-if="tutorial.id && isDesktop"
class="learn-grid"
>
<n-gi :span="1" class="learn-col">
<n-card title="教程目录" :bordered="false" size="small">
<n-list hoverable clickable>
@@ -51,7 +56,11 @@
class="code-card"
content-style="height: calc(100% - 44px); padding: 0;"
>
<CodeEditor language="Python3" v-model="tutorial.code" height="100%" />
<CodeEditor
language="Python3"
v-model="tutorial.code"
height="100%"
/>
</n-card>
</n-gi>
</n-grid>

View File

@@ -514,42 +514,71 @@ watch(
<n-modal
v-model:show="showClassDetailModal"
preset="card"
:title="classDetailData ? `${classDetailData.class_name.slice(0, 2)}计算机${classDetailData.class_name.slice(2)}班` : '班级详情'"
:title="
classDetailData
? `${classDetailData.class_name.slice(0, 2)}计算机${classDetailData.class_name.slice(2)}班`
: '班级详情'
"
:style="{ width: '700px', maxWidth: '95vw' }"
>
<n-spin :show="classDetailLoading" style="min-height: 200px">
<n-flex v-if="classDetailData" vertical :size="12">
<n-grid :cols="5" :x-gap="8" responsive="screen">
<n-gi>
<n-statistic label="总AC数" :value="classDetailData.total_ac" size="large" class="stat-total-ac">
<n-statistic
label="总AC数"
:value="classDetailData.total_ac"
size="large"
class="stat-total-ac"
>
<template #suffix>
<Icon icon="streamline-emojis:raised-fist-1" width="20" />
</template>
</n-statistic>
</n-gi>
<n-gi>
<n-statistic label="平均AC数" :value="classDetailData.avg_ac.toFixed(2)" size="large" class="stat-avg-ac">
<n-statistic
label="平均AC数"
:value="classDetailData.avg_ac.toFixed(2)"
size="large"
class="stat-avg-ac"
>
<template #suffix>
<Icon icon="streamline-emojis:chart" width="20" />
</template>
</n-statistic>
</n-gi>
<n-gi>
<n-statistic label="中位数AC数" :value="classDetailData.median_ac.toFixed(2)" size="large" class="stat-median-ac">
<n-statistic
label="中位数AC数"
:value="classDetailData.median_ac.toFixed(2)"
size="large"
class="stat-median-ac"
>
<template #suffix>
<Icon icon="streamline-emojis:target" width="20" />
</template>
</n-statistic>
</n-gi>
<n-gi>
<n-statistic label="总提交数" :value="classDetailData.total_submission" size="large" class="stat-total-submission">
<n-statistic
label="总提交数"
:value="classDetailData.total_submission"
size="large"
class="stat-total-submission"
>
<template #suffix>
<Icon icon="streamline-emojis:paper" width="20" />
</template>
</n-statistic>
</n-gi>
<n-gi>
<n-statistic label="AC率" :value="classDetailData.ac_rate.toFixed(1) + '%'" size="large" class="stat-ac-rate">
<n-statistic
label="AC率"
:value="classDetailData.ac_rate.toFixed(1) + '%'"
size="large"
class="stat-ac-rate"
>
<template #suffix>
<Icon icon="streamline-emojis:check-mark" width="20" />
</template>
@@ -559,43 +588,88 @@ watch(
<n-divider style="margin: 12px 0" />
<n-descriptions bordered :column="2" size="small" label-placement="left">
<n-descriptions
bordered
:column="2"
size="small"
label-placement="left"
>
<n-descriptions-item label="第一四分位数(Q1)">
<span style="color: #9254de; font-weight: 500">{{ classDetailData.q1_ac.toFixed(2) }}</span>
<span style="color: #9254de; font-weight: 500">{{
classDetailData.q1_ac.toFixed(2)
}}</span>
</n-descriptions-item>
<n-descriptions-item label="第三四分位数(Q3)">
<span style="color: #f759ab; font-weight: 500">{{ classDetailData.q3_ac.toFixed(2) }}</span>
<span style="color: #f759ab; font-weight: 500">{{
classDetailData.q3_ac.toFixed(2)
}}</span>
</n-descriptions-item>
<n-descriptions-item label="四分位距(IQR)">
<span style="color: #13c2c2; font-weight: 500">{{ classDetailData.iqr.toFixed(2) }}</span>
<span style="color: #13c2c2; font-weight: 500">{{
classDetailData.iqr.toFixed(2)
}}</span>
</n-descriptions-item>
<n-descriptions-item label="标准差">
<span style="color: #fa8c16; font-weight: 500">{{ classDetailData.std_dev.toFixed(2) }}</span>
<span style="color: #fa8c16; font-weight: 500">{{
classDetailData.std_dev.toFixed(2)
}}</span>
</n-descriptions-item>
<n-descriptions-item label="前10%均值">
<span style="color: #cf1322; font-weight: 600">{{ classDetailData.top_10_avg.toFixed(2) }}</span>
<span style="color: #cf1322; font-weight: 600">{{
classDetailData.top_10_avg.toFixed(2)
}}</span>
</n-descriptions-item>
<n-descriptions-item label="中间80%均值">
<span style="color: #389e0d; font-weight: 600">{{ classDetailData.middle_80_avg.toFixed(2) }}</span>
<span style="color: #389e0d; font-weight: 600">{{
classDetailData.middle_80_avg.toFixed(2)
}}</span>
</n-descriptions-item>
<n-descriptions-item label="后10%均值">
<span style="color: #096dd9; font-weight: 500">{{ classDetailData.bottom_10_avg.toFixed(2) }}</span>
<span style="color: #096dd9; font-weight: 500">{{
classDetailData.bottom_10_avg.toFixed(2)
}}</span>
</n-descriptions-item>
<n-descriptions-item label="人数">
<span style="color: #1890ff; font-weight: 600">{{ classDetailData.user_count }}</span>
<span style="color: #1890ff; font-weight: 600">{{
classDetailData.user_count
}}</span>
</n-descriptions-item>
</n-descriptions>
<n-card size="small" title="比率统计" embedded style="margin-top: 12px">
<n-space vertical :size="10">
<n-progress type="line" :percentage="classDetailData.excellent_rate" :show-indicator="true" :border-radius="4">
<template #default>优秀率: {{ classDetailData.excellent_rate.toFixed(1) }}%</template>
<n-progress
type="line"
:percentage="classDetailData.excellent_rate"
:show-indicator="true"
:border-radius="4"
>
<template #default
>优秀率:
{{ classDetailData.excellent_rate.toFixed(1) }}%</template
>
</n-progress>
<n-progress type="line" :percentage="classDetailData.pass_rate" :show-indicator="true" :border-radius="4" status="success">
<template #default>及格率: {{ classDetailData.pass_rate.toFixed(1) }}%</template>
<n-progress
type="line"
:percentage="classDetailData.pass_rate"
:show-indicator="true"
:border-radius="4"
status="success"
>
<template #default
>及格率: {{ classDetailData.pass_rate.toFixed(1) }}%</template
>
</n-progress>
<n-progress type="line" :percentage="classDetailData.active_rate" :show-indicator="true" :border-radius="4" status="info">
<template #default>参与度: {{ classDetailData.active_rate.toFixed(1) }}%</template>
<n-progress
type="line"
:percentage="classDetailData.active_rate"
:show-indicator="true"
:border-radius="4"
status="info"
>
<template #default
>参与度: {{ classDetailData.active_rate.toFixed(1) }}%</template
>
</n-progress>
</n-space>
</n-card>
@@ -606,7 +680,11 @@ watch(
</n-tag>
</n-flex>
</n-flex>
<n-empty v-else-if="!classDetailLoading" description="暂无数据" style="padding: 40px 0" />
<n-empty
v-else-if="!classDetailLoading"
description="暂无数据"
style="padding: 40px 0"
/>
</n-spin>
</n-modal>
</template>

View File

@@ -1,6 +1,11 @@
import { DetailsData, DurationData } from "utils/types"
import { consumeJSONEventStream } from "utils/stream"
import { getAIDetailData, getAIDurationData, getAIHeatmapData, getAIPinnedReport } from "../api"
import {
getAIDetailData,
getAIDurationData,
getAIHeatmapData,
getAIPinnedReport,
} from "../api"
import { getCSRFToken } from "utils/functions"
export const useAIStore = defineStore("ai", () => {

29
src/oj/transforms.ts Normal file
View File

@@ -0,0 +1,29 @@
import { DIFFICULTY } from "utils/constants"
import { getACRate } from "utils/functions"
import type { Problem } from "utils/types"
// 把后端的 Problem 塑形成列表项需要的形状,与请求逻辑解耦。
export function filterResult(result: Problem) {
const newResult = {
id: result.id,
_id: result._id,
title: result.title,
difficulty: DIFFICULTY[result.difficulty],
tags: result.tags,
submission: result.submission_number,
rate: getACRate(result.accepted_number, result.submission_number),
status: "",
author: result.created_by.username,
allow_flowchart: result.allow_flowchart,
show_flowchart: result.show_flowchart,
has_ast_rules: result.has_ast_rules,
}
if (result.my_status === null || result.my_status === undefined) {
newResult.status = "not_test"
} else if (result.my_status === 0) {
newResult.status = "passed"
} else {
newResult.status = "failed"
}
return newResult
}