update
This commit is contained in:
@@ -8,8 +8,14 @@
|
|||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-alert v-if="pinnedReports.length > 0" type="warning" :show-icon="true" style="margin-bottom: 12px">
|
<n-alert
|
||||||
以下 <strong>{{ pinnedReports.length }}</strong> 位用户的 AI 分析报告已被锁定,前台将固定显示该报告:
|
v-if="pinnedReports.length > 0"
|
||||||
|
type="warning"
|
||||||
|
:show-icon="true"
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
>
|
||||||
|
以下 <strong>{{ pinnedReports.length }}</strong> 位用户的 AI
|
||||||
|
分析报告已被锁定,前台将固定显示该报告:
|
||||||
<n-flex style="margin-top: 8px" :wrap="true" :size="[8, 6]">
|
<n-flex style="margin-top: 8px" :wrap="true" :size="[8, 6]">
|
||||||
<n-tag
|
<n-tag
|
||||||
v-for="r in pinnedReports"
|
v-for="r in pinnedReports"
|
||||||
@@ -30,13 +36,24 @@
|
|||||||
v-model:page="query.page"
|
v-model:page="query.page"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<n-modal v-model:show="showModal" preset="card" title="分析报告详情" style="width: 800px; max-width: 95vw">
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="card"
|
||||||
|
title="分析报告详情"
|
||||||
|
style="width: 800px; max-width: 95vw"
|
||||||
|
>
|
||||||
<n-spin :show="loadingDetail">
|
<n-spin :show="loadingDetail">
|
||||||
<div v-if="detail" class="detail">
|
<div v-if="detail" class="detail">
|
||||||
<n-descriptions :column="2" bordered size="small" class="meta">
|
<n-descriptions :column="2" bordered size="small" class="meta">
|
||||||
<n-descriptions-item label="用户">{{ detail.username }}</n-descriptions-item>
|
<n-descriptions-item label="用户">{{
|
||||||
<n-descriptions-item label="班级">{{ detail.class_name || "-" }}</n-descriptions-item>
|
detail.username
|
||||||
<n-descriptions-item label="时间" :span="2">{{ parseTime(detail.create_time, "YYYY-MM-DD HH:mm:ss") }}</n-descriptions-item>
|
}}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="班级">{{
|
||||||
|
detail.class_name || "-"
|
||||||
|
}}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="时间" :span="2">{{
|
||||||
|
parseTime(detail.create_time, "YYYY-MM-DD HH:mm:ss")
|
||||||
|
}}</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
<n-scrollbar style="max-height: 60vh; margin-top: 12px">
|
<n-scrollbar style="max-height: 60vh; margin-top: 12px">
|
||||||
<MdPreview :model-value="detail.analysis" />
|
<MdPreview :model-value="detail.analysis" />
|
||||||
@@ -51,7 +68,12 @@ import { MdPreview } from "md-editor-v3"
|
|||||||
import "md-editor-v3/lib/preview.css"
|
import "md-editor-v3/lib/preview.css"
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import { getAIReportList, getAIReportDetail, pinAIReport, getPinnedAIReports } from "../api"
|
import {
|
||||||
|
getAIReportList,
|
||||||
|
getAIReportDetail,
|
||||||
|
pinAIReport,
|
||||||
|
getPinnedAIReports,
|
||||||
|
} from "../api"
|
||||||
import { NButton, NTag } from "naive-ui"
|
import { NButton, NTag } from "naive-ui"
|
||||||
|
|
||||||
interface ReportItem {
|
interface ReportItem {
|
||||||
@@ -83,7 +105,11 @@ const columns: DataTableColumn<ReportItem>[] = [
|
|||||||
key: "username",
|
key: "username",
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h("span", { style: row.is_pinned ? "font-weight:600" : "" }, row.username),
|
h(
|
||||||
|
"span",
|
||||||
|
{ style: row.is_pinned ? "font-weight:600" : "" },
|
||||||
|
row.username,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI 分析内容",
|
title: "AI 分析内容",
|
||||||
@@ -111,7 +137,11 @@ const columns: DataTableColumn<ReportItem>[] = [
|
|||||||
width: 160,
|
width: 160,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h("span", { style: "display:flex;gap:8px" }, [
|
h("span", { style: "display:flex;gap:8px" }, [
|
||||||
h(NButton, { size: "small", type: "primary", onClick: () => openDetail(row.id) }, () => "查看"),
|
h(
|
||||||
|
NButton,
|
||||||
|
{ size: "small", type: "primary", onClick: () => openDetail(row.id) },
|
||||||
|
() => "查看",
|
||||||
|
),
|
||||||
h(
|
h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
@@ -156,7 +186,10 @@ async function openDetail(id: number) {
|
|||||||
|
|
||||||
onMounted(() => Promise.all([listReports(), loadPinnedReports()]))
|
onMounted(() => Promise.all([listReports(), loadPinnedReports()]))
|
||||||
watch(() => [query.page, query.limit], listReports)
|
watch(() => [query.page, query.limit], listReports)
|
||||||
watchDebounced(() => query.username, listReports, { debounce: 500, maxWait: 1000 })
|
watchDebounced(() => query.username, listReports, {
|
||||||
|
debounce: 500,
|
||||||
|
maxWait: 1000,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import http from "utils/http"
|
import http from "utils/http"
|
||||||
|
import { toProblemListItem } from "admin/transforms"
|
||||||
import type {
|
import type {
|
||||||
AdminProblem,
|
AdminProblem,
|
||||||
Announcement,
|
Announcement,
|
||||||
@@ -30,7 +31,9 @@ export async function getProblemList(
|
|||||||
contestID?: string,
|
contestID?: string,
|
||||||
) {
|
) {
|
||||||
const endpoint = !!contestID ? "admin/contest/problem" : "admin/problem"
|
const endpoint = !!contestID ? "admin/contest/problem" : "admin/problem"
|
||||||
const res = await http.get(endpoint, {
|
const res = await http.get<{ results: AdminProblem[]; total: number }>(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
paging: true,
|
paging: true,
|
||||||
offset,
|
offset,
|
||||||
@@ -39,21 +42,10 @@ export async function getProblemList(
|
|||||||
author,
|
author,
|
||||||
contest_id: contestID,
|
contest_id: contestID,
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
results: res.data.results.map((result: AdminProblem) => ({
|
results: res.data.results.map(toProblemListItem),
|
||||||
id: result.id,
|
|
||||||
_id: result._id,
|
|
||||||
title: result.title,
|
|
||||||
username: result.created_by.username,
|
|
||||||
create_time: result.create_time,
|
|
||||||
visible: result.visible,
|
|
||||||
difficulty: result.difficulty,
|
|
||||||
tags: result.tags,
|
|
||||||
has_ast_rules: result.has_ast_rules,
|
|
||||||
allow_flowchart: result.allow_flowchart,
|
|
||||||
show_flowchart: result.show_flowchart,
|
|
||||||
})),
|
|
||||||
total: res.data.total,
|
total: res.data.total,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,10 +125,10 @@ export function getContestList(offset = 0, limit = 10, keyword: string) {
|
|||||||
export async function uploadImage(file: File): Promise<string> {
|
export async function uploadImage(file: File): Promise<string> {
|
||||||
const form = new window.FormData()
|
const form = new window.FormData()
|
||||||
form.append("image", file)
|
form.append("image", file)
|
||||||
const res: { success: boolean; file_path: string; msg: "Success" } =
|
// 该端点不走 { error, data } 信封,直接返回上传结果
|
||||||
await http.post("admin/upload_image", form, {
|
const res = (await http.post("admin/upload_image", form, {
|
||||||
headers: { "content-type": "multipart/form-data" },
|
headers: { "content-type": "multipart/form-data" },
|
||||||
})
|
})) as unknown as { success: boolean; file_path: string; msg: "Success" }
|
||||||
return res.success ? res.file_path : ""
|
return res.success ? res.file_path : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,17 +236,17 @@ export function deleteComment(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getTutorialList() {
|
export async function getTutorialList() {
|
||||||
const res = await http.get("admin/tutorial")
|
const res = await http.get<Tutorial[]>("admin/tutorial")
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTutorial(id: number) {
|
export async function getTutorial(id: number) {
|
||||||
const res = await http.get("admin/tutorial", { params: { id } })
|
const res = await http.get<Tutorial>("admin/tutorial", { params: { id } })
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTutorial(data: Partial<Tutorial>) {
|
export async function createTutorial(data: Partial<Tutorial>) {
|
||||||
const res = await http.post("admin/tutorial", data)
|
const res = await http.post<Tutorial>("admin/tutorial", data)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,10 +264,10 @@ export function setTutorialVisibility(id: number, is_public: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminExercises(tutorialId: number) {
|
export async function getAdminExercises(tutorialId: number) {
|
||||||
const res = await http.get("admin/exercise", {
|
const res = await http.get<Exercise[]>("admin/exercise", {
|
||||||
params: { tutorial_id: tutorialId },
|
params: { tutorial_id: tutorialId },
|
||||||
})
|
})
|
||||||
return res.data as Exercise[]
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createExercise(data: {
|
export async function createExercise(data: {
|
||||||
@@ -284,8 +276,8 @@ export async function createExercise(data: {
|
|||||||
data: object
|
data: object
|
||||||
order: number
|
order: number
|
||||||
}) {
|
}) {
|
||||||
const res = await http.post("admin/exercise", data)
|
const res = await http.post<Exercise>("admin/exercise", data)
|
||||||
return res.data as Exercise
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateExercise(data: {
|
export async function updateExercise(data: {
|
||||||
|
|||||||
18
src/admin/transforms.ts
Normal file
18
src/admin/transforms.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { AdminProblem } from "utils/types"
|
||||||
|
|
||||||
|
// 把后端的 AdminProblem 塑形成管理端列表项,与请求逻辑解耦。
|
||||||
|
export function toProblemListItem(result: AdminProblem) {
|
||||||
|
return {
|
||||||
|
id: result.id,
|
||||||
|
_id: result._id,
|
||||||
|
title: result.title,
|
||||||
|
username: result.created_by.username,
|
||||||
|
create_time: result.create_time,
|
||||||
|
visible: result.visible,
|
||||||
|
difficulty: result.difficulty,
|
||||||
|
tags: result.tags,
|
||||||
|
has_ast_rules: result.has_ast_rules,
|
||||||
|
allow_flowchart: result.allow_flowchart,
|
||||||
|
show_flowchart: result.show_flowchart,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,13 @@ const isNotRegularUser = computed(
|
|||||||
>
|
>
|
||||||
{{ getUserRole(props.user.admin_type).label }}
|
{{ getUserRole(props.user.admin_type).label }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag size="small" v-if="props.user.admin_type === USER_TYPE.STUDENT_ADMIN || props.user.admin_type === USER_TYPE.TEACHER_ADMIN">
|
<n-tag
|
||||||
|
size="small"
|
||||||
|
v-if="
|
||||||
|
props.user.admin_type === USER_TYPE.STUDENT_ADMIN ||
|
||||||
|
props.user.admin_type === USER_TYPE.TEACHER_ADMIN
|
||||||
|
"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
props.user.problem_permission === PROBLEM_PERMISSION.ALL
|
props.user.problem_permission === PROBLEM_PERMISSION.ALL
|
||||||
? "全部"
|
? "全部"
|
||||||
|
|||||||
@@ -314,7 +314,11 @@ watch(() => [query.page, query.limit, query.type, query.orderBy], listUsers)
|
|||||||
<n-input v-model:value="password" />
|
<n-input v-model:value="password" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi
|
<n-form-item-gi
|
||||||
v-if="!create && (userEditing.admin_type === USER_TYPE.STUDENT_ADMIN || userEditing.admin_type === USER_TYPE.TEACHER_ADMIN)"
|
v-if="
|
||||||
|
!create &&
|
||||||
|
(userEditing.admin_type === USER_TYPE.STUDENT_ADMIN ||
|
||||||
|
userEditing.admin_type === USER_TYPE.TEACHER_ADMIN)
|
||||||
|
"
|
||||||
:span="1"
|
:span="1"
|
||||||
label="出题权限"
|
label="出题权限"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -65,9 +65,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
next("/")
|
next("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (to.matched.some((record) => record.meta.requiresTeacherAdmin)) {
|
||||||
to.matched.some((record) => record.meta.requiresTeacherAdmin)
|
|
||||||
) {
|
|
||||||
if (!userStore.isTeacherOrAbove) {
|
if (!userStore.isTeacherOrAbove) {
|
||||||
next("/")
|
next("/")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { DIFFICULTY } from "utils/constants"
|
|
||||||
import { getACRate } from "utils/functions"
|
|
||||||
import http from "utils/http"
|
import http from "utils/http"
|
||||||
import {
|
import { filterResult } from "oj/transforms"
|
||||||
|
import type {
|
||||||
Exercise,
|
Exercise,
|
||||||
Problem,
|
Problem,
|
||||||
Submission,
|
Submission,
|
||||||
@@ -9,31 +8,6 @@ import {
|
|||||||
SubmitCodePayload,
|
SubmitCodePayload,
|
||||||
} from "utils/types"
|
} 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() {
|
export function getWebsiteConfig() {
|
||||||
return http.get("website")
|
return http.get("website")
|
||||||
}
|
}
|
||||||
@@ -43,17 +17,9 @@ export async function getProblemList(
|
|||||||
limit = 10,
|
limit = 10,
|
||||||
searchParams: any = {},
|
searchParams: any = {},
|
||||||
) {
|
) {
|
||||||
let params: any = {
|
const res = await http.get<{ results: Problem[]; total: number }>("problem", {
|
||||||
paging: true,
|
params: { paging: true, offset, limit, ...searchParams },
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
Object.keys(searchParams).forEach((element) => {
|
|
||||||
if (searchParams[element]) {
|
|
||||||
params[element] = searchParams[element]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const res = await http.get("problem", { params })
|
|
||||||
return {
|
return {
|
||||||
results: res.data.results.map(filterResult),
|
results: res.data.results.map(filterResult),
|
||||||
total: res.data.total,
|
total: res.data.total,
|
||||||
@@ -203,7 +169,7 @@ export function checkContestPassword(contestID: string, password: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getContestProblems(contestID: 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 },
|
params: { contest_id: contestID },
|
||||||
})
|
})
|
||||||
return res.data.map(filterResult)
|
return res.data.map(filterResult)
|
||||||
@@ -460,7 +426,7 @@ export function getProblemSetUserProgress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getExercises(tutorialId: number): Promise<Exercise[]> {
|
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 },
|
params: { tutorial_id: tutorialId },
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ async function analyzeWithAI() {
|
|||||||
aiController = controller
|
aiController = controller
|
||||||
|
|
||||||
const timeRangeLabel =
|
const timeRangeLabel =
|
||||||
timeRangeOptions.find((o) => o.value === duration.value)?.label ?? "全部时间"
|
timeRangeOptions.find((o) => o.value === duration.value)?.label ??
|
||||||
|
"全部时间"
|
||||||
|
|
||||||
showAIModal.value = true
|
showAIModal.value = true
|
||||||
aiContent.value = ""
|
aiContent.value = ""
|
||||||
@@ -195,7 +196,11 @@ async function analyzeWithAI() {
|
|||||||
if (event === "end" && !hasStarted) aiLoading.value = false
|
if (event === "end" && !hasStarted) aiLoading.value = false
|
||||||
},
|
},
|
||||||
onMessage(payload) {
|
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 (parsed.type === "delta" && parsed.content) {
|
||||||
if (!hasStarted) {
|
if (!hasStarted) {
|
||||||
hasStarted = true
|
hasStarted = true
|
||||||
@@ -1176,7 +1181,6 @@ const radarChartOptions = {
|
|||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 对比表格 -->
|
<!-- 对比表格 -->
|
||||||
@@ -1185,10 +1189,7 @@ const radarChartOptions = {
|
|||||||
title="对比表格"
|
title="对比表格"
|
||||||
style="margin-top: 20px"
|
style="margin-top: 20px"
|
||||||
>
|
>
|
||||||
<n-data-table
|
<n-data-table :data="comparisons" :columns="tableColumns" />
|
||||||
:data="comparisons"
|
|
||||||
:columns="tableColumns"
|
|
||||||
/>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|||||||
@@ -85,16 +85,14 @@ function inputWidth(idx: number): string {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card
|
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
|
||||||
style="margin: 16px 0; border: 1.5px solid var(--n-border-color)"
|
|
||||||
>
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-tag type="warning" :bordered="false"
|
<n-tag type="warning" :bordered="false">练一练 · 代码填空</n-tag>
|
||||||
>练一练 · 代码填空</n-tag
|
|
||||||
>
|
|
||||||
</template>
|
</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
|
<pre
|
||||||
:style="{
|
:style="{
|
||||||
@@ -147,11 +145,7 @@ function inputWidth(idx: number): string {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<n-space style="margin-top: 12px" :size="8">
|
<n-space style="margin-top: 12px" :size="8">
|
||||||
<n-button
|
<n-button type="warning" :disabled="allCorrect" @click="submit">
|
||||||
type="warning"
|
|
||||||
:disabled="allCorrect"
|
|
||||||
@click="submit"
|
|
||||||
>
|
|
||||||
提交
|
提交
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="reset">重置</n-button>
|
<n-button @click="reset">重置</n-button>
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ function optionType(idx: number): "default" | "primary" | "success" {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card
|
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
|
||||||
style="margin: 16px 0; border: 1.5px solid var(--n-border-color)"
|
|
||||||
>
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-space align="center" :size="8">
|
<n-space align="center" :size="8">
|
||||||
<n-tag type="success" :bordered="false">
|
<n-tag type="success" :bordered="false">
|
||||||
@@ -74,7 +72,9 @@ function optionType(idx: number): "default" | "primary" | "success" {
|
|||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</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-space vertical :size="8">
|
||||||
<n-button
|
<n-button
|
||||||
|
|||||||
@@ -101,16 +101,14 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card
|
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
|
||||||
style="margin: 16px 0; border: 1.5px solid var(--n-border-color)"
|
|
||||||
>
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-tag type="info" :bordered="false"
|
<n-tag type="info" :bordered="false">练一练 · 代码排序</n-tag>
|
||||||
>练一练 · 代码排序</n-tag
|
|
||||||
>
|
|
||||||
</template>
|
</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">
|
<n-space vertical :size="6">
|
||||||
<div
|
<div
|
||||||
@@ -157,11 +155,7 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<n-space style="margin-top: 12px" :size="8">
|
<n-space style="margin-top: 12px" :size="8">
|
||||||
<n-button
|
<n-button type="info" :disabled="submitted && allCorrect" @click="submit">
|
||||||
type="info"
|
|
||||||
:disabled="submitted && allCorrect"
|
|
||||||
@click="submit"
|
|
||||||
>
|
|
||||||
提交
|
提交
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="reset">重置</n-button>
|
<n-button @click="reset">重置</n-button>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="learn-container">
|
<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-gi :span="1" class="learn-col">
|
||||||
<n-card title="教程目录" :bordered="false" size="small">
|
<n-card title="教程目录" :bordered="false" size="small">
|
||||||
<n-list hoverable clickable>
|
<n-list hoverable clickable>
|
||||||
@@ -51,7 +56,11 @@
|
|||||||
class="code-card"
|
class="code-card"
|
||||||
content-style="height: calc(100% - 44px); padding: 0;"
|
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-card>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|||||||
@@ -514,42 +514,71 @@ watch(
|
|||||||
<n-modal
|
<n-modal
|
||||||
v-model:show="showClassDetailModal"
|
v-model:show="showClassDetailModal"
|
||||||
preset="card"
|
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' }"
|
:style="{ width: '700px', maxWidth: '95vw' }"
|
||||||
>
|
>
|
||||||
<n-spin :show="classDetailLoading" style="min-height: 200px">
|
<n-spin :show="classDetailLoading" style="min-height: 200px">
|
||||||
<n-flex v-if="classDetailData" vertical :size="12">
|
<n-flex v-if="classDetailData" vertical :size="12">
|
||||||
<n-grid :cols="5" :x-gap="8" responsive="screen">
|
<n-grid :cols="5" :x-gap="8" responsive="screen">
|
||||||
<n-gi>
|
<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>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:raised-fist-1" width="20" />
|
<Icon icon="streamline-emojis:raised-fist-1" width="20" />
|
||||||
</template>
|
</template>
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<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>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:chart" width="20" />
|
<Icon icon="streamline-emojis:chart" width="20" />
|
||||||
</template>
|
</template>
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<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>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:target" width="20" />
|
<Icon icon="streamline-emojis:target" width="20" />
|
||||||
</template>
|
</template>
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<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>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:paper" width="20" />
|
<Icon icon="streamline-emojis:paper" width="20" />
|
||||||
</template>
|
</template>
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<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>
|
<template #suffix>
|
||||||
<Icon icon="streamline-emojis:check-mark" width="20" />
|
<Icon icon="streamline-emojis:check-mark" width="20" />
|
||||||
</template>
|
</template>
|
||||||
@@ -559,43 +588,88 @@ watch(
|
|||||||
|
|
||||||
<n-divider style="margin: 12px 0" />
|
<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)">
|
<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>
|
||||||
<n-descriptions-item label="第三四分位数(Q3)">
|
<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>
|
||||||
<n-descriptions-item label="四分位距(IQR)">
|
<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>
|
||||||
<n-descriptions-item label="标准差">
|
<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>
|
||||||
<n-descriptions-item label="前10%均值">
|
<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>
|
||||||
<n-descriptions-item label="中间80%均值">
|
<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>
|
||||||
<n-descriptions-item label="后10%均值">
|
<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>
|
||||||
<n-descriptions-item label="人数">
|
<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-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
|
|
||||||
<n-card size="small" title="比率统计" embedded style="margin-top: 12px">
|
<n-card size="small" title="比率统计" embedded style="margin-top: 12px">
|
||||||
<n-space vertical :size="10">
|
<n-space vertical :size="10">
|
||||||
<n-progress type="line" :percentage="classDetailData.excellent_rate" :show-indicator="true" :border-radius="4">
|
<n-progress
|
||||||
<template #default>优秀率: {{ classDetailData.excellent_rate.toFixed(1) }}%</template>
|
type="line"
|
||||||
|
:percentage="classDetailData.excellent_rate"
|
||||||
|
:show-indicator="true"
|
||||||
|
:border-radius="4"
|
||||||
|
>
|
||||||
|
<template #default
|
||||||
|
>优秀率:
|
||||||
|
{{ classDetailData.excellent_rate.toFixed(1) }}%</template
|
||||||
|
>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
<n-progress type="line" :percentage="classDetailData.pass_rate" :show-indicator="true" :border-radius="4" status="success">
|
<n-progress
|
||||||
<template #default>及格率: {{ classDetailData.pass_rate.toFixed(1) }}%</template>
|
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>
|
||||||
<n-progress type="line" :percentage="classDetailData.active_rate" :show-indicator="true" :border-radius="4" status="info">
|
<n-progress
|
||||||
<template #default>参与度: {{ classDetailData.active_rate.toFixed(1) }}%</template>
|
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-progress>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -606,7 +680,11 @@ watch(
|
|||||||
</n-tag>
|
</n-tag>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</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-spin>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { DetailsData, DurationData } from "utils/types"
|
import { DetailsData, DurationData } from "utils/types"
|
||||||
import { consumeJSONEventStream } from "utils/stream"
|
import { consumeJSONEventStream } from "utils/stream"
|
||||||
import { getAIDetailData, getAIDurationData, getAIHeatmapData, getAIPinnedReport } from "../api"
|
import {
|
||||||
|
getAIDetailData,
|
||||||
|
getAIDurationData,
|
||||||
|
getAIHeatmapData,
|
||||||
|
getAIPinnedReport,
|
||||||
|
} from "../api"
|
||||||
import { getCSRFToken } from "utils/functions"
|
import { getCSRFToken } from "utils/functions"
|
||||||
|
|
||||||
export const useAIStore = defineStore("ai", () => {
|
export const useAIStore = defineStore("ai", () => {
|
||||||
|
|||||||
29
src/oj/transforms.ts
Normal file
29
src/oj/transforms.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -169,10 +169,7 @@ import {
|
|||||||
BarElement,
|
BarElement,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
} from "chart.js"
|
} from "chart.js"
|
||||||
import {
|
import { WordCloudController, WordElement } from "chartjs-chart-wordcloud"
|
||||||
WordCloudController,
|
|
||||||
WordElement,
|
|
||||||
} from "chartjs-chart-wordcloud"
|
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
ArcElement,
|
ArcElement,
|
||||||
@@ -332,16 +329,16 @@ const gradeChartData = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const completionChartData = computed(() => {
|
const completionChartData = computed(() => {
|
||||||
const uncompleted = Math.max(0, adjustedPersonCount.value - data.completed_count)
|
const uncompleted = Math.max(
|
||||||
|
0,
|
||||||
|
adjustedPersonCount.value - data.completed_count,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
labels: ["已完成", "未完成"],
|
labels: ["已完成", "未完成"],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
data: [data.completed_count, uncompleted],
|
data: [data.completed_count, uncompleted],
|
||||||
backgroundColor: [
|
backgroundColor: ["rgba(106, 176, 76, 0.6)", "rgba(255, 159, 64, 0.6)"],
|
||||||
"rgba(106, 176, 76, 0.6)",
|
|
||||||
"rgba(255, 159, 64, 0.6)",
|
|
||||||
],
|
|
||||||
borderColor: ["rgba(106, 176, 76, 1)", "rgba(255, 159, 64, 1)"],
|
borderColor: ["rgba(106, 176, 76, 1)", "rgba(255, 159, 64, 1)"],
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
@@ -490,9 +487,7 @@ function renderWordCloud() {
|
|||||||
{
|
{
|
||||||
label: "",
|
label: "",
|
||||||
data: words.map((w) => 10 + (w.count / maxCount) * 50),
|
data: words.map((w) => 10 + (w.count / maxCount) * 50),
|
||||||
color: words.map(
|
color: words.map((_, i) => WORD_COLORS[i % WORD_COLORS.length]),
|
||||||
(_, i) => WORD_COLORS[i % WORD_COLORS.length],
|
|
||||||
),
|
|
||||||
rotate: words.map(() => 0),
|
rotate: words.map(() => 0),
|
||||||
} as any,
|
} as any,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from "axios"
|
import axios, { type AxiosRequestConfig } from "axios"
|
||||||
import { createDiscreteApi } from "naive-ui"
|
import { createDiscreteApi } from "naive-ui"
|
||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
import storage from "./storage"
|
import storage from "./storage"
|
||||||
@@ -6,13 +6,56 @@ import { STORAGE_KEY } from "./constants"
|
|||||||
|
|
||||||
const { message } = createDiscreteApi(["message"])
|
const { message } = createDiscreteApi(["message"])
|
||||||
|
|
||||||
const http = axios.create({
|
// 后端统一返回 { error, data } 信封;拦截器剥掉 axios 外层后,
|
||||||
|
// 调用方拿到的就是这个信封,data 才是真正的业务数据。
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
error: string | null
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
// 让 http.get<T>() 的类型真实反映"解包后返回信封"这件事,
|
||||||
|
// 调用方 res.data 直接拿到带类型的 T,不再依赖 axios 的 AxiosResponse 巧合对齐。
|
||||||
|
interface Http {
|
||||||
|
get<T = any>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<ApiResponse<T>>
|
||||||
|
delete<T = any>(
|
||||||
|
url: string,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<ApiResponse<T>>
|
||||||
|
post<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<ApiResponse<T>>
|
||||||
|
put<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<ApiResponse<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
baseURL: "/api",
|
baseURL: "/api",
|
||||||
xsrfHeaderName: "X-CSRFToken",
|
xsrfHeaderName: "X-CSRFToken",
|
||||||
xsrfCookieName: "csrftoken",
|
xsrfCookieName: "csrftoken",
|
||||||
})
|
})
|
||||||
|
|
||||||
http.interceptors.response.use(
|
// 统一剥掉空字符串 / null / undefined 的 query 参数,
|
||||||
|
// 各 api 函数不必再手写过滤逻辑(保留 0、false)。
|
||||||
|
instance.interceptors.request.use((config) => {
|
||||||
|
if (config.params) {
|
||||||
|
config.params = Object.fromEntries(
|
||||||
|
Object.entries(config.params).filter(
|
||||||
|
([, v]) => v !== "" && v !== null && v !== undefined,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
instance.interceptors.response.use(
|
||||||
(res) => {
|
(res) => {
|
||||||
if (res.data.error) {
|
if (res.data.error) {
|
||||||
if (res.data.error === "login-required") {
|
if (res.data.error === "login-required") {
|
||||||
@@ -31,4 +74,6 @@ http.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const http = instance as unknown as Http
|
||||||
|
|
||||||
export default http
|
export default http
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ export interface Profile {
|
|||||||
submission_number: number
|
submission_number: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserAdminType = "Regular User" | "Student Admin" | "Teacher Admin" | "Super Admin"
|
export type UserAdminType =
|
||||||
|
| "Regular User"
|
||||||
|
| "Student Admin"
|
||||||
|
| "Teacher Admin"
|
||||||
|
| "Super Admin"
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
Reference in New Issue
Block a user