refactor(前端): 拆掉 camelCase→snake_case 转换层,契约成为唯一真相
utils/legacy.ts 是迁移期的临时层:新后端一律 camelCase,而组件读的还是
旧 Django 的 snake_case,于是在 api 层做一次递归键名重写。它自己的注释就
写了「迁移完成后这一层应当整体拆掉」。现在拆了。
代价不只是那 96 处包装:每个响应都要递归遍历整个对象重写一遍键名,而且
utils/types.ts 和 packages/contract 是两份真相 —— 手抄的那份还抄歪了好几处。
做法是按域推进,每域都用 vue-tsc 相对基线做差,确认零新增错误后再往下走。
前端的类型现在一律以契约为准,只在必要处窄化(比如 languages/template 的键
窄化成 LANGUAGE),删掉的重复定义包括 WebsiteConfig、LoginSummary、
AchievementSummary、ProblemSet、Contest、User、Profile、AdminTag、
StuckProblem 等等,其中 ClassComparison 有两个组件各手抄了一份。
## 顺带修掉的真 bug
- 管理端公告列表的「可见」开关每次都 400:列表响应被契约 omit 掉了 content,
而更新接口要求 content 必填,toggleVisible 把列表行原样回传。而且是乐观
翻转、不 await 不 catch,管理员看到开关动了、实际没存也没有提示。
改成先 GET 整条再 PUT,加失败提示。
- 删有提交的题时只显示笼统的「删除失败」:前端还在 match 旧 Django 的英文
文案,而后端返回的是 problem-has-submissions + 中文。连同另外 8 处同类
匹配一起改成判错误码 —— 文案是后端随时能改的,match 文案改一个字就静默失效。
- SubmissionStatus.time_limit_exceeded 写成 `1 | 2`,TS 按位或算成 3,和
memory_limit_exceeded 撞了同一个值。后端 judge/status.ts 里这是分开的
两个码,按后端拆成 cpu_/real_ 两项。当前没有代码读这两个成员,但
CLAUDE.md 明确要求判题状态码三处同步。
- 流程图历史翻到没有提交的那一页会直接抛:契约里 submission 是 nullable,
被 any 掩盖成看起来非空。补了 null 分支。
## 契约里被逼出来的三处不诚实
- grade 写成 z.string(),但 averageGrade() 在没有可用数据时返回空串,
前端三张图表拿它查 Record<Grade,...> 会查出 undefined。按实际收紧成
z.enum([...,""]),四个查表点都补了「无评级」分支。
- difficulty 写成 z.string()。核对过生产库 dump:956 道题只有
Low/Mid/High 三个值(761/149/46)。收紧成枚举。
- topReaction 写成 z.string(),既对不上前端渲染的 {type,count},也对不上
旧后端 get_top_reactions 下发的形状。改成正确形状并注明当前恒传 null。
## 明确保留 snake_case 的 54 处
判题沙箱原始输出(cpu_time/exit_code/output_md5/compile_output)、
statistic_info 内容(err_info/time_cost/ast_results)、submission_info
JSONB(is_ac/ac_time/error_number,回滚时旧后端还要读)、SQL 判题引擎的
total_rows/order_sensitive/changed_tables、WebSocket 的 submission_id、
以及数据库选项键 enable_maxkb。每一处都在类型定义旁写了为什么不能改。
language 没有跟着收紧契约 —— 它是配置项、随时可能加语言,收紧会让新语言
在后端 parse 时直接抛。改在 api 边界一处窄化。
## 另外
- utils/http.ts 整个模块已是死代码(四处引用全是 import type),删除。
- profile 的 blog/github/school/major/language 五个字段全链路空转,没有
任何组件读,从契约到类型一并摘除(数据库列不动)。
- admin/account.ts 往 user_profile 塞的 totalScore 是 OI 模式遗留,表里
没这一列。Drizzle 按表定义拼列名会把它静默丢弃,所以没出过错,是死代码。
验证:vue-tsc 143 → 54 条且无新增,apps/api tsc、check:routes、web build
全通过;各域响应形状逐条打接口核对过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,15 +84,10 @@ accountRoutes.post("/users", async (c) => {
|
|||||||
userId: created.id,
|
userId: created.id,
|
||||||
acmProblemsStatus: {},
|
acmProblemsStatus: {},
|
||||||
avatar: `${config.avatarUriPrefix}/default.png`,
|
avatar: `${config.avatarUriPrefix}/default.png`,
|
||||||
blog: null,
|
|
||||||
mood: null,
|
mood: null,
|
||||||
acceptedNumber: 0,
|
acceptedNumber: 0,
|
||||||
submissionNumber: 0,
|
submissionNumber: 0,
|
||||||
github: null,
|
|
||||||
school: null,
|
|
||||||
major: null,
|
|
||||||
realName: null,
|
realName: null,
|
||||||
language: null,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return success(c, { ok: true }, 201)
|
return success(c, { ok: true }, 201)
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ adminAccountRoutes.post("/users", requireSuperAdmin, async (c) => {
|
|||||||
acmProblemsStatus: {},
|
acmProblemsStatus: {},
|
||||||
submissionNumber: 0,
|
submissionNumber: 0,
|
||||||
acceptedNumber: 0,
|
acceptedNumber: 0,
|
||||||
totalScore: 0,
|
|
||||||
})))
|
})))
|
||||||
return users.length
|
return users.length
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { loginRequestSchema } from "@oj2/contract"
|
||||||
loginRequestSchema,
|
import { eq, sql } from "drizzle-orm"
|
||||||
sessionUserSchema,
|
|
||||||
userProfileSchema,
|
|
||||||
} from "@oj2/contract"
|
|
||||||
import { and, eq, sql } from "drizzle-orm"
|
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
|
|
||||||
import { optionalAuth, type AppEnv } from "../auth/middleware"
|
import { optionalAuth, type AppEnv } from "../auth/middleware"
|
||||||
@@ -17,21 +13,31 @@ import { getUserProfileById } from "../services/profile"
|
|||||||
export const authRoutes = new Hono<AppEnv>()
|
export const authRoutes = new Hono<AppEnv>()
|
||||||
|
|
||||||
authRoutes.post("/auth/login", async (c) => {
|
authRoutes.post("/auth/login", async (c) => {
|
||||||
const parsed = loginRequestSchema.safeParse(await c.req.json().catch(() => null))
|
const parsed = loginRequestSchema.safeParse(
|
||||||
|
await c.req.json().catch(() => null),
|
||||||
|
)
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return failure(c, 400, "invalid-request", "Username and password are required")
|
return failure(
|
||||||
|
c,
|
||||||
|
400,
|
||||||
|
"invalid-request",
|
||||||
|
"Username and password are required",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [user] = await db
|
const [user] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.user)
|
.from(schema.user)
|
||||||
.where(
|
.where(sql`lower(${schema.user.username}) = lower(${parsed.data.username})`)
|
||||||
sql`lower(${schema.user.username}) = lower(${parsed.data.username})`,
|
|
||||||
)
|
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return failure(c, 401, "invalid-credentials", "Invalid username or password")
|
return failure(
|
||||||
|
c,
|
||||||
|
401,
|
||||||
|
"invalid-credentials",
|
||||||
|
"Invalid username or password",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (user.isDisabled) {
|
if (user.isDisabled) {
|
||||||
return failure(c, 403, "account-disabled", "Your account has been disabled")
|
return failure(c, 403, "account-disabled", "Your account has been disabled")
|
||||||
@@ -39,7 +45,12 @@ authRoutes.post("/auth/login", async (c) => {
|
|||||||
|
|
||||||
const password = await verifyPassword(parsed.data.password, user.password)
|
const password = await verifyPassword(parsed.data.password, user.password)
|
||||||
if (!password.valid) {
|
if (!password.valid) {
|
||||||
return failure(c, 401, "invalid-credentials", "Invalid username or password")
|
return failure(
|
||||||
|
c,
|
||||||
|
401,
|
||||||
|
"invalid-credentials",
|
||||||
|
"Invalid username or password",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
@@ -67,6 +78,7 @@ authRoutes.get("/me", optionalAuth, async (c) => {
|
|||||||
if (!authUser) return success(c, null)
|
if (!authUser) return success(c, null)
|
||||||
|
|
||||||
const data = await getUserProfileById(authUser.id, true)
|
const data = await getUserProfileById(authUser.id, true)
|
||||||
if (!data) return failure(c, 404, "profile-not-found", "User profile does not exist")
|
if (!data)
|
||||||
|
return failure(c, 404, "profile-not-found", "User profile does not exist")
|
||||||
return success(c, data)
|
return success(c, data)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,12 +29,7 @@ export async function getUserProfileById(userId: number, showRealName: boolean)
|
|||||||
realName: showRealName ? row.profile.realName : null,
|
realName: showRealName ? row.profile.realName : null,
|
||||||
acmProblemsStatus: row.profile.acmProblemsStatus,
|
acmProblemsStatus: row.profile.acmProblemsStatus,
|
||||||
avatar: row.profile.avatar,
|
avatar: row.profile.avatar,
|
||||||
blog: row.profile.blog,
|
|
||||||
mood: row.profile.mood,
|
mood: row.profile.mood,
|
||||||
github: row.profile.github,
|
|
||||||
school: row.profile.school,
|
|
||||||
major: row.profile.major,
|
|
||||||
language: row.profile.language,
|
|
||||||
acceptedNumber: row.profile.acceptedNumber,
|
acceptedNumber: row.profile.acceptedNumber,
|
||||||
submissionNumber: row.profile.submissionNumber,
|
submissionNumber: row.profile.submissionNumber,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AchievementRarity } from "utils/types"
|
||||||
import {
|
import {
|
||||||
createAchievement,
|
createAchievement,
|
||||||
getMetricOptions,
|
getMetricOptions,
|
||||||
@@ -29,7 +30,7 @@ function emptyForm() {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
icon: "noto:trophy",
|
icon: "noto:trophy",
|
||||||
rarity: "bronze",
|
rarity: "bronze" as AchievementRarity,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
metric: "",
|
metric: "",
|
||||||
operator: "gte" as "gte" | "lte",
|
operator: "gte" as "gte" | "lte",
|
||||||
@@ -46,7 +47,7 @@ const metricOptions = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const metricHelp = computed(
|
const metricHelp = computed(
|
||||||
() => metrics.value.find((m) => m.key === form.value.metric)?.help_text ?? "",
|
() => metrics.value.find((m) => m.key === form.value.metric)?.helpText ?? "",
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const columns: DataTableColumn<AdminAchievement>[] = [
|
|||||||
width: 90,
|
width: 90,
|
||||||
render: (row) => RARITY_LABEL[row.rarity] ?? row.rarity,
|
render: (row) => RARITY_LABEL[row.rarity] ?? row.rarity,
|
||||||
},
|
},
|
||||||
{ title: "指标", key: "metric_name" },
|
{ title: "指标", key: "metricName" },
|
||||||
{
|
{
|
||||||
title: "条件",
|
title: "条件",
|
||||||
key: "threshold",
|
key: "threshold",
|
||||||
@@ -93,7 +93,7 @@ const columns: DataTableColumn<AdminAchievement>[] = [
|
|||||||
width: 70,
|
width: 70,
|
||||||
render: (row) => (row.visible ? "是" : "否"),
|
render: (row) => (row.visible ? "是" : "否"),
|
||||||
},
|
},
|
||||||
{ title: "已解锁人数", key: "unlock_count", width: 110 },
|
{ title: "已解锁人数", key: "unlockCount", width: 110 },
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
|
|||||||
@@ -49,10 +49,10 @@
|
|||||||
detail.username
|
detail.username
|
||||||
}}</n-descriptions-item>
|
}}</n-descriptions-item>
|
||||||
<n-descriptions-item label="班级">{{
|
<n-descriptions-item label="班级">{{
|
||||||
detail.class_name || "-"
|
detail.className || "-"
|
||||||
}}</n-descriptions-item>
|
}}</n-descriptions-item>
|
||||||
<n-descriptions-item label="时间" :span="2">{{
|
<n-descriptions-item label="时间" :span="2">{{
|
||||||
parseTime(detail.create_time, "YYYY-MM-DD HH:mm:ss")
|
parseTime(detail.createTime, "YYYY-MM-DD HH:mm:ss")
|
||||||
}}</n-descriptions-item>
|
}}</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">
|
||||||
@@ -75,19 +75,10 @@ import {
|
|||||||
getPinnedAIReports,
|
getPinnedAIReports,
|
||||||
} from "../api"
|
} from "../api"
|
||||||
import { NButton, NTag } from "naive-ui"
|
import { NButton, NTag } from "naive-ui"
|
||||||
|
import type { AdminAiReport, AdminAiReportListItem } from "utils/types"
|
||||||
|
|
||||||
interface ReportItem {
|
type ReportItem = AdminAiReportListItem
|
||||||
id: number
|
type ReportDetail = AdminAiReport
|
||||||
create_time: string
|
|
||||||
username: string
|
|
||||||
analysis_excerpt: string
|
|
||||||
is_pinned: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReportDetail extends ReportItem {
|
|
||||||
analysis: string
|
|
||||||
class_name: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const reports = ref<ReportItem[]>([])
|
const reports = ref<ReportItem[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -105,29 +96,25 @@ const columns: DataTableColumn<ReportItem>[] = [
|
|||||||
key: "username",
|
key: "username",
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h("span", { style: row.isPinned ? "font-weight:600" : "" }, row.username),
|
||||||
"span",
|
|
||||||
{ style: row.is_pinned ? "font-weight:600" : "" },
|
|
||||||
row.username,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI 分析内容",
|
title: "AI 分析内容",
|
||||||
key: "analysis_excerpt",
|
key: "analysis_excerpt",
|
||||||
render: (row) => row.analysis_excerpt || "-",
|
render: (row) => row.analysisExcerpt || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "生成时间",
|
title: "生成时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "PIN 状态",
|
title: "PIN 状态",
|
||||||
key: "is_pinned",
|
key: "is_pinned",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
row.is_pinned
|
row.isPinned
|
||||||
? h(NTag, { type: "warning", size: "small" }, () => "已锁定")
|
? h(NTag, { type: "warning", size: "small" }, () => "已锁定")
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
@@ -146,10 +133,10 @@ const columns: DataTableColumn<ReportItem>[] = [
|
|||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
size: "small",
|
size: "small",
|
||||||
type: row.is_pinned ? "error" : "default",
|
type: row.isPinned ? "error" : "default",
|
||||||
onClick: () => togglePin(row),
|
onClick: () => togglePin(row),
|
||||||
},
|
},
|
||||||
() => (row.is_pinned ? "取消 PIN" : "PIN"),
|
() => (row.isPinned ? "取消 PIN" : "PIN"),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@@ -157,7 +144,7 @@ const columns: DataTableColumn<ReportItem>[] = [
|
|||||||
|
|
||||||
async function loadPinnedReports() {
|
async function loadPinnedReports() {
|
||||||
const res = await getPinnedAIReports()
|
const res = await getPinnedAIReports()
|
||||||
pinnedReports.value = res.data
|
pinnedReports.value = res.data.results
|
||||||
}
|
}
|
||||||
|
|
||||||
async function togglePin(row: ReportItem) {
|
async function togglePin(row: ReportItem) {
|
||||||
|
|||||||
@@ -2,18 +2,19 @@
|
|||||||
import { NSwitch } from "naive-ui"
|
import { NSwitch } from "naive-ui"
|
||||||
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 type { Announcement } from "utils/types"
|
import type { AnnouncementListItem } from "utils/types"
|
||||||
import { editAnnouncement, getAnnouncementList } from "../api"
|
import { editAnnouncement, getAnnouncement, getAnnouncementList } from "../api"
|
||||||
import Actions from "./components/Actions.vue"
|
import Actions from "./components/Actions.vue"
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
limit: 10,
|
limit: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
})
|
})
|
||||||
const announcements = ref<Announcement[]>([])
|
const announcements = ref<AnnouncementListItem[]>([])
|
||||||
|
|
||||||
const columns: DataTableColumn<Announcement>[] = [
|
const columns: DataTableColumn<AnnouncementListItem>[] = [
|
||||||
{ title: "ID", key: "id", width: 60 },
|
{ title: "ID", key: "id", width: 60 },
|
||||||
{ title: "标题", key: "title", minWidth: 300 },
|
{ title: "标题", key: "title", minWidth: 300 },
|
||||||
{ title: "标签", key: "tag", width: 80 },
|
{ title: "标签", key: "tag", width: 80 },
|
||||||
@@ -25,20 +26,20 @@ const columns: DataTableColumn<Announcement>[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "create_time",
|
key: "createTime",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "上次更新时间",
|
title: "上次更新时间",
|
||||||
key: "last_update_time",
|
key: "lastUpdateTime",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => parseTime(row.last_update_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.lastUpdateTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "作者",
|
title: "作者",
|
||||||
key: "created_by",
|
key: "createdBy",
|
||||||
render: (row) => row.created_by.username,
|
render: (row) => row.createdBy.username,
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -61,16 +62,24 @@ const columns: DataTableColumn<Announcement>[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async function toggleVisible(announcement: Announcement) {
|
// 列表响应不含 content(正文是 8MB 上限的富文本),而更新接口要求 content 必填 ——
|
||||||
announcement.visible = !announcement.visible
|
// 拿列表里那行直接回传会 400。所以先取回整条再改。
|
||||||
editAnnouncement({
|
async function toggleVisible(announcement: AnnouncementListItem) {
|
||||||
id: announcement.id,
|
const next = !announcement.visible
|
||||||
title: announcement.title,
|
try {
|
||||||
tag: announcement.tag,
|
const { data: full } = await getAnnouncement(announcement.id)
|
||||||
content: announcement.content,
|
await editAnnouncement({
|
||||||
visible: announcement.visible,
|
id: full.id,
|
||||||
top: announcement.top,
|
title: full.title,
|
||||||
})
|
tag: full.tag,
|
||||||
|
content: full.content,
|
||||||
|
visible: next,
|
||||||
|
top: full.top,
|
||||||
|
})
|
||||||
|
announcement.visible = next
|
||||||
|
} catch {
|
||||||
|
message.error("修改可见性失败")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAnnouncements() {
|
async function listAnnouncements() {
|
||||||
|
|||||||
@@ -1,32 +1,53 @@
|
|||||||
import api2 from "utils/api2"
|
import api2 from "utils/api2"
|
||||||
import { legacyResponse } from "utils/legacy"
|
|
||||||
import { toProblemListItem } from "admin/transforms"
|
import { toProblemListItem } from "admin/transforms"
|
||||||
import type {
|
import type {
|
||||||
|
AcTrend,
|
||||||
|
BatchProblemTagResponse,
|
||||||
|
GenerateSqlTestCaseResponse,
|
||||||
|
RenameTagResponse,
|
||||||
|
SqlTestCaseScript,
|
||||||
|
AcmHelperItem,
|
||||||
|
AdminAiReport,
|
||||||
|
AdminAiReportList,
|
||||||
|
StuckProblem,
|
||||||
|
AdminContestList,
|
||||||
|
AdminUser,
|
||||||
|
AdminUserList,
|
||||||
|
DashboardInfo,
|
||||||
|
JudgeServerList,
|
||||||
|
OrphanTestCase,
|
||||||
AdminProblem,
|
AdminProblem,
|
||||||
|
AdminProblemList,
|
||||||
AdminTag,
|
AdminTag,
|
||||||
Announcement,
|
Announcement,
|
||||||
AnnouncementEdit,
|
AnnouncementEdit,
|
||||||
|
AnnouncementListItem,
|
||||||
BlankContest,
|
BlankContest,
|
||||||
BlankProblem,
|
BlankProblem,
|
||||||
Contest,
|
Contest,
|
||||||
Exercise,
|
Exercise,
|
||||||
ExerciseType,
|
ExerciseType,
|
||||||
Server,
|
|
||||||
SQLDisplay,
|
SQLDisplay,
|
||||||
TestcaseUploadedReturns,
|
TestcaseUploadedReturns,
|
||||||
Tutorial,
|
Tutorial,
|
||||||
User,
|
User,
|
||||||
WebsiteConfig,
|
WebsiteConfig,
|
||||||
|
ProblemSet,
|
||||||
|
ProblemSetBadge,
|
||||||
|
ProblemSetList,
|
||||||
|
ProblemSetProblem,
|
||||||
|
ProblemSetProgressList,
|
||||||
|
TutorialListItem,
|
||||||
} from "utils/types"
|
} from "utils/types"
|
||||||
|
|
||||||
export function getBaseInfo() {
|
export function getBaseInfo() {
|
||||||
return legacyResponse(api2.get("admin/dashboard"))
|
return api2.get<DashboardInfo>("admin/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function randomUser10(classroom: string) {
|
export function randomUser10(classroom: string) {
|
||||||
return legacyResponse(
|
return api2.get<string[]>("admin/random-usernames", {
|
||||||
api2.get("admin/random-usernames", { params: { classroom } }),
|
params: { classroom },
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProblemList(
|
export async function getProblemList(
|
||||||
@@ -40,10 +61,9 @@ export async function getProblemList(
|
|||||||
const endpoint = contestID
|
const endpoint = contestID
|
||||||
? `admin/contests/${contestID}/problems`
|
? `admin/contests/${contestID}/problems`
|
||||||
: "admin/problems"
|
: "admin/problems"
|
||||||
const res = await legacyResponse<{
|
const res = await api2.get<AdminProblemList>(endpoint, {
|
||||||
results: AdminProblem[]
|
params: { offset, limit, keyword, author, tagId },
|
||||||
total: number
|
})
|
||||||
}>(api2.get(endpoint, { params: { offset, limit, keyword, author, tagId } }))
|
|
||||||
return {
|
return {
|
||||||
results: res.data.results.map(toProblemListItem),
|
results: res.data.results.map(toProblemListItem),
|
||||||
total: res.data.total,
|
total: res.data.total,
|
||||||
@@ -60,53 +80,46 @@ export function deleteContestProblem(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function editProblem(problem: AdminProblem | BlankProblem) {
|
export function editProblem(problem: AdminProblem | BlankProblem) {
|
||||||
return legacyResponse(
|
return api2.put<AdminProblem>(
|
||||||
api2.put(
|
`admin/problems/${(problem as AdminProblem).id}`,
|
||||||
`admin/problems/${(problem as AdminProblem).id}`,
|
toProblemBody(problem),
|
||||||
toProblemBody(problem),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleProblemVisible(problemID: number) {
|
export function toggleProblemVisible(problemID: number) {
|
||||||
return legacyResponse(api2.put(`admin/problems/${problemID}/visibility`))
|
return api2.put<{ visible: boolean }>(
|
||||||
|
`admin/problems/${problemID}/visibility`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateFlowchartFromPythonCode(python: string) {
|
export function generateFlowchartFromPythonCode(python: string) {
|
||||||
return legacyResponse(api2.post("admin/problems/flowchart", { python }))
|
return api2.post<{ flowchart: string }>("admin/problems/flowchart", {
|
||||||
|
python,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editContestProblem(problem: AdminProblem | BlankProblem) {
|
export function editContestProblem(problem: AdminProblem | BlankProblem) {
|
||||||
return legacyResponse(
|
return api2.put<AdminProblem>(
|
||||||
api2.put(
|
`admin/problems/${(problem as AdminProblem).id}`,
|
||||||
`admin/problems/${(problem as AdminProblem).id}`,
|
toProblemBody(problem),
|
||||||
toProblemBody(problem),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProblem(id: string | number) {
|
export function getProblem(id: string | number) {
|
||||||
return legacyResponse<AdminProblem>(api2.get(`admin/problems/${id}`))
|
return api2.get<AdminProblem>(`admin/problems/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContestProblem(id: number) {
|
export function getContestProblem(id: number) {
|
||||||
return legacyResponse<AdminProblem>(api2.get(`admin/problems/${id}`))
|
return api2.get<AdminProblem>(`admin/problems/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标签管理
|
// 标签管理
|
||||||
export function getTagAdminList(keyword = "") {
|
export function getTagAdminList(keyword = "") {
|
||||||
return legacyResponse<AdminTag[]>(
|
return api2.get<AdminTag[]>("admin/problem-tags", { params: { keyword } })
|
||||||
api2.get("admin/problem-tags", { params: { keyword } }),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renameTag(id: number, name: string) {
|
export function renameTag(id: number, name: string) {
|
||||||
return legacyResponse<{
|
return api2.put<RenameTagResponse>(`admin/problem-tags/${id}`, { name })
|
||||||
merged: boolean
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
affected_count: number
|
|
||||||
}>(api2.put(`admin/problem-tags/${id}`, { name }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTag(id: number) {
|
export function deleteTag(id: number) {
|
||||||
@@ -118,13 +131,11 @@ export function batchTagProblems(
|
|||||||
tagNames: string[],
|
tagNames: string[],
|
||||||
action: "add" | "remove",
|
action: "add" | "remove",
|
||||||
) {
|
) {
|
||||||
return legacyResponse<{ problem_count: number; tag_count: number }>(
|
return api2.post<BatchProblemTagResponse>("admin/problems/batch-tag", {
|
||||||
api2.post("admin/problems/batch-tag", {
|
problemIds,
|
||||||
problemIds,
|
tagNames,
|
||||||
tagNames,
|
action,
|
||||||
action,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户列表
|
// 用户列表
|
||||||
@@ -135,34 +146,30 @@ export function getUserList(
|
|||||||
keyword: string,
|
keyword: string,
|
||||||
orderBy = "",
|
orderBy = "",
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.get<AdminUserList>("admin/users", {
|
||||||
api2.get("admin/users", {
|
// 旧接口的 order_by 只有 "-last_login" 一个取值
|
||||||
// 旧接口的 order_by 只有 "-last_login" 一个取值
|
params: {
|
||||||
params: {
|
offset,
|
||||||
offset,
|
limit,
|
||||||
limit,
|
keyword,
|
||||||
keyword,
|
type,
|
||||||
type,
|
orderBy: orderBy === "-last_login" ? "-lastLogin" : orderBy,
|
||||||
orderBy: orderBy === "-last_login" ? "-lastLogin" : orderBy,
|
},
|
||||||
},
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑用户
|
// 编辑用户
|
||||||
export function editUser(user: User) {
|
export function editUser(user: User) {
|
||||||
return legacyResponse(
|
return api2.put<AdminUser>(`admin/users/${user.id}`, {
|
||||||
api2.put(`admin/users/${user.id}`, {
|
username: user.username,
|
||||||
username: user.username,
|
email: user.email,
|
||||||
email: user.email,
|
adminType: user.adminType,
|
||||||
adminType: user.admin_type,
|
problemPermission: user.problemPermission,
|
||||||
problemPermission: user.problem_permission,
|
realName: user.realName ?? null,
|
||||||
realName: user.real_name ?? null,
|
isDisabled: user.isDisabled,
|
||||||
isDisabled: user.is_disabled,
|
openApi: user.openApi,
|
||||||
openApi: user.open_api,
|
password: user.password ?? "",
|
||||||
password: user.password ?? "",
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置用户密码。调用方直接用 res.data 当密码字符串(旧后端返回的就是裸字符串),
|
// 重置用户密码。调用方直接用 res.data 当密码字符串(旧后端返回的就是裸字符串),
|
||||||
@@ -185,9 +192,9 @@ export function deleteUsers(userIDs: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getContestList(offset = 0, limit = 10, keyword: string) {
|
export function getContestList(offset = 0, limit = 10, keyword: string) {
|
||||||
return legacyResponse(
|
return api2.get<AdminContestList>("admin/contests", {
|
||||||
api2.get("admin/contests", { params: { offset, limit, keyword } }),
|
params: { offset, limit, keyword },
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传图片
|
// 上传图片
|
||||||
@@ -218,53 +225,45 @@ export function uploadTestcases(file: File, options: { sql?: boolean } = {}) {
|
|||||||
|
|
||||||
// SQL 题测试点预览:后端跑一遍初始化脚本+标准答案,返回数据表和期望结果展示数据
|
// SQL 题测试点预览:后端跑一遍初始化脚本+标准答案,返回数据表和期望结果展示数据
|
||||||
export function previewSQLTestcase(data: {
|
export function previewSQLTestcase(data: {
|
||||||
init_sql: string
|
initSql: string
|
||||||
ref_sql: string
|
refSql: string
|
||||||
mode: "query" | "modify"
|
mode: "query" | "modify"
|
||||||
}) {
|
}) {
|
||||||
return legacyResponse<SQLDisplay>(
|
return api2.post<SQLDisplay>("admin/sql-test-cases/preview", data)
|
||||||
api2.post("admin/sql-test-cases/preview", {
|
|
||||||
initSql: data.init_sql,
|
|
||||||
refSql: data.ref_sql,
|
|
||||||
mode: data.mode,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序)
|
// 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序)
|
||||||
export function getSQLTestcaseScripts(problemId: number) {
|
export function getSQLTestcaseScripts(problemId: number) {
|
||||||
return legacyResponse<{ name: string; content: string }[]>(
|
return api2.get<SqlTestCaseScript[]>(
|
||||||
api2.get(`admin/problems/${problemId}/sql-scripts`),
|
`admin/problems/${problemId}/sql-scripts`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI 根据标准答案生成一个 SQL 测试点初始化脚本
|
// AI 根据标准答案生成一个 SQL 测试点初始化脚本
|
||||||
export function generateSQLTestcase(data: {
|
export function generateSQLTestcase(data: {
|
||||||
ref_sql: string
|
refSql: string
|
||||||
mode: "query" | "modify"
|
mode: "query" | "modify"
|
||||||
}) {
|
}) {
|
||||||
return legacyResponse<{ sql: string }>(
|
return api2.post<GenerateSqlTestCaseResponse>(
|
||||||
api2.post("admin/sql-test-cases/generate", {
|
"admin/sql-test-cases/generate",
|
||||||
refSql: data.ref_sql,
|
data,
|
||||||
mode: data.mode,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 组件里的题目对象是 snake_case,出站转成新后端要的 camelCase */
|
/** 出站补默认值。字段名两边已经一致,不再做键名转换 */
|
||||||
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
||||||
const p = problem as Record<string, any>
|
const p = problem as Partial<AdminProblem>
|
||||||
return {
|
return {
|
||||||
_id: p._id,
|
_id: p._id,
|
||||||
title: p.title,
|
title: p.title,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
inputDescription: p.input_description ?? "",
|
inputDescription: p.inputDescription ?? "",
|
||||||
outputDescription: p.output_description ?? "",
|
outputDescription: p.outputDescription ?? "",
|
||||||
samples: p.samples ?? [],
|
samples: p.samples ?? [],
|
||||||
testCaseId: p.test_case_id,
|
testCaseId: p.testCaseId,
|
||||||
testCaseScore: p.test_case_score ?? [],
|
testCaseScore: p.testCaseScore ?? [],
|
||||||
timeLimit: p.time_limit,
|
timeLimit: p.timeLimit,
|
||||||
memoryLimit: p.memory_limit,
|
memoryLimit: p.memoryLimit,
|
||||||
languages: p.languages ?? [],
|
languages: p.languages ?? [],
|
||||||
template: p.template ?? {},
|
template: p.template ?? {},
|
||||||
visible: p.visible,
|
visible: p.visible,
|
||||||
@@ -274,25 +273,25 @@ function toProblemBody(problem: AdminProblem | BlankProblem) {
|
|||||||
source: p.source ?? null,
|
source: p.source ?? null,
|
||||||
prompt: p.prompt ?? null,
|
prompt: p.prompt ?? null,
|
||||||
answers: p.answers ?? [],
|
answers: p.answers ?? [],
|
||||||
shareSubmission: p.share_submission ?? false,
|
shareSubmission: p.shareSubmission ?? false,
|
||||||
allowFlowchart: p.allow_flowchart ?? false,
|
allowFlowchart: p.allowFlowchart ?? false,
|
||||||
showFlowchart: p.show_flowchart ?? false,
|
showFlowchart: p.showFlowchart ?? false,
|
||||||
mermaidCode: p.mermaid_code ?? null,
|
mermaidCode: p.mermaidCode ?? null,
|
||||||
flowchartHint: p.flowchart_hint ?? null,
|
flowchartHint: p.flowchartHint ?? null,
|
||||||
astRules: p.ast_rules ?? null,
|
astRules: p.astRules ?? null,
|
||||||
sqlConfig: p.sql_config ?? null,
|
sqlConfig: p.sqlConfig ?? null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProblem(problem: BlankProblem) {
|
export function createProblem(problem: BlankProblem) {
|
||||||
return legacyResponse(api2.post("admin/problems", toProblemBody(problem)))
|
return api2.post<AdminProblem>("admin/problems", toProblemBody(problem))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createContestProblem(problem: BlankProblem) {
|
export function createContestProblem(problem: BlankProblem) {
|
||||||
// contest_id 由 detail.vue 在提交前写进 problem 对象
|
// contestId 由 detail.vue 在提交前写进 problem 对象
|
||||||
const contestID = (problem as Record<string, any>).contest_id
|
return api2.post<AdminProblem>(
|
||||||
return legacyResponse(
|
`admin/contests/${problem.contestId}/problems`,
|
||||||
api2.post(`admin/contests/${contestID}/problems`, toProblemBody(problem)),
|
toProblemBody(problem),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,35 +301,31 @@ function toContestBody(contest: Contest | BlankContest) {
|
|||||||
title: contest.title,
|
title: contest.title,
|
||||||
description: contest.description,
|
description: contest.description,
|
||||||
tag: contest.tag,
|
tag: contest.tag,
|
||||||
startTime: contest.start_time,
|
startTime: contest.startTime,
|
||||||
endTime: contest.end_time,
|
endTime: contest.endTime,
|
||||||
password: contest.password || null,
|
password: contest.password || null,
|
||||||
visible: contest.visible,
|
visible: contest.visible,
|
||||||
allowedIpRanges: contest.allowed_ip_ranges ?? [],
|
allowedIpRanges: contest.allowedIpRanges ?? [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createContest(contest: BlankContest) {
|
export function createContest(contest: BlankContest) {
|
||||||
return legacyResponse(api2.post("admin/contests", toContestBody(contest)))
|
return api2.post<Contest>("admin/contests", toContestBody(contest))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editContest(contest: Contest | BlankContest) {
|
export function editContest(contest: Contest | BlankContest) {
|
||||||
return legacyResponse(
|
return api2.put<Contest>(
|
||||||
api2.put(
|
`admin/contests/${(contest as Contest).id}`,
|
||||||
`admin/contests/${(contest as Contest).id}`,
|
toContestBody(contest),
|
||||||
toContestBody(contest),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloneContest(contest_id: number) {
|
export function cloneContest(contestId: number) {
|
||||||
return legacyResponse(api2.post(`admin/contests/${contest_id}/clone`))
|
return api2.post<Contest>(`admin/contests/${contestId}/clone`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContest(id: string) {
|
export function getContest(id: string) {
|
||||||
return legacyResponse<Contest & { password: string }>(
|
return api2.get<Contest>(`admin/contests/${id}`)
|
||||||
api2.get(`admin/contests/${id}`),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addProblemForContest(
|
export function addProblemForContest(
|
||||||
@@ -338,33 +333,22 @@ export function addProblemForContest(
|
|||||||
problemID: number,
|
problemID: number,
|
||||||
displayID: string,
|
displayID: string,
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.post<AdminProblem>(
|
||||||
api2.post(`admin/contests/${contestID}/problems/from-public`, {
|
`admin/contests/${contestID}/problems/from-public`,
|
||||||
problemId: problemID,
|
{ problemId: problemID, displayId: displayID },
|
||||||
displayId: displayID,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebsite() {
|
export function getWebsite() {
|
||||||
return legacyResponse<WebsiteConfig>(api2.get("admin/website"))
|
return api2.get<WebsiteConfig>("admin/website")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editWebsite(data: WebsiteConfig) {
|
export function editWebsite(data: WebsiteConfig) {
|
||||||
return api2.post("admin/website", {
|
return api2.post<WebsiteConfig>("admin/website", data)
|
||||||
websiteBaseUrl: data.website_base_url,
|
|
||||||
websiteName: data.website_name,
|
|
||||||
websiteNameShortcut: data.website_name_shortcut,
|
|
||||||
websiteFooter: data.website_footer,
|
|
||||||
allowRegister: data.allow_register,
|
|
||||||
submissionListShowAll: data.submission_list_show_all,
|
|
||||||
classList: data.class_list,
|
|
||||||
enableMaxkb: data.enable_maxkb,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listInvalidTestcases() {
|
export function listInvalidTestcases() {
|
||||||
return legacyResponse(api2.get("admin/orphan-test-cases"))
|
return api2.get<OrphanTestCase[]>("admin/orphan-test-cases")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pruneInvalidTestcases(id?: string) {
|
export function pruneInvalidTestcases(id?: string) {
|
||||||
@@ -372,9 +356,7 @@ export function pruneInvalidTestcases(id?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getJudgeServer() {
|
export function getJudgeServer() {
|
||||||
return legacyResponse<{ token: string; servers: Server[] }>(
|
return api2.get<JudgeServerList>("admin/judge-servers")
|
||||||
api2.get("admin/judge-servers"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteJudgeServer(hostname: string) {
|
export function deleteJudgeServer(hostname: string) {
|
||||||
@@ -382,15 +364,14 @@ export function deleteJudgeServer(hostname: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAnnouncementList(offset = 0, limit = 10) {
|
export function getAnnouncementList(offset = 0, limit = 10) {
|
||||||
return legacyResponse(
|
return api2.get<{ results: AnnouncementListItem[]; total: number }>(
|
||||||
api2.get("admin/announcements", {
|
"admin/announcements",
|
||||||
params: { offset, limit },
|
{ params: { offset, limit } },
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAnnouncement(id: number) {
|
export function getAnnouncement(id: number) {
|
||||||
return legacyResponse<Announcement>(api2.get(`admin/announcements/${id}`))
|
return api2.get<Announcement>(`admin/announcements/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteAnnouncement(id: number) {
|
export function deleteAnnouncement(id: number) {
|
||||||
@@ -399,48 +380,46 @@ export function deleteAnnouncement(id: number) {
|
|||||||
|
|
||||||
export function editAnnouncement(announcement: AnnouncementEdit) {
|
export function editAnnouncement(announcement: AnnouncementEdit) {
|
||||||
const { id, ...body } = announcement
|
const { id, ...body } = announcement
|
||||||
return legacyResponse(api2.put(`admin/announcements/${id}`, body))
|
return api2.put<Announcement>(`admin/announcements/${id}`, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAnnouncement(announcement: AnnouncementEdit) {
|
export function createAnnouncement(announcement: AnnouncementEdit) {
|
||||||
const { id: _id, ...body } = announcement
|
const { id: _id, ...body } = announcement
|
||||||
return legacyResponse(api2.post("admin/announcements", body))
|
return api2.post<Announcement>("admin/announcements", body)
|
||||||
}
|
|
||||||
|
|
||||||
/** 组件里的 Tutorial 仍是 snake_case,出站时转成新后端要的 camelCase */
|
|
||||||
function toTutorialBody(data: Partial<Tutorial>) {
|
|
||||||
return {
|
|
||||||
title: data.title,
|
|
||||||
content: data.content,
|
|
||||||
code: data.code ?? null,
|
|
||||||
isPublic: data.is_public ?? false,
|
|
||||||
order: data.order ?? 0,
|
|
||||||
type: data.type,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTutorialList() {
|
export async function getTutorialList() {
|
||||||
const res = await legacyResponse<{ [key: string]: Tutorial[] }>(
|
const res = await api2.get<{ [key: string]: TutorialListItem[] }>(
|
||||||
api2.get("admin/tutorials"),
|
"admin/tutorials",
|
||||||
)
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTutorial(id: number) {
|
export async function getTutorial(id: number) {
|
||||||
const res = await legacyResponse<Tutorial>(api2.get(`admin/tutorials/${id}`))
|
const res = await api2.get<Tutorial>(`admin/tutorials/${id}`)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toTutorialBody(data: Partial<Tutorial>) {
|
||||||
|
return {
|
||||||
|
title: data.title,
|
||||||
|
content: data.content,
|
||||||
|
code: data.code ?? null,
|
||||||
|
isPublic: data.isPublic ?? false,
|
||||||
|
order: data.order ?? 0,
|
||||||
|
type: data.type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createTutorial(data: Partial<Tutorial>) {
|
export async function createTutorial(data: Partial<Tutorial>) {
|
||||||
const res = await legacyResponse<Tutorial>(
|
const res = await api2.post<Tutorial>("admin/tutorials", toTutorialBody(data))
|
||||||
api2.post("admin/tutorials", toTutorialBody(data)),
|
|
||||||
)
|
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateTutorial(data: Partial<Tutorial>) {
|
export async function updateTutorial(data: Partial<Tutorial>) {
|
||||||
const res = await legacyResponse<Tutorial>(
|
const res = await api2.put<Tutorial>(
|
||||||
api2.put(`admin/tutorials/${data.id}`, toTutorialBody(data)),
|
`admin/tutorials/${data.id}`,
|
||||||
|
toTutorialBody(data),
|
||||||
)
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
@@ -449,33 +428,24 @@ export function deleteTutorial(id: number) {
|
|||||||
return api2.delete(`admin/tutorials/${id}`)
|
return api2.delete(`admin/tutorials/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTutorialVisibility(id: number, is_public: boolean) {
|
export function setTutorialVisibility(id: number, isPublic: boolean) {
|
||||||
return legacyResponse(
|
return api2.put<Tutorial>(`admin/tutorials/${id}/visibility`, { isPublic })
|
||||||
api2.put(`admin/tutorials/${id}/visibility`, { isPublic: is_public }),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminExercises(tutorialId: number) {
|
export async function getAdminExercises(tutorialId: number) {
|
||||||
const res = await legacyResponse<Exercise[]>(
|
const res = await api2.get<Exercise[]>(
|
||||||
api2.get(`admin/tutorials/${tutorialId}/exercises`),
|
`admin/tutorials/${tutorialId}/exercises`,
|
||||||
)
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createExercise(data: {
|
export async function createExercise(data: {
|
||||||
tutorial_id: number
|
tutorialId: number
|
||||||
type: ExerciseType
|
type: ExerciseType
|
||||||
data: object
|
data: object
|
||||||
order: number
|
order: number
|
||||||
}) {
|
}) {
|
||||||
const res = await legacyResponse<Exercise>(
|
const res = await api2.post<Exercise>("admin/exercises", data)
|
||||||
api2.post("admin/exercises", {
|
|
||||||
tutorialId: data.tutorial_id,
|
|
||||||
type: data.type,
|
|
||||||
data: data.data,
|
|
||||||
order: data.order,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,13 +455,11 @@ export async function updateExercise(data: {
|
|||||||
data: object
|
data: object
|
||||||
order: number
|
order: number
|
||||||
}) {
|
}) {
|
||||||
const res = await legacyResponse<Exercise>(
|
const res = await api2.put<Exercise>(`admin/exercises/${data.id}`, {
|
||||||
api2.put(`admin/exercises/${data.id}`, {
|
type: data.type,
|
||||||
type: data.type,
|
data: data.data,
|
||||||
data: data.data,
|
order: data.order,
|
||||||
order: data.order,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,15 +468,15 @@ export function deleteExercise(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 将竞赛题目转为公开题目
|
// 将竞赛题目转为公开题目
|
||||||
export function makeProblemPublic(id: number, display_id: string) {
|
export function makeProblemPublic(id: number, displayId: string) {
|
||||||
return legacyResponse(
|
return api2.post<AdminProblem>(`admin/problems/${id}/make-public`, {
|
||||||
api2.post(`admin/problems/${id}/make-public`, { displayId: display_id }),
|
displayId,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 比赛辅助检查
|
// 比赛辅助检查
|
||||||
export function getACMHelperList(contest_id: number) {
|
export function getACMHelperList(contestId: number) {
|
||||||
return legacyResponse(api2.get(`admin/contests/${contest_id}/acm-helper`))
|
return api2.get<AcmHelperItem[]>(`admin/contests/${contestId}/acm-helper`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateACMHelperChecked(
|
export function updateACMHelperChecked(
|
||||||
@@ -532,57 +500,44 @@ export function getProblemSetList(
|
|||||||
difficulty = "",
|
difficulty = "",
|
||||||
status = "",
|
status = "",
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.get<ProblemSetList>("admin/problem-sets", {
|
||||||
api2.get("admin/problem-sets", {
|
params: { offset, limit, keyword, difficulty, status },
|
||||||
params: { offset, limit, keyword, difficulty, status },
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProblemSetDetail(id: number) {
|
export function getProblemSetDetail(id: number) {
|
||||||
return legacyResponse(api2.get(`admin/problem-sets/${id}`))
|
return api2.get<ProblemSet>(`admin/problem-sets/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 组件传的是 snake_case,出站转成新后端要的 camelCase */
|
interface ProblemSetBody {
|
||||||
function toProblemSetBody(data: {
|
|
||||||
title?: string
|
title?: string
|
||||||
description?: string
|
description?: string
|
||||||
difficulty?: string
|
difficulty?: ProblemSet["difficulty"]
|
||||||
status?: string
|
status?: ProblemSet["status"]
|
||||||
end_time?: Date | null
|
// 表单里是 Date,出站要 ISO 串
|
||||||
|
endTime?: Date | null
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
function toProblemSetBody(data: ProblemSetBody) {
|
||||||
return {
|
return {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description ?? "",
|
description: data.description ?? "",
|
||||||
difficulty: data.difficulty ?? "Easy",
|
difficulty: data.difficulty ?? "Easy",
|
||||||
status: data.status ?? "active",
|
status: data.status ?? "active",
|
||||||
endTime: data.end_time ? new Date(data.end_time).toISOString() : null,
|
endTime: data.endTime ? new Date(data.endTime).toISOString() : null,
|
||||||
visible: data.visible ?? true,
|
visible: data.visible ?? true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProblemSet(data: {
|
export function createProblemSet(data: ProblemSetBody) {
|
||||||
title: string
|
return api2.post<ProblemSet>("admin/problem-sets", toProblemSetBody(data))
|
||||||
description: string
|
|
||||||
difficulty: string
|
|
||||||
status: string
|
|
||||||
end_time?: Date | null
|
|
||||||
}) {
|
|
||||||
return legacyResponse(api2.post("admin/problem-sets", toProblemSetBody(data)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editProblemSet(data: {
|
export function editProblemSet(data: ProblemSetBody & { id: number }) {
|
||||||
id: number
|
return api2.put<ProblemSet>(
|
||||||
title?: string
|
`admin/problem-sets/${data.id}`,
|
||||||
description?: string
|
toProblemSetBody(data),
|
||||||
difficulty?: string
|
|
||||||
status?: string
|
|
||||||
end_time?: Date | null
|
|
||||||
visible?: boolean
|
|
||||||
}) {
|
|
||||||
return legacyResponse(
|
|
||||||
api2.put(`admin/problem-sets/${data.id}`, toProblemSetBody(data)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,32 +546,34 @@ export function deleteProblemSet(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toggleProblemSetVisible(id: number) {
|
export function toggleProblemSetVisible(id: number) {
|
||||||
return legacyResponse(api2.put(`admin/problem-sets/${id}/visibility`))
|
return api2.put<ProblemSet>(`admin/problem-sets/${id}/visibility`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateProblemSetStatus(id: number, status: string) {
|
export function updateProblemSetStatus(id: number, status: string) {
|
||||||
return legacyResponse(api2.put(`admin/problem-sets/${id}/status`, { status }))
|
return api2.put<ProblemSet>(`admin/problem-sets/${id}/status`, { status })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 题单题目管理 API
|
// 题单题目管理 API
|
||||||
export function getProblemSetProblems(problemSetId: number) {
|
export function getProblemSetProblems(problemSetId: number) {
|
||||||
return legacyResponse(api2.get(`admin/problem-sets/${problemSetId}/problems`))
|
return api2.get<ProblemSetProblem[]>(
|
||||||
|
`admin/problem-sets/${problemSetId}/problems`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addProblemToSet(
|
export function addProblemToSet(
|
||||||
problemSetId: number,
|
problemSetId: number,
|
||||||
data: {
|
data: {
|
||||||
problem_id: string
|
problemId: string
|
||||||
order?: number
|
order?: number
|
||||||
is_required?: boolean
|
isRequired?: boolean
|
||||||
score?: number
|
score?: number
|
||||||
hint?: string
|
hint?: string
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return api2.post(`admin/problem-sets/${problemSetId}/problems`, {
|
return api2.post(`admin/problem-sets/${problemSetId}/problems`, {
|
||||||
problemId: data.problem_id,
|
problemId: data.problemId,
|
||||||
order: data.order ?? 0,
|
order: data.order ?? 0,
|
||||||
isRequired: data.is_required ?? true,
|
isRequired: data.isRequired ?? true,
|
||||||
score: data.score ?? 0,
|
score: data.score ?? 0,
|
||||||
hint: data.hint ?? "",
|
hint: data.hint ?? "",
|
||||||
})
|
})
|
||||||
@@ -627,19 +584,14 @@ export function editProblemInSet(
|
|||||||
problemSetProblemId: number,
|
problemSetProblemId: number,
|
||||||
data: {
|
data: {
|
||||||
order?: number
|
order?: number
|
||||||
is_required?: boolean
|
isRequired?: boolean
|
||||||
score?: number
|
score?: number
|
||||||
hint?: string
|
hint?: string
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return api2.put(
|
return api2.put(
|
||||||
`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`,
|
`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`,
|
||||||
{
|
data,
|
||||||
order: data.order,
|
|
||||||
isRequired: data.is_required,
|
|
||||||
score: data.score,
|
|
||||||
hint: data.hint,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,58 +606,44 @@ export function removeProblemFromSet(
|
|||||||
|
|
||||||
// 题单奖章管理 API
|
// 题单奖章管理 API
|
||||||
export function getProblemSetBadges(problemSetId: number) {
|
export function getProblemSetBadges(problemSetId: number) {
|
||||||
return legacyResponse(api2.get(`admin/problem-sets/${problemSetId}/badges`))
|
return api2.get<ProblemSetBadge[]>(
|
||||||
|
`admin/problem-sets/${problemSetId}/badges`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBadgeBody(data: {
|
interface BadgeBody {
|
||||||
name?: string
|
name?: string
|
||||||
description?: string
|
description?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
condition_type?: string
|
conditionType?: ProblemSetBadge["conditionType"]
|
||||||
condition_value?: number
|
conditionValue?: number
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
function toBadgeBody(data: BadgeBody) {
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description ?? "",
|
description: data.description ?? "",
|
||||||
icon: data.icon ?? "",
|
icon: data.icon ?? "",
|
||||||
conditionType: data.condition_type,
|
conditionType: data.conditionType,
|
||||||
conditionValue: data.condition_value ?? 0,
|
conditionValue: data.conditionValue ?? 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProblemSetBadge(
|
export function createProblemSetBadge(problemSetId: number, data: BadgeBody) {
|
||||||
problemSetId: number,
|
return api2.post<ProblemSetBadge>(
|
||||||
data: {
|
`admin/problem-sets/${problemSetId}/badges`,
|
||||||
name: string
|
toBadgeBody(data),
|
||||||
description: string
|
|
||||||
icon: string
|
|
||||||
condition_type: string
|
|
||||||
condition_value: number
|
|
||||||
level?: number
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return legacyResponse(
|
|
||||||
api2.post(`admin/problem-sets/${problemSetId}/badges`, toBadgeBody(data)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editProblemSetBadge(
|
export function editProblemSetBadge(
|
||||||
problemSetId: number,
|
problemSetId: number,
|
||||||
badgeId: number,
|
badgeId: number,
|
||||||
data: {
|
data: BadgeBody,
|
||||||
name?: string
|
|
||||||
description?: string
|
|
||||||
icon?: string
|
|
||||||
condition_type?: string
|
|
||||||
condition_value?: number
|
|
||||||
level?: number
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.put<ProblemSetBadge>(
|
||||||
api2.put(
|
`admin/problem-sets/${problemSetId}/badges/${badgeId}`,
|
||||||
`admin/problem-sets/${problemSetId}/badges/${badgeId}`,
|
toBadgeBody(data),
|
||||||
toBadgeBody(data),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,7 +653,9 @@ export function deleteProblemSetBadge(problemSetId: number, badgeId: number) {
|
|||||||
|
|
||||||
// 题单进度管理 API
|
// 题单进度管理 API
|
||||||
export function getProblemSetProgress(problemSetId: number) {
|
export function getProblemSetProgress(problemSetId: number) {
|
||||||
return legacyResponse(api2.get(`admin/problem-sets/${problemSetId}/progress`))
|
return api2.get<ProblemSetProgressList>(
|
||||||
|
`admin/problem-sets/${problemSetId}/progress`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
||||||
@@ -724,74 +664,46 @@ export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
|||||||
|
|
||||||
// 学生卡点分析
|
// 学生卡点分析
|
||||||
export function getStuckProblems() {
|
export function getStuckProblems() {
|
||||||
return legacyResponse(api2.get("admin/problem-analytics/stuck"))
|
return api2.get<StuckProblem[]>("admin/problem-analytics/stuck")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTopACTrend(params: {
|
export function getTopACTrend(params: {
|
||||||
since_year: number
|
sinceYear: number
|
||||||
until_year: number
|
untilYear: number
|
||||||
min_per_year: number
|
minPerYear: number
|
||||||
}) {
|
}) {
|
||||||
return legacyResponse(
|
return api2.get<AcTrend[]>("admin/problem-analytics/ac-trend", { params })
|
||||||
api2.get("admin/problem-analytics/ac-trend", {
|
|
||||||
params: {
|
|
||||||
sinceYear: params.since_year,
|
|
||||||
untilYear: params.until_year,
|
|
||||||
minPerYear: params.min_per_year,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI 学习分析报告
|
// AI 学习分析报告
|
||||||
export function getAIReportList(offset = 0, limit = 10, username = "") {
|
export function getAIReportList(offset = 0, limit = 10, username = "") {
|
||||||
return legacyResponse(
|
return api2.get<AdminAiReportList>("admin/ai/reports", {
|
||||||
api2.get("admin/ai/reports", {
|
params: { offset, limit, username: username || undefined },
|
||||||
params: { offset, limit, username: username || undefined },
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAIReportDetail(id: number) {
|
export function getAIReportDetail(id: number) {
|
||||||
return legacyResponse(api2.get(`admin/ai/reports/${id}`))
|
return api2.get<AdminAiReport>(`admin/ai/reports/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pinAIReport(id: number) {
|
export function pinAIReport(id: number) {
|
||||||
return legacyResponse(api2.post(`admin/ai/reports/${id}/pin`))
|
return api2.post<{ isPinned: boolean }>(`admin/ai/reports/${id}/pin`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPinnedAIReports() {
|
export function getPinnedAIReports() {
|
||||||
return legacyResponse(
|
return api2.get<AdminAiReportList>("admin/ai/reports", {
|
||||||
api2.get("admin/ai/reports", { params: { pinnedOnly: "true" } }),
|
params: { pinnedOnly: "true" },
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 成就 ====================
|
// ==================== 成就 ====================
|
||||||
|
|
||||||
export interface AdminAchievement {
|
import type {
|
||||||
id: number
|
AdminAchievement,
|
||||||
name: string
|
AchievementMetric as MetricOption,
|
||||||
description: string
|
} from "@oj2/contract"
|
||||||
icon: string
|
export type { AdminAchievement, MetricOption }
|
||||||
rarity: string
|
|
||||||
hidden: boolean
|
|
||||||
metric: string
|
|
||||||
metric_name: string
|
|
||||||
operator: "gte" | "lte"
|
|
||||||
threshold: number
|
|
||||||
visible: boolean
|
|
||||||
unlock_count: number
|
|
||||||
order: number
|
|
||||||
create_time: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MetricOption {
|
|
||||||
key: string
|
|
||||||
name: string
|
|
||||||
help_text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 组件里的成就对象是 snake_case,出站转成新后端要的 camelCase */
|
|
||||||
function toAchievementBody(data: Partial<AdminAchievement>) {
|
function toAchievementBody(data: Partial<AdminAchievement>) {
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -808,22 +720,24 @@ function toAchievementBody(data: Partial<AdminAchievement>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAdminAchievements() {
|
export function getAdminAchievements() {
|
||||||
return legacyResponse<AdminAchievement[]>(api2.get("admin/achievements"))
|
return api2.get<AdminAchievement[]>("admin/achievements")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetricOptions() {
|
export function getMetricOptions() {
|
||||||
return legacyResponse<MetricOption[]>(api2.get("admin/achievement-metrics"))
|
return api2.get<MetricOption[]>("admin/achievement-metrics")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAchievement(data: Partial<AdminAchievement>) {
|
export function createAchievement(data: Partial<AdminAchievement>) {
|
||||||
return legacyResponse<AdminAchievement>(
|
return api2.post<AdminAchievement>(
|
||||||
api2.post("admin/achievements", toAchievementBody(data)),
|
"admin/achievements",
|
||||||
|
toAchievementBody(data),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAchievement(data: Partial<AdminAchievement>) {
|
export function updateAchievement(data: Partial<AdminAchievement>) {
|
||||||
return legacyResponse<AdminAchievement>(
|
return api2.put<AdminAchievement>(
|
||||||
api2.put(`admin/achievements/${data.id}`, toAchievementBody(data)),
|
`admin/achievements/${data.id}`,
|
||||||
|
toAchievementBody(data),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const durationMins = ref(10) // 比赛默认时长10分钟
|
|||||||
|
|
||||||
watch([waitMins, durationMins], () => {
|
watch([waitMins, durationMins], () => {
|
||||||
const times = getTimes()
|
const times = getTimes()
|
||||||
contest.start_time = formatISO(times[0])
|
contest.startTime = formatISO(times[0])
|
||||||
contest.end_time = formatISO(times[1])
|
contest.endTime = formatISO(times[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
// 编辑的时候
|
// 编辑的时候
|
||||||
@@ -32,8 +32,8 @@ const startTime = ref(0)
|
|||||||
const endTime = ref(0)
|
const endTime = ref(0)
|
||||||
|
|
||||||
watch([startTime, endTime], (values) => {
|
watch([startTime, endTime], (values) => {
|
||||||
contest.start_time = formatISO(values[0])
|
contest.startTime = formatISO(values[0])
|
||||||
contest.end_time = formatISO(values[1])
|
contest.endTime = formatISO(values[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -54,18 +54,18 @@ const contest = reactive<BlankContest & { id: number }>({
|
|||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
tag: "练习",
|
tag: "练习",
|
||||||
start_time: "",
|
startTime: "",
|
||||||
end_time: "",
|
endTime: "",
|
||||||
password: "",
|
password: "",
|
||||||
visible: false,
|
visible: false,
|
||||||
allowed_ip_ranges: [],
|
allowedIpRanges: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getContestDetail() {
|
async function getContestDetail() {
|
||||||
if (!props.contestID) {
|
if (!props.contestID) {
|
||||||
const times = getTimes()
|
const times = getTimes()
|
||||||
contest.start_time = formatISO(times[0])
|
contest.startTime = formatISO(times[0])
|
||||||
contest.end_time = formatISO(times[1])
|
contest.endTime = formatISO(times[1])
|
||||||
toggleReady(true)
|
toggleReady(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -75,15 +75,15 @@ async function getContestDetail() {
|
|||||||
contest.title = data.title
|
contest.title = data.title
|
||||||
contest.description = data.description
|
contest.description = data.description
|
||||||
contest.tag = data.tag
|
contest.tag = data.tag
|
||||||
contest.start_time = data.start_time
|
contest.startTime = data.startTime
|
||||||
contest.end_time = data.end_time
|
contest.endTime = data.endTime
|
||||||
contest.password = data.password
|
contest.password = data.password
|
||||||
contest.visible = data.visible
|
contest.visible = data.visible
|
||||||
contest.allowed_ip_ranges = []
|
contest.allowedIpRanges = []
|
||||||
|
|
||||||
// 显示
|
// 显示
|
||||||
startTime.value = Date.parse(data.start_time)
|
startTime.value = Date.parse(data.startTime)
|
||||||
endTime.value = Date.parse(data.end_time)
|
endTime.value = Date.parse(data.endTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -118,12 +118,12 @@ onMounted(getContestDetail)
|
|||||||
<template v-if="!props.contestID">
|
<template v-if="!props.contestID">
|
||||||
<n-alert type="success">
|
<n-alert type="success">
|
||||||
<template #header>
|
<template #header>
|
||||||
开始时间 {{ parseTime(contest.start_time, "YYYY年M月D日 HH:mm:ss") }}
|
开始时间 {{ parseTime(contest.startTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||||
</template>
|
</template>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-alert type="warning">
|
<n-alert type="warning">
|
||||||
<template #header>
|
<template #header>
|
||||||
结束时间 {{ parseTime(contest.end_time, "YYYY年M月D日 HH:mm:ss") }}
|
结束时间 {{ parseTime(contest.endTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||||
</template>
|
</template>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AcmHelperItem, SubmissionInfo } from "utils/types"
|
||||||
import { NButton, NCheckbox, NSelect, NTag } from "naive-ui"
|
import { NButton, NCheckbox, NSelect, NTag } from "naive-ui"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import { getACMHelperList, getContest, updateACMHelperChecked } from "../api"
|
import { getACMHelperList, getContest, updateACMHelperChecked } from "../api"
|
||||||
@@ -10,19 +11,12 @@ interface Props {
|
|||||||
contestID: string
|
contestID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HelperItem {
|
/**
|
||||||
id: number
|
* ACM 助手行。`acInfo` 的**内容**是 acm_contest_rank.submission_info 的 JSONB 原文,
|
||||||
username: string
|
* 键名保持 snake_case —— 回滚时旧后端还要读。
|
||||||
real_name: string
|
*/
|
||||||
problem_id: string
|
type HelperItem = Omit<AcmHelperItem, "acInfo"> & {
|
||||||
problem_display_id: string
|
acInfo: SubmissionInfo
|
||||||
ac_info: {
|
|
||||||
is_ac: boolean
|
|
||||||
ac_time: number
|
|
||||||
error_number: number
|
|
||||||
checked?: boolean
|
|
||||||
}
|
|
||||||
checked: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -65,12 +59,12 @@ async function toggleChecked(item: HelperItem) {
|
|||||||
await updateACMHelperChecked(
|
await updateACMHelperChecked(
|
||||||
Number(props.contestID),
|
Number(props.contestID),
|
||||||
item.id,
|
item.id,
|
||||||
item.problem_id,
|
item.problemId,
|
||||||
newChecked,
|
newChecked,
|
||||||
)
|
)
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
item.checked = newChecked
|
item.checked = newChecked
|
||||||
item.ac_info.checked = newChecked
|
item.acInfo.checked = newChecked
|
||||||
|
|
||||||
// 强制触发响应式更新
|
// 强制触发响应式更新
|
||||||
submissions.value = [...submissions.value]
|
submissions.value = [...submissions.value]
|
||||||
@@ -95,11 +89,11 @@ async function markAllAsChecked() {
|
|||||||
await updateACMHelperChecked(
|
await updateACMHelperChecked(
|
||||||
Number(props.contestID),
|
Number(props.contestID),
|
||||||
item.id,
|
item.id,
|
||||||
item.problem_id,
|
item.problemId,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
item.checked = true
|
item.checked = true
|
||||||
item.ac_info.checked = true
|
item.acInfo.checked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制触发响应式更新
|
// 强制触发响应式更新
|
||||||
@@ -117,7 +111,7 @@ async function markAllAsChecked() {
|
|||||||
const filteredSubmissions = computed(() => {
|
const filteredSubmissions = computed(() => {
|
||||||
return submissions.value.filter((item) => {
|
return submissions.value.filter((item) => {
|
||||||
if (query.username && !item.username.includes(query.username)) return false
|
if (query.username && !item.username.includes(query.username)) return false
|
||||||
if (query.problemId && !item.problem_display_id.includes(query.problemId))
|
if (query.problemId && !item.problemDisplayId.includes(query.problemId))
|
||||||
return false
|
return false
|
||||||
if (query.checked === "checked" && !item.checked) return false
|
if (query.checked === "checked" && !item.checked) return false
|
||||||
if (query.checked === "unchecked" && item.checked) return false
|
if (query.checked === "unchecked" && item.checked) return false
|
||||||
@@ -139,8 +133,8 @@ async function viewSubmission(item: HelperItem) {
|
|||||||
// 查询该用户在该竞赛该题目的 AC 提交
|
// 查询该用户在该竞赛该题目的 AC 提交
|
||||||
const res = await getSubmissions({
|
const res = await getSubmissions({
|
||||||
username: item.username,
|
username: item.username,
|
||||||
problem_id: item.problem_display_id,
|
problemId: item.problemDisplayId,
|
||||||
contest_id: props.contestID,
|
contestId: props.contestID,
|
||||||
result: "0", // ACCEPTED
|
result: "0", // ACCEPTED
|
||||||
language: "",
|
language: "",
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -161,7 +155,7 @@ async function viewSubmission(item: HelperItem) {
|
|||||||
currentSubmission.value = {
|
currentSubmission.value = {
|
||||||
...detailRes.data,
|
...detailRes.data,
|
||||||
contest: Number(props.contestID),
|
contest: Number(props.contestID),
|
||||||
problem_display_id: item.problem_display_id,
|
problem_display_id: item.problemDisplayId,
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCodePanel(true)
|
toggleCodePanel(true)
|
||||||
@@ -175,7 +169,7 @@ async function loadData() {
|
|||||||
try {
|
try {
|
||||||
// 先获取比赛信息,获取开始时间
|
// 先获取比赛信息,获取开始时间
|
||||||
const contestRes = await getContest(props.contestID)
|
const contestRes = await getContest(props.contestID)
|
||||||
contestStartTime.value = new Date(contestRes.data.start_time)
|
contestStartTime.value = new Date(contestRes.data.startTime)
|
||||||
|
|
||||||
// 再获取 AC 提交列表
|
// 再获取 AC 提交列表
|
||||||
const { data } = await getACMHelperList(Number(props.contestID))
|
const { data } = await getACMHelperList(Number(props.contestID))
|
||||||
@@ -195,13 +189,13 @@ const columns: DataTableColumn<HelperItem>[] = [
|
|||||||
title: "题目",
|
title: "题目",
|
||||||
key: "problem_display_id",
|
key: "problem_display_id",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => h(NTag, { type: "info" }, () => row.problem_display_id),
|
render: (row) => h(NTag, { type: "info" }, () => row.problemDisplayId),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AC时间",
|
title: "AC时间",
|
||||||
key: "ac_time",
|
key: "ac_time",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => formatACTime(row.ac_info.ac_time),
|
render: (row) => formatACTime(row.acInfo.ac_time),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "错误次数",
|
title: "错误次数",
|
||||||
@@ -211,10 +205,10 @@ const columns: DataTableColumn<HelperItem>[] = [
|
|||||||
h(
|
h(
|
||||||
NTag,
|
NTag,
|
||||||
{
|
{
|
||||||
type: row.ac_info.error_number > 0 ? "warning" : "success",
|
type: row.acInfo.error_number > 0 ? "warning" : "success",
|
||||||
size: "small",
|
size: "small",
|
||||||
},
|
},
|
||||||
() => row.ac_info.error_number,
|
() => row.acInfo.error_number,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -310,7 +304,7 @@ onMounted(loadData)
|
|||||||
<SubmissionDetail
|
<SubmissionDetail
|
||||||
v-if="currentSubmission"
|
v-if="currentSubmission"
|
||||||
:submission="currentSubmission"
|
:submission="currentSubmission"
|
||||||
:problemID="currentSubmission.problem_display_id"
|
:problemID="currentSubmission.problemDisplayId"
|
||||||
:submissionID="currentSubmission.id"
|
:submissionID="currentSubmission.id"
|
||||||
hideList
|
hideList
|
||||||
@copied="toggleCodePanel(false)"
|
@copied="toggleCodePanel(false)"
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ const columns: DataTableColumn<Contest>[] = [
|
|||||||
title: "创建者",
|
title: "创建者",
|
||||||
key: "created_by",
|
key: "created_by",
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (row) => row.created_by.username,
|
render: (row) => row.createdBy.username,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 160,
|
width: 160,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "可见",
|
title: "可见",
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getStuckProblems } from "admin/api"
|
import { getStuckProblems } from "admin/api"
|
||||||
|
import type { StuckProblem } from "utils/types"
|
||||||
interface StuckProblem {
|
|
||||||
problem_id: string
|
|
||||||
problem_title: string
|
|
||||||
total: number
|
|
||||||
failed: number
|
|
||||||
failed_users: number
|
|
||||||
ac_rate: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const data = ref<StuckProblem[]>([])
|
const data = ref<StuckProblem[]>([])
|
||||||
|
|
||||||
const columns: DataTableColumn<StuckProblem>[] = [
|
const columns: DataTableColumn<StuckProblem>[] = [
|
||||||
{ title: "题目 ID", key: "problem_id", width: 100 },
|
{ title: "题目 ID", key: "problemId", width: 100 },
|
||||||
{ title: "题目名称", key: "problem_title", minWidth: 200 },
|
{ title: "题目名称", key: "problemTitle", minWidth: 200 },
|
||||||
{ title: "总提交", key: "total", width: 100, sorter: "default" },
|
{ title: "总提交", key: "total", width: 100, sorter: "default" },
|
||||||
{ title: "失败次数", key: "failed", width: 100, sorter: "default" },
|
{ title: "失败次数", key: "failed", width: 100, sorter: "default" },
|
||||||
{
|
{
|
||||||
title: "卡住学生数",
|
title: "卡住学生数",
|
||||||
key: "failed_users",
|
key: "failedUsers",
|
||||||
width: 120,
|
width: 120,
|
||||||
sorter: "default",
|
sorter: "default",
|
||||||
defaultSortOrder: "descend",
|
defaultSortOrder: "descend",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AC 率",
|
title: "AC 率",
|
||||||
key: "ac_rate",
|
key: "acRate",
|
||||||
width: 100,
|
width: 100,
|
||||||
sorter: "default",
|
sorter: "default",
|
||||||
render: (row) => `${row.ac_rate}%`,
|
render: (row) => `${row.acRate}%`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AcTrend } from "utils/types"
|
||||||
import { Line } from "vue-chartjs"
|
import { Line } from "vue-chartjs"
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
@@ -22,18 +23,7 @@ ChartJS.register(
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface YearlyEntry {
|
type ProblemTrend = AcTrend
|
||||||
year: number
|
|
||||||
total: number
|
|
||||||
accepted: number
|
|
||||||
ac_rate: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProblemTrend {
|
|
||||||
problem_id: string
|
|
||||||
problem_title: string
|
|
||||||
yearly: YearlyEntry[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
const yearOptions = Array.from({ length: currentYear - 2022 + 1 }, (_, i) => ({
|
const yearOptions = Array.from({ length: currentYear - 2022 + 1 }, (_, i) => ({
|
||||||
@@ -79,7 +69,7 @@ function getChartData(problem: ProblemTrend) {
|
|||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "AC 率",
|
label: "AC 率",
|
||||||
data: problem.yearly.map((y) => y.ac_rate),
|
data: problem.yearly.map((y) => y.acRate),
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
||||||
@@ -98,14 +88,14 @@ function getChartOptions(problem: ProblemTrend) {
|
|||||||
plugins: {
|
plugins: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: `${problem.problem_id} · ${problem.problem_title}`,
|
text: `${problem.problemId} · ${problem.problemTitle}`,
|
||||||
font: { size: 14 },
|
font: { size: 14 },
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (ctx: any) => {
|
label: (ctx: any) => {
|
||||||
const entry = problem.yearly[ctx.dataIndex]
|
const entry = problem.yearly[ctx.dataIndex]
|
||||||
return `AC 率: ${entry.ac_rate}% (${entry.accepted}/${entry.total})`
|
return `AC 率: ${entry.acRate}% (${entry.accepted}/${entry.total})`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -127,9 +117,9 @@ async function fetchData() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getTopACTrend({
|
const res = await getTopACTrend({
|
||||||
since_year: sinceYear.value,
|
sinceYear: sinceYear.value,
|
||||||
until_year: untilYear.value,
|
untilYear: untilYear.value,
|
||||||
min_per_year: minPerYear.value,
|
minPerYear: minPerYear.value,
|
||||||
})
|
})
|
||||||
data.value = res.data
|
data.value = res.data
|
||||||
} finally {
|
} finally {
|
||||||
@@ -174,7 +164,7 @@ onMounted(fetchData)
|
|||||||
暂无数据
|
暂无数据
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="grid">
|
<div v-else class="grid">
|
||||||
<div v-for="problem in data" :key="problem.problem_id" class="chart-card">
|
<div v-for="problem in data" :key="problem.problemId" class="chart-card">
|
||||||
<Line
|
<Line
|
||||||
:data="getChartData(problem)"
|
:data="getChartData(problem)"
|
||||||
:options="getChartOptions(problem)"
|
:options="getChartOptions(problem)"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ async function handleDeleteProblem() {
|
|||||||
message.success("删除成功")
|
message.success("删除成功")
|
||||||
emit("updated")
|
emit("updated")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.data === "Can't delete the problem as it has submissions") {
|
if (err.error === "problem-has-submissions") {
|
||||||
message.error("这道题有提交之后,就不能被删除")
|
message.error("这道题有提交之后,就不能被删除")
|
||||||
} else {
|
} else {
|
||||||
message.error("删除失败")
|
message.error("删除失败")
|
||||||
@@ -82,9 +82,9 @@ async function handleMakePublic() {
|
|||||||
showMakePublicModal.value = false
|
showMakePublicModal.value = false
|
||||||
emit("updated") // 刷新列表
|
emit("updated") // 刷新列表
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.data === "Duplicate display ID") {
|
if (err.error === "display-id-exists") {
|
||||||
message.error("该题目编号已存在,请使用其他编号")
|
message.error("该题目编号已存在,请使用其他编号")
|
||||||
} else if (err.data === "Already be a public problem") {
|
} else if (err.error === "already-public") {
|
||||||
message.error("该题目已经是公开题目")
|
message.error("该题目已经是公开题目")
|
||||||
} else {
|
} else {
|
||||||
message.error("转换失败:" + (err.data || "未知错误"))
|
message.error("转换失败:" + (err.data || "未知错误"))
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ async function addProblem() {
|
|||||||
)
|
)
|
||||||
emit("added")
|
emit("added")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.data === "Duplicate display id in this contest") {
|
if (err.error === "display-id-exists") {
|
||||||
message.error("显示编号重复了,请重新写一个")
|
message.error("显示编号重复了,请重新写一个")
|
||||||
} else if (err.data === "Contest has ended") {
|
} else if (err.error === "contest-ended") {
|
||||||
message.error("这场比赛已经结束了,不能添加题目")
|
message.error("这场比赛已经结束了,不能添加题目")
|
||||||
} else {
|
} else {
|
||||||
message.error(err.data)
|
message.error(err.data)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ async function submit() {
|
|||||||
)
|
)
|
||||||
const verb = props.action === "add" ? "添加" : "移除"
|
const verb = props.action === "add" ? "添加" : "移除"
|
||||||
message.success(
|
message.success(
|
||||||
`已为 ${res.data.problem_count} 道题${verb} ${res.data.tag_count} 个标签`,
|
`已为 ${res.data.problemCount} 道题${verb} ${res.data.tagCount} 个标签`,
|
||||||
)
|
)
|
||||||
close()
|
close()
|
||||||
emit("done")
|
emit("done")
|
||||||
@@ -96,7 +96,7 @@ watch(
|
|||||||
:checked="selectedSet.has(tag.name)"
|
:checked="selectedSet.has(tag.name)"
|
||||||
@update:checked="toggleTag(tag.name)"
|
@update:checked="toggleTag(tag.name)"
|
||||||
>
|
>
|
||||||
{{ tag.name }}({{ tag.problem_count }})
|
{{ tag.name }}({{ tag.problemCount }})
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-dynamic-tags v-if="action === 'add'" v-model:value="newTags" />
|
<n-dynamic-tags v-if="action === 'add'" v-model:value="newTags" />
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async function generate() {
|
|||||||
blanks.map(async (s) => {
|
blanks.map(async (s) => {
|
||||||
try {
|
try {
|
||||||
const res = await generateSQLTestcase({
|
const res = await generateSQLTestcase({
|
||||||
ref_sql: refSQL.value,
|
refSql: refSQL.value,
|
||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
})
|
})
|
||||||
s.sql = res.data.sql
|
s.sql = res.data.sql
|
||||||
@@ -154,8 +154,8 @@ async function preview() {
|
|||||||
s.stale = false
|
s.stale = false
|
||||||
try {
|
try {
|
||||||
const res = await previewSQLTestcase({
|
const res = await previewSQLTestcase({
|
||||||
init_sql: s.sql,
|
initSql: s.sql,
|
||||||
ref_sql: refSQL.value,
|
refSql: refSQL.value,
|
||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
})
|
})
|
||||||
s.display = res.data
|
s.display = res.data
|
||||||
|
|||||||
@@ -11,13 +11,7 @@ import {
|
|||||||
} from "utils/constants"
|
} from "utils/constants"
|
||||||
import download from "utils/download"
|
import download from "utils/download"
|
||||||
import { unique } from "utils/functions"
|
import { unique } from "utils/functions"
|
||||||
import type {
|
import type { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types"
|
||||||
BlankProblem,
|
|
||||||
LANGUAGE,
|
|
||||||
SQLConfig,
|
|
||||||
Tag,
|
|
||||||
Testcase,
|
|
||||||
} from "utils/types"
|
|
||||||
import {
|
import {
|
||||||
createContestProblem,
|
createContestProblem,
|
||||||
createProblem,
|
createProblem,
|
||||||
@@ -62,13 +56,13 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
|
|||||||
_id: "",
|
_id: "",
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
input_description: "",
|
inputDescription: "",
|
||||||
output_description: "",
|
outputDescription: "",
|
||||||
time_limit: 1000,
|
timeLimit: 1000,
|
||||||
memory_limit: 64,
|
memoryLimit: 64,
|
||||||
difficulty: "Low" as "Low" | "Mid" | "High",
|
difficulty: "Low",
|
||||||
visible: false,
|
visible: false,
|
||||||
share_submission: false,
|
shareSubmission: false,
|
||||||
tags: [],
|
tags: [],
|
||||||
languages: ["Python3", "C"] as LANGUAGE[],
|
languages: ["Python3", "C"] as LANGUAGE[],
|
||||||
template: {} as { [key in LANGUAGE]?: string },
|
template: {} as { [key in LANGUAGE]?: string },
|
||||||
@@ -77,20 +71,20 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
|
|||||||
{ input: "", output: "" },
|
{ input: "", output: "" },
|
||||||
{ input: "", output: "" },
|
{ input: "", output: "" },
|
||||||
],
|
],
|
||||||
test_case_id: "",
|
testCaseId: "",
|
||||||
test_case_score: [] as Testcase[],
|
testCaseScore: [] as Testcase[],
|
||||||
hint: "",
|
hint: "",
|
||||||
source: "",
|
source: "",
|
||||||
prompt: "",
|
prompt: "",
|
||||||
answers: [] as { language: LANGUAGE; code: string }[],
|
answers: [] as { language: LANGUAGE; code: string }[],
|
||||||
contest_id: "",
|
contestId: null,
|
||||||
allow_flowchart: false,
|
allowFlowchart: false,
|
||||||
mermaid_code: "",
|
showFlowchart: false,
|
||||||
flowchart_data: {},
|
mermaidCode: "",
|
||||||
flowchart_hint: "",
|
flowchartHint: "",
|
||||||
show_flowchart: false,
|
astRules: null,
|
||||||
ast_rules: null as { [key: string]: any[] } | null,
|
sqlConfig: null,
|
||||||
sql_config: null as SQLConfig | null,
|
sqlDisplay: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 从服务器来的tag列表
|
// 从服务器来的tag列表
|
||||||
@@ -189,19 +183,19 @@ watch(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
needTemplate.value = false
|
needTemplate.value = false
|
||||||
if (!problem.value.sql_config) {
|
if (!problem.value.sqlConfig) {
|
||||||
problem.value.sql_config = { mode: "query", order_sensitive: false }
|
problem.value.sqlConfig = { mode: "query", order_sensitive: false }
|
||||||
}
|
}
|
||||||
currentActiveAnswer.value = "SQL"
|
currentActiveAnswer.value = "SQL"
|
||||||
// 代码规则检查基于 Python/C 的 AST 解析,对 SQL 没有意义,清空避免脏数据
|
// 代码规则检查基于 Python/C 的 AST 解析,对 SQL 没有意义,清空避免脏数据
|
||||||
if (problem.value.ast_rules) {
|
if (problem.value.astRules) {
|
||||||
problem.value.ast_rules = null
|
problem.value.astRules = null
|
||||||
}
|
}
|
||||||
// 流程图依赖 Python 答案生成,对 SQL 没有意义
|
// 流程图依赖 Python 答案生成,对 SQL 没有意义
|
||||||
problem.value.allow_flowchart = false
|
problem.value.allowFlowchart = false
|
||||||
problem.value.show_flowchart = false
|
problem.value.showFlowchart = false
|
||||||
} else if (problem.value.sql_config) {
|
} else if (problem.value.sqlConfig) {
|
||||||
problem.value.sql_config = null
|
problem.value.sqlConfig = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -219,32 +213,31 @@ async function getProblemDetail() {
|
|||||||
problem.value._id = data._id
|
problem.value._id = data._id
|
||||||
problem.value.title = data.title
|
problem.value.title = data.title
|
||||||
problem.value.description = data.description
|
problem.value.description = data.description
|
||||||
problem.value.input_description = data.input_description
|
problem.value.inputDescription = data.inputDescription
|
||||||
problem.value.output_description = data.output_description
|
problem.value.outputDescription = data.outputDescription
|
||||||
problem.value.time_limit = data.time_limit
|
problem.value.timeLimit = data.timeLimit
|
||||||
problem.value.memory_limit = data.memory_limit
|
problem.value.memoryLimit = data.memoryLimit
|
||||||
problem.value.memory_limit = data.memory_limit
|
problem.value.memoryLimit = data.memoryLimit
|
||||||
problem.value.difficulty = data.difficulty
|
problem.value.difficulty = data.difficulty
|
||||||
problem.value.visible = data.visible
|
problem.value.visible = data.visible
|
||||||
problem.value.share_submission = data.share_submission
|
problem.value.shareSubmission = data.shareSubmission
|
||||||
problem.value.tags = normalizeTagNames(data.tags)
|
problem.value.tags = normalizeTagNames(data.tags)
|
||||||
problem.value.languages = data.languages
|
problem.value.languages = data.languages
|
||||||
problem.value.template = data.template
|
problem.value.template = data.template
|
||||||
problem.value.samples = data.samples
|
problem.value.samples = data.samples
|
||||||
problem.value.samples = data.samples
|
problem.value.samples = data.samples
|
||||||
problem.value.test_case_id = data.test_case_id
|
problem.value.testCaseId = data.testCaseId
|
||||||
problem.value.test_case_score = data.test_case_score
|
problem.value.testCaseScore = data.testCaseScore
|
||||||
problem.value.hint = data.hint
|
problem.value.hint = data.hint ?? ""
|
||||||
problem.value.source = data.source
|
problem.value.source = data.source
|
||||||
problem.value.prompt = data.prompt
|
problem.value.prompt = data.prompt
|
||||||
// 流程图相关字段
|
// 流程图相关字段
|
||||||
problem.value.allow_flowchart = data.allow_flowchart
|
problem.value.allowFlowchart = data.allowFlowchart
|
||||||
problem.value.show_flowchart = data.show_flowchart
|
problem.value.showFlowchart = data.showFlowchart
|
||||||
problem.value.mermaid_code = data.mermaid_code ?? ""
|
problem.value.mermaidCode = data.mermaidCode ?? ""
|
||||||
problem.value.flowchart_hint = data.flowchart_hint ?? ""
|
problem.value.flowchartHint = data.flowchartHint ?? ""
|
||||||
problem.value.flowchart_data = data.flowchart_data
|
problem.value.astRules = data.astRules ?? null
|
||||||
problem.value.ast_rules = data.ast_rules ?? null
|
problem.value.sqlConfig = data.sqlConfig ?? null
|
||||||
problem.value.sql_config = data.sql_config ?? null
|
|
||||||
if (data.answers && data.answers.length) {
|
if (data.answers && data.answers.length) {
|
||||||
problem.value.answers = data.answers
|
problem.value.answers = data.answers
|
||||||
} else {
|
} else {
|
||||||
@@ -253,8 +246,8 @@ async function getProblemDetail() {
|
|||||||
code: "",
|
code: "",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if (problem.value.contest_id) {
|
if (problem.value.contestId) {
|
||||||
problem.value.contest_id = problem.value.contest_id
|
problem.value.contestId = problem.value.contestId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下面是用来显示的:
|
// 下面是用来显示的:
|
||||||
@@ -305,8 +298,8 @@ async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
|
|||||||
for (let file of testcases) {
|
for (let file of testcases) {
|
||||||
file.score = (100 / testcases.length).toFixed(0)
|
file.score = (100 / testcases.length).toFixed(0)
|
||||||
}
|
}
|
||||||
problem.value.test_case_score = testcases
|
problem.value.testCaseScore = testcases
|
||||||
problem.value.test_case_id = res.data.id
|
problem.value.testCaseId = res.data.id
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error("上传测试用例失败")
|
message.error("上传测试用例失败")
|
||||||
}
|
}
|
||||||
@@ -338,7 +331,7 @@ async function validateProblem() {
|
|||||||
else if (
|
else if (
|
||||||
!problem.value.description ||
|
!problem.value.description ||
|
||||||
(!isSQLProblem.value &&
|
(!isSQLProblem.value &&
|
||||||
(!problem.value.input_description || !problem.value.output_description))
|
(!problem.value.inputDescription || !problem.value.outputDescription))
|
||||||
) {
|
) {
|
||||||
message.error("题目或输入或输出没有填写")
|
message.error("题目或输入或输出没有填写")
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
@@ -359,7 +352,7 @@ async function validateProblem() {
|
|||||||
hasErrors = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 测试用例
|
// 测试用例
|
||||||
else if (problem.value.test_case_score.length === 0) {
|
else if (problem.value.testCaseScore.length === 0) {
|
||||||
message.error("测试用例没有上传")
|
message.error("测试用例没有上传")
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
} else if (problem.value.languages.length === 0) {
|
} else if (problem.value.languages.length === 0) {
|
||||||
@@ -367,7 +360,7 @@ async function validateProblem() {
|
|||||||
hasErrors = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// SQL 题验证
|
// SQL 题验证
|
||||||
else if (isSQLProblem.value && !problem.value.sql_config?.mode) {
|
else if (isSQLProblem.value && !problem.value.sqlConfig?.mode) {
|
||||||
message.error("SQL 题需要选择题型(查询题/增删改题)")
|
message.error("SQL 题需要选择题型(查询题/增删改题)")
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
} else if (
|
} else if (
|
||||||
@@ -380,11 +373,8 @@ async function validateProblem() {
|
|||||||
hasErrors = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 流程图验证
|
// 流程图验证
|
||||||
else if (problem.value.show_flowchart || problem.value.allow_flowchart) {
|
else if (problem.value.showFlowchart || problem.value.allowFlowchart) {
|
||||||
if (
|
if (!problem.value.mermaidCode || problem.value.mermaidCode.trim() === "") {
|
||||||
!problem.value.mermaid_code ||
|
|
||||||
problem.value.mermaid_code.trim() === ""
|
|
||||||
) {
|
|
||||||
message.error("启用了流程图功能,但流程图代码为空")
|
message.error("启用了流程图功能,但流程图代码为空")
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
} else if (!mermaidRenderSuccess.value) {
|
} else if (!mermaidRenderSuccess.value) {
|
||||||
@@ -449,7 +439,7 @@ async function submit() {
|
|||||||
route.name === "admin contest problem create" ||
|
route.name === "admin contest problem create" ||
|
||||||
route.name === "admin contest problem edit"
|
route.name === "admin contest problem edit"
|
||||||
) {
|
) {
|
||||||
problem.value.contest_id = props.contestID
|
problem.value.contestId = Number(props.contestID)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api!(problem.value)
|
await api!(problem.value)
|
||||||
@@ -474,7 +464,7 @@ async function submit() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.data === "Display ID already exists") {
|
if (err.error === "display-id-exists") {
|
||||||
message.error("显示编号重复了,请换一个显示编号")
|
message.error("显示编号重复了,请换一个显示编号")
|
||||||
} else {
|
} else {
|
||||||
message.error(err.data)
|
message.error(err.data)
|
||||||
@@ -503,7 +493,7 @@ async function generateMermaid() {
|
|||||||
)
|
)
|
||||||
isAIGenerating.value = false
|
isAIGenerating.value = false
|
||||||
message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法")
|
message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法")
|
||||||
problem.value.mermaid_code = res.data.flowchart
|
problem.value.mermaidCode = res.data.flowchart
|
||||||
}
|
}
|
||||||
|
|
||||||
const showGeneratorModal = ref(false)
|
const showGeneratorModal = ref(false)
|
||||||
@@ -512,8 +502,8 @@ function handleTestcasesGenerated(
|
|||||||
testCaseId: string,
|
testCaseId: string,
|
||||||
testCaseScore: Testcase[],
|
testCaseScore: Testcase[],
|
||||||
) {
|
) {
|
||||||
problem.value.test_case_id = testCaseId
|
problem.value.testCaseId = testCaseId
|
||||||
problem.value.test_case_score = testCaseScore
|
problem.value.testCaseScore = testCaseScore
|
||||||
showGeneratorModal.value = false
|
showGeneratorModal.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,12 +583,12 @@ watch(
|
|||||||
/>
|
/>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
v-if="ready && !isSQLProblem"
|
v-if="ready && !isSQLProblem"
|
||||||
v-model:value="problem.input_description"
|
v-model:value="problem.inputDescription"
|
||||||
title="输入的描述"
|
title="输入的描述"
|
||||||
/>
|
/>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
v-if="ready && !isSQLProblem"
|
v-if="ready && !isSQLProblem"
|
||||||
v-model:value="problem.output_description"
|
v-model:value="problem.outputDescription"
|
||||||
title="输出的描述"
|
title="输出的描述"
|
||||||
/>
|
/>
|
||||||
<template v-if="!isSQLProblem">
|
<template v-if="!isSQLProblem">
|
||||||
@@ -686,12 +676,12 @@ watch(
|
|||||||
</n-form>
|
</n-form>
|
||||||
|
|
||||||
<n-form
|
<n-form
|
||||||
v-if="isSQLProblem && problem.sql_config"
|
v-if="isSQLProblem && problem.sqlConfig"
|
||||||
inline
|
inline
|
||||||
label-placement="left"
|
label-placement="left"
|
||||||
>
|
>
|
||||||
<n-form-item label="SQL 题型">
|
<n-form-item label="SQL 题型">
|
||||||
<n-radio-group v-model:value="problem.sql_config.mode">
|
<n-radio-group v-model:value="problem.sqlConfig.mode">
|
||||||
<n-radio-button value="query">查询题(比对查询结果)</n-radio-button>
|
<n-radio-button value="query">查询题(比对查询结果)</n-radio-button>
|
||||||
<n-radio-button value="modify">
|
<n-radio-button value="modify">
|
||||||
增删改题(比对执行后的表数据)
|
增删改题(比对执行后的表数据)
|
||||||
@@ -699,7 +689,7 @@ watch(
|
|||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="严格比对行顺序">
|
<n-form-item label="严格比对行顺序">
|
||||||
<n-switch v-model:value="problem.sql_config.order_sensitive" />
|
<n-switch v-model:value="problem.sqlConfig.order_sensitive" />
|
||||||
<n-text depth="3" style="margin-left: 12px">
|
<n-text depth="3" style="margin-left: 12px">
|
||||||
题目要求 ORDER BY 时开启;关闭则按无序集合比对
|
题目要求 ORDER BY 时开启;关闭则按无序集合比对
|
||||||
</n-text>
|
</n-text>
|
||||||
@@ -766,7 +756,7 @@ watch(
|
|||||||
<n-grid v-if="!isSQLProblem" :cols="2">
|
<n-grid v-if="!isSQLProblem" :cols="2">
|
||||||
<n-gi :span="1">
|
<n-gi :span="1">
|
||||||
<AstRulesEditor
|
<AstRulesEditor
|
||||||
v-model="problem.ast_rules!"
|
v-model="problem.astRules!"
|
||||||
:languages="problem.languages"
|
:languages="problem.languages"
|
||||||
/>
|
/>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
@@ -802,22 +792,22 @@ watch(
|
|||||||
<SQLTestcaseEditor
|
<SQLTestcaseEditor
|
||||||
v-if="isSQLProblem"
|
v-if="isSQLProblem"
|
||||||
:answers="problem.answers"
|
:answers="problem.answers"
|
||||||
:mode="problem.sql_config?.mode ?? 'query'"
|
:mode="problem.sqlConfig?.mode ?? 'query'"
|
||||||
:problem-id="problem.id"
|
:problem-id="problem.id"
|
||||||
@uploaded="handleTestcasesGenerated"
|
@uploaded="handleTestcasesGenerated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<n-alert
|
<n-alert
|
||||||
class="box"
|
class="box"
|
||||||
v-if="problem.test_case_score.length"
|
v-if="problem.testCaseScore.length"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
type="info"
|
type="info"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<div>
|
<div>
|
||||||
测试组编号 {{ problem.test_case_id.slice(0, 12) }} 共有
|
测试组编号 {{ problem.testCaseId.slice(0, 12) }} 共有
|
||||||
{{ problem.test_case_score.length }}
|
{{ problem.testCaseScore.length }}
|
||||||
条测试用例
|
条测试用例
|
||||||
</div>
|
</div>
|
||||||
<n-button
|
<n-button
|
||||||
@@ -870,23 +860,23 @@ watch(
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="允许提交流程图">
|
<n-form-item label="允许提交流程图">
|
||||||
<n-switch v-model:value="problem.allow_flowchart" />
|
<n-switch v-model:value="problem.allowFlowchart" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="显示标准流程图">
|
<n-form-item label="显示标准流程图">
|
||||||
<n-switch v-model:value="problem.show_flowchart" />
|
<n-switch v-model:value="problem.showFlowchart" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
|
||||||
<n-form>
|
<n-form>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<MermaidEditor
|
<MermaidEditor
|
||||||
v-model="problem.mermaid_code"
|
v-model="problem.mermaidCode"
|
||||||
@render-success="onMermaidRenderSuccess"
|
@render-success="onMermaidRenderSuccess"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="流程图提示信息(选填)">
|
<n-form-item label="流程图提示信息(选填)">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="problem.flowchart_hint"
|
v-model:value="problem.flowchartHint"
|
||||||
placeholder="请输入流程图相关的提示信息,帮助学生理解题目要求"
|
placeholder="请输入流程图相关的提示信息,帮助学生理解题目要求"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|||||||
@@ -113,20 +113,20 @@ const baseColumns: DataTableColumn<AdminProblemFiltered>[] = [
|
|||||||
width: 80,
|
width: 80,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(NFlex, { size: 4, align: "center" }, () => [
|
h(NFlex, { size: 4, align: "center" }, () => [
|
||||||
row.allow_flowchart
|
row.allowFlowchart
|
||||||
? h(Icon, {
|
? h(Icon, {
|
||||||
width: 18,
|
width: 18,
|
||||||
icon: "vscode-icons:file-type-drawio",
|
icon: "vscode-icons:file-type-drawio",
|
||||||
title: "绘图",
|
title: "绘图",
|
||||||
})
|
})
|
||||||
: row.show_flowchart
|
: row.showFlowchart
|
||||||
? h(Icon, {
|
? h(Icon, {
|
||||||
width: 18,
|
width: 18,
|
||||||
icon: "vscode-icons:file-type-graphql",
|
icon: "vscode-icons:file-type-graphql",
|
||||||
title: "流程图",
|
title: "流程图",
|
||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
row.has_ast_rules
|
row.hasAstRules
|
||||||
? h(Icon, {
|
? h(Icon, {
|
||||||
width: 18,
|
width: 18,
|
||||||
icon: "vscode-icons:file-type-light-todo",
|
icon: "vscode-icons:file-type-light-todo",
|
||||||
@@ -140,7 +140,7 @@ const baseColumns: DataTableColumn<AdminProblemFiltered>[] = [
|
|||||||
key: "top_reaction",
|
key: "top_reaction",
|
||||||
width: 60,
|
width: 60,
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
const top = row.top_reaction
|
const top = row.topReaction
|
||||||
if (!top) return null
|
if (!top) return null
|
||||||
const reaction = REACTIONS.find((it) => it.key === top.type)
|
const reaction = REACTIONS.find((it) => it.key === top.type)
|
||||||
if (!reaction) return null
|
if (!reaction) return null
|
||||||
@@ -155,7 +155,7 @@ const baseColumns: DataTableColumn<AdminProblemFiltered>[] = [
|
|||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "可见",
|
title: "可见",
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const columns: DataTableColumn<AdminTag>[] = [
|
|||||||
h(
|
h(
|
||||||
NButton,
|
NButton,
|
||||||
{ text: true, type: "primary", onClick: () => openTagProblems(row) },
|
{ text: true, type: "primary", onClick: () => openTagProblems(row) },
|
||||||
() => String(row.problem_count),
|
() => String(row.problemCount),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -123,7 +123,7 @@ async function saveTag(tag: AdminTag) {
|
|||||||
const res = await renameTag(tag.id, name)
|
const res = await renameTag(tag.id, name)
|
||||||
if (res.data.merged) {
|
if (res.data.merged) {
|
||||||
message.success(
|
message.success(
|
||||||
`已合并到「${res.data.name}」,影响 ${res.data.affected_count} 道题`,
|
`已合并到「${res.data.name}」,影响 ${res.data.affectedCount} 道题`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
message.success("已重命名")
|
message.success("已重命名")
|
||||||
@@ -135,7 +135,7 @@ async function saveTag(tag: AdminTag) {
|
|||||||
function confirmDelete(tag: AdminTag) {
|
function confirmDelete(tag: AdminTag) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: "删除标签",
|
title: "删除标签",
|
||||||
content: `确定删除标签「${tag.name}」吗?当前有 ${tag.problem_count} 道题在使用它,删除后这些题目会失去该标签。`,
|
content: `确定删除标签「${tag.name}」吗?当前有 ${tag.problemCount} 道题在使用它,删除后这些题目会失去该标签。`,
|
||||||
positiveText: "删除",
|
positiveText: "删除",
|
||||||
negativeText: "取消",
|
negativeText: "取消",
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function handleConfirm() {
|
|||||||
|
|
||||||
// 只有非"完成所有题目"时才添加条件值
|
// 只有非"完成所有题目"时才添加条件值
|
||||||
if (newBadgeConditionType.value !== "all_problems") {
|
if (newBadgeConditionType.value !== "all_problems") {
|
||||||
data.condition_value = newBadgeConditionValue.value
|
data.conditionValue = newBadgeConditionValue.value
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("confirm", data)
|
emit("confirm", data)
|
||||||
|
|||||||
@@ -48,16 +48,16 @@ defineEmits<Emits>()
|
|||||||
problem_count: '完成指定数量题目',
|
problem_count: '完成指定数量题目',
|
||||||
score: '达到指定分数',
|
score: '达到指定分数',
|
||||||
}
|
}
|
||||||
return typeMap[row.condition_type] || row.condition_type
|
return typeMap[row.conditionType] || row.conditionType
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '条件值',
|
title: '条件值',
|
||||||
key: 'condition_value',
|
key: 'condition_value',
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return row.condition_type === 'all_problems'
|
return row.conditionType === 'all_problems'
|
||||||
? '-'
|
? '-'
|
||||||
: row.condition_value
|
: row.conditionValue
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: '描述', key: 'description' },
|
{ title: '描述', key: 'description' },
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function handleConfirm() {
|
|||||||
|
|
||||||
// 只有非"完成所有题目"时才添加条件值
|
// 只有非"完成所有题目"时才添加条件值
|
||||||
if (editBadgeConditionType.value !== "all_problems") {
|
if (editBadgeConditionType.value !== "all_problems") {
|
||||||
data.condition_value = editBadgeConditionValue.value
|
data.conditionValue = editBadgeConditionValue.value
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("confirm", data)
|
emit("confirm", data)
|
||||||
@@ -76,8 +76,8 @@ watch(
|
|||||||
editBadgeName.value = newBadge.name
|
editBadgeName.value = newBadge.name
|
||||||
editBadgeDescription.value = newBadge.description
|
editBadgeDescription.value = newBadge.description
|
||||||
editBadgeIcon.value = newBadge.icon
|
editBadgeIcon.value = newBadge.icon
|
||||||
editBadgeConditionType.value = newBadge.condition_type
|
editBadgeConditionType.value = newBadge.conditionType
|
||||||
editBadgeConditionValue.value = newBadge.condition_value
|
editBadgeConditionValue.value = newBadge.conditionValue
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ watch(
|
|||||||
(newProblem) => {
|
(newProblem) => {
|
||||||
if (newProblem) {
|
if (newProblem) {
|
||||||
editProblemOrder.value = newProblem.order
|
editProblemOrder.value = newProblem.order
|
||||||
editProblemRequired.value = newProblem.is_required
|
editProblemRequired.value = newProblem.isRequired
|
||||||
editProblemScore.value = newProblem.score
|
editProblemScore.value = newProblem.score
|
||||||
editProblemHint.value = newProblem.hint || ""
|
editProblemHint.value = newProblem.hint || ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ defineEmits<Emits>()
|
|||||||
title: '必做',
|
title: '必做',
|
||||||
key: 'is_required',
|
key: 'is_required',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (row) => (row.is_required ? '是' : '否'),
|
render: (row) => (row.isRequired ? '是' : '否'),
|
||||||
},
|
},
|
||||||
{ title: '分数', key: 'score', width: 80 },
|
{ title: '分数', key: 'score', width: 80 },
|
||||||
{ title: '提示', key: 'hint', minWidth: 200 },
|
{ title: '提示', key: 'hint', minWidth: 200 },
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ defineProps<Props>()
|
|||||||
{{ problemSet.description }}
|
{{ problemSet.description }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="创建者">
|
<n-descriptions-item label="创建者">
|
||||||
{{ problemSet.created_by.username }}
|
{{ problemSet.createdBy.username }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="难度">
|
<n-descriptions-item label="难度">
|
||||||
<n-tag
|
<n-tag
|
||||||
@@ -60,10 +60,10 @@ defineProps<Props>()
|
|||||||
{{ problemSet.visible ? "是" : "否" }}
|
{{ problemSet.visible ? "是" : "否" }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="题目数量">
|
<n-descriptions-item label="题目数量">
|
||||||
{{ problemSet.problems_count }}
|
{{ problemSet.problemsCount }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="创建时间">
|
<n-descriptions-item label="创建时间">
|
||||||
{{ parseTime(problemSet.create_time, "YYYY-MM-DD HH:mm:ss") }}
|
{{ parseTime(problemSet.createTime, "YYYY-MM-DD HH:mm:ss") }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const progressColumns = [
|
|||||||
key: "join_time",
|
key: "join_time",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row: ProblemSetProgress) =>
|
render: (row: ProblemSetProgress) =>
|
||||||
parseTime(row.join_time, "YYYY-MM-DD HH:mm:ss"),
|
parseTime(row.joinTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{ title: "已完成", key: "completed_problems_count", width: 100 },
|
{ title: "已完成", key: "completed_problems_count", width: 100 },
|
||||||
{ title: "总题目", key: "total_problems_count", width: 100 },
|
{ title: "总题目", key: "total_problems_count", width: 100 },
|
||||||
@@ -32,13 +32,13 @@ const progressColumns = [
|
|||||||
key: "progress_percentage",
|
key: "progress_percentage",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row: ProblemSetProgress) =>
|
render: (row: ProblemSetProgress) =>
|
||||||
`${row.progress_percentage.toFixed(0)}%`,
|
`${row.progressPercentage.toFixed(0)}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "是否完成",
|
title: "是否完成",
|
||||||
key: "is_completed",
|
key: "is_completed",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row: ProblemSetProgress) => (row.is_completed ? "是" : "否"),
|
render: (row: ProblemSetProgress) => (row.isCompleted ? "是" : "否"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
|
|||||||
@@ -15,16 +15,14 @@ const formData = ref<CreateProblemSetData & Partial<EditProblemSetData>>({
|
|||||||
difficulty: "Easy",
|
difficulty: "Easy",
|
||||||
status: "draft",
|
status: "draft",
|
||||||
visible: false,
|
visible: false,
|
||||||
end_time: null,
|
endTime: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const endTimeTimestamp = computed({
|
const endTimeTimestamp = computed({
|
||||||
get: () =>
|
get: () =>
|
||||||
formData.value.end_time
|
formData.value.endTime ? new Date(formData.value.endTime).getTime() : null,
|
||||||
? new Date(formData.value.end_time).getTime()
|
|
||||||
: null,
|
|
||||||
set: (val: number | null) => {
|
set: (val: number | null) => {
|
||||||
formData.value.end_time = val ? new Date(val) : null
|
formData.value.endTime = val ? new Date(val) : null
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ async function loadProblemSetDetail() {
|
|||||||
difficulty: data.difficulty,
|
difficulty: data.difficulty,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
visible: data.visible,
|
visible: data.visible,
|
||||||
end_time: data.end_time ? new Date(data.end_time) : null,
|
endTime: data.endTime ? new Date(data.endTime) : null,
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import type { ProblemSetList } from "utils/types"
|
import type { ProblemSet } from "utils/types"
|
||||||
import { getProblemSetList, toggleProblemSetVisible } from "../api"
|
import { getProblemSetList, toggleProblemSetVisible } from "../api"
|
||||||
import Actions from "./components/Actions.vue"
|
import Actions from "./components/Actions.vue"
|
||||||
import { NTag, NSwitch } from "naive-ui"
|
import { NTag, NSwitch } from "naive-ui"
|
||||||
|
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const problemSets = ref<ProblemSetList[]>([])
|
const problemSets = ref<ProblemSet[]>([])
|
||||||
|
|
||||||
interface ProblemSetQuery {
|
interface ProblemSetQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
@@ -37,7 +37,7 @@ const statusOptions = [
|
|||||||
{ label: "草稿", value: "draft" },
|
{ label: "草稿", value: "draft" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const columns: DataTableColumn<ProblemSetList>[] = [
|
const columns: DataTableColumn<ProblemSet>[] = [
|
||||||
{ title: "ID", key: "id", width: 80 },
|
{ title: "ID", key: "id", width: 80 },
|
||||||
{ title: "标题", key: "title", minWidth: 200 },
|
{ title: "标题", key: "title", minWidth: 200 },
|
||||||
{ title: "描述", key: "description", minWidth: 300, ellipsis: true },
|
{ title: "描述", key: "description", minWidth: 300, ellipsis: true },
|
||||||
@@ -45,7 +45,7 @@ const columns: DataTableColumn<ProblemSetList>[] = [
|
|||||||
title: "创建者",
|
title: "创建者",
|
||||||
key: "created_by",
|
key: "created_by",
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (row) => row.created_by.username,
|
render: (row) => row.createdBy.username,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "难度",
|
title: "难度",
|
||||||
@@ -87,7 +87,7 @@ const columns: DataTableColumn<ProblemSetList>[] = [
|
|||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "可见",
|
title: "可见",
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const serverColumns: DataTableColumn<Server>[] = [
|
|||||||
{
|
{
|
||||||
title: "内存占用",
|
title: "内存占用",
|
||||||
key: "memory_usage",
|
key: "memory_usage",
|
||||||
render: (row) => row.memory_usage + "%",
|
render: (row) => row.memoryUsage + "%",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{ title: "IP", key: "ip", width: 140 },
|
{ title: "IP", key: "ip", width: 140 },
|
||||||
@@ -103,13 +103,13 @@ const serverColumns: DataTableColumn<Server>[] = [
|
|||||||
{
|
{
|
||||||
title: "上一次心跳",
|
title: "上一次心跳",
|
||||||
key: "last_heartbeat",
|
key: "last_heartbeat",
|
||||||
render: (row) => parseTime(row.last_heartbeat, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.lastHeartbeat, "YYYY-MM-DD HH:mm:ss"),
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -122,32 +122,32 @@ const abnormalServers = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const websiteConfig = reactive({
|
const websiteConfig = reactive({
|
||||||
website_base_url: import.meta.env.PUBLIC_OJ_URL,
|
websiteBaseUrl: import.meta.env.PUBLIC_OJ_URL,
|
||||||
website_name: "判题狗",
|
websiteName: "判题狗",
|
||||||
website_name_shortcut: "判题狗",
|
websiteNameShortcut: "判题狗",
|
||||||
website_footer: "所有权归属于徐越,感谢青岛大学开源 OJ 系统,感谢开源社区",
|
websiteFooter: "所有权归属于徐越,感谢青岛大学开源 OJ 系统,感谢开源社区",
|
||||||
allow_register: true,
|
allowRegister: true,
|
||||||
submission_list_show_all: true,
|
submissionListShowAll: true,
|
||||||
class_list: [],
|
classList: [],
|
||||||
enable_maxkb: true,
|
enableMaxkb: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getWebsiteConfig() {
|
async function getWebsiteConfig() {
|
||||||
const res = await getWebsite()
|
const res = await getWebsite()
|
||||||
websiteConfig.website_base_url = res.data.website_base_url
|
websiteConfig.websiteBaseUrl = res.data.websiteBaseUrl
|
||||||
websiteConfig.website_name = res.data.website_name
|
websiteConfig.websiteName = res.data.websiteName
|
||||||
websiteConfig.website_name_shortcut = res.data.website_name_shortcut
|
websiteConfig.websiteNameShortcut = res.data.websiteNameShortcut
|
||||||
websiteConfig.website_footer = res.data.website_footer
|
websiteConfig.websiteFooter = res.data.websiteFooter
|
||||||
websiteConfig.allow_register = res.data.allow_register
|
websiteConfig.allowRegister = res.data.allowRegister
|
||||||
websiteConfig.submission_list_show_all = res.data.submission_list_show_all
|
websiteConfig.submissionListShowAll = res.data.submissionListShowAll
|
||||||
websiteConfig.class_list = res.data.class_list
|
websiteConfig.classList = res.data.classList
|
||||||
websiteConfig.enable_maxkb = res.data.enable_maxkb
|
websiteConfig.enableMaxkb = res.data.enableMaxkb
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveWebsiteConfig() {
|
async function saveWebsiteConfig() {
|
||||||
// 班级号要和用户名里 ks 后面那段对得上,位数不对登录页会查不到该班学生。
|
// 班级号要和用户名里 ks 后面那段对得上,位数不对登录页会查不到该班学生。
|
||||||
// 后端 CreateEditWebsiteConfigSerializer 也会拦,这里先报更明确的错
|
// 后端 CreateEditWebsiteConfigSerializer 也会拦,这里先报更明确的错
|
||||||
const invalid = websiteConfig.class_list.filter((c) => !CLASS_NAME_RE.test(c))
|
const invalid = websiteConfig.classList.filter((c) => !CLASS_NAME_RE.test(c))
|
||||||
if (invalid.length) {
|
if (invalid.length) {
|
||||||
message.error(
|
message.error(
|
||||||
`班级号 ${invalid.join("、")} 必须是 ${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位数字`,
|
`班级号 ${invalid.join("、")} 必须是 ${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位数字`,
|
||||||
@@ -165,11 +165,8 @@ async function saveWebsiteConfig() {
|
|||||||
configStore.getConfig()
|
configStore.getConfig()
|
||||||
|
|
||||||
// 通过 WebSocket 广播配置变化,实现实时切换
|
// 通过 WebSocket 广播配置变化,实现实时切换
|
||||||
updateConfig("enable_maxkb", websiteConfig.enable_maxkb)
|
updateConfig("enable_maxkb", websiteConfig.enableMaxkb)
|
||||||
updateConfig(
|
updateConfig("submission_list_show_all", websiteConfig.submissionListShowAll)
|
||||||
"submission_list_show_all",
|
|
||||||
websiteConfig.submission_list_show_all,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteTestcase(id?: string) {
|
async function deleteTestcase(id?: string) {
|
||||||
@@ -222,19 +219,19 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<n-form inline label-placement="left">
|
<n-form inline label-placement="left">
|
||||||
<n-form-item label="网站 URL">
|
<n-form-item label="网站 URL">
|
||||||
<n-input class="url" v-model:value="websiteConfig.website_base_url" />
|
<n-input class="url" v-model:value="websiteConfig.websiteBaseUrl" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="网站名">
|
<n-form-item label="网站名">
|
||||||
<n-input v-model:value="websiteConfig.website_name" />
|
<n-input v-model:value="websiteConfig.websiteName" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="网站简称">
|
<n-form-item label="网站简称">
|
||||||
<n-input v-model:value="websiteConfig.website_name_shortcut" />
|
<n-input v-model:value="websiteConfig.websiteNameShortcut" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<n-form label-placement="left">
|
<n-form label-placement="left">
|
||||||
<n-form-item label="班级列表">
|
<n-form-item label="班级列表">
|
||||||
<n-flex vertical size="small">
|
<n-flex vertical size="small">
|
||||||
<n-dynamic-tags v-model:value="websiteConfig.class_list" />
|
<n-dynamic-tags v-model:value="websiteConfig.classList" />
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">
|
||||||
填 {{ CLASS_NAME_MIN_DIGITS }}~{{ CLASS_NAME_MAX_DIGITS }}
|
填 {{ CLASS_NAME_MIN_DIGITS }}~{{ CLASS_NAME_MAX_DIGITS }}
|
||||||
位数字,如 251、2510,要和用户名里 ks 后面那段一致
|
位数字,如 251、2510,要和用户名里 ks 后面那段一致
|
||||||
@@ -245,15 +242,15 @@ onMounted(() => {
|
|||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<span>是否允许注册</span>
|
<span>是否允许注册</span>
|
||||||
<n-switch v-model:value="websiteConfig.allow_register" />
|
<n-switch v-model:value="websiteConfig.allowRegister" />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<span>显示所有提交</span>
|
<span>显示所有提交</span>
|
||||||
<n-switch v-model:value="websiteConfig.submission_list_show_all" />
|
<n-switch v-model:value="websiteConfig.submissionListShowAll" />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<span>启用AI小助手</span>
|
<span>启用AI小助手</span>
|
||||||
<n-switch v-model:value="websiteConfig.enable_maxkb" />
|
<n-switch v-model:value="websiteConfig.enableMaxkb" />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ const columns: DataTableColumn<Rank>[] = [
|
|||||||
title: "正确率",
|
title: "正确率",
|
||||||
key: "rate",
|
key: "rate",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => getACRate(row.accepted_number, row.submission_number),
|
render: (row) => getACRate(row.acceptedNumber, row.submissionNumber),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const res = await getBaseInfo()
|
const res = await getBaseInfo()
|
||||||
userCount.value = res.data.user_count
|
userCount.value = res.data.userCount
|
||||||
submissionCount.value = res.data.today_submission_count
|
submissionCount.value = res.data.todaySubmissionCount
|
||||||
contestCount.value = res.data.recent_contest_count
|
contestCount.value = res.data.recentContestCount
|
||||||
})
|
})
|
||||||
|
|
||||||
async function listRanks() {
|
async function listRanks() {
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import type { AdminProblem } from "utils/types"
|
import type { AdminProblemFiltered, AdminProblemListItem } from "utils/types"
|
||||||
|
|
||||||
// 把后端的 AdminProblem 塑形成管理端列表项,与请求逻辑解耦。
|
// 把后端的列表项塑形成管理端列表行,与请求逻辑解耦。
|
||||||
export function toProblemListItem(result: AdminProblem) {
|
export function toProblemListItem(
|
||||||
|
result: AdminProblemListItem,
|
||||||
|
): AdminProblemFiltered {
|
||||||
return {
|
return {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
_id: result._id,
|
_id: result._id,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
username: result.created_by.username,
|
username: result.createdBy.username,
|
||||||
create_time: result.create_time,
|
createTime: result.createTime,
|
||||||
visible: result.visible,
|
visible: result.visible,
|
||||||
difficulty: result.difficulty,
|
difficulty: result.difficulty,
|
||||||
tags: result.tags,
|
tags: result.tags,
|
||||||
has_ast_rules: result.has_ast_rules,
|
hasAstRules: result.hasAstRules,
|
||||||
allow_flowchart: result.allow_flowchart,
|
allowFlowchart: result.allowFlowchart,
|
||||||
show_flowchart: result.show_flowchart,
|
showFlowchart: result.showFlowchart,
|
||||||
// 比赛题目列表接口不返回这个字段
|
// 比赛题目列表接口不返回这个字段
|
||||||
top_reaction: result.top_reaction ?? null,
|
topReaction: (result.topReaction ??
|
||||||
|
null) as AdminProblemFiltered["topReaction"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ async function save() {
|
|||||||
message.success("练习题已更新")
|
message.success("练习题已更新")
|
||||||
} else {
|
} else {
|
||||||
await createExercise({
|
await createExercise({
|
||||||
tutorial_id: props.tutorialId,
|
tutorialId: props.tutorialId,
|
||||||
type: formType.value,
|
type: formType.value,
|
||||||
data,
|
data,
|
||||||
order: formOrder.value,
|
order: formOrder.value,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const tutorial = reactive<Tutorial>({
|
|||||||
title: "",
|
title: "",
|
||||||
content: "",
|
content: "",
|
||||||
code: "",
|
code: "",
|
||||||
is_public: false,
|
isPublic: false,
|
||||||
order: 0,
|
order: 0,
|
||||||
type: "python", // 默认选择 Python
|
type: "python", // 默认选择 Python
|
||||||
})
|
})
|
||||||
@@ -39,7 +39,7 @@ async function init() {
|
|||||||
tutorial.title = data.title
|
tutorial.title = data.title
|
||||||
tutorial.content = data.content
|
tutorial.content = data.content
|
||||||
tutorial.code = data.code || ""
|
tutorial.code = data.code || ""
|
||||||
tutorial.is_public = data.is_public
|
tutorial.isPublic = data.isPublic
|
||||||
tutorial.order = data.order
|
tutorial.order = data.order
|
||||||
tutorial.type = data.type || "python"
|
tutorial.type = data.type || "python"
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ async function submit() {
|
|||||||
title: tutorial.title,
|
title: tutorial.title,
|
||||||
content: tutorial.content,
|
content: tutorial.content,
|
||||||
code: tutorial.code,
|
code: tutorial.code,
|
||||||
is_public: tutorial.is_public,
|
isPublic: tutorial.isPublic,
|
||||||
order: tutorial.order,
|
order: tutorial.order,
|
||||||
type: tutorial.type,
|
type: tutorial.type,
|
||||||
})
|
})
|
||||||
@@ -94,7 +94,7 @@ onMounted(init)
|
|||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="可见">
|
<n-form-item label="可见">
|
||||||
<n-switch v-model:value="tutorial.is_public" />
|
<n-switch v-model:value="tutorial.isPublic" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<n-button type="primary" @click="submit">保存</n-button>
|
<n-button type="primary" @click="submit">保存</n-button>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NSwitch } from "naive-ui"
|
import { NSwitch } from "naive-ui"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import type { Tutorial } from "utils/types"
|
import type { TutorialListItem } from "utils/types"
|
||||||
import { getTutorialList, setTutorialVisibility } from "../api"
|
import { getTutorialList, setTutorialVisibility } from "../api"
|
||||||
import Actions from "./components/Actions.vue"
|
import Actions from "./components/Actions.vue"
|
||||||
|
|
||||||
const tutorials = ref<{ [key: string]: Tutorial[] }>({
|
const tutorials = ref<{ [key: string]: TutorialListItem[] }>({
|
||||||
python: [],
|
python: [],
|
||||||
c: [],
|
c: [],
|
||||||
})
|
})
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const activeTab = ref("python")
|
const activeTab = ref("python")
|
||||||
|
|
||||||
const columns: DataTableColumn<Tutorial>[] = [
|
const columns: DataTableColumn<TutorialListItem>[] = [
|
||||||
{
|
{
|
||||||
title: "顺序",
|
title: "顺序",
|
||||||
key: "order",
|
key: "order",
|
||||||
@@ -21,29 +21,29 @@ const columns: DataTableColumn<Tutorial>[] = [
|
|||||||
{ title: "标题", key: "title", minWidth: 200 },
|
{ title: "标题", key: "title", minWidth: 200 },
|
||||||
{
|
{
|
||||||
title: "作者",
|
title: "作者",
|
||||||
key: "created_by",
|
key: "createdBy",
|
||||||
render: (row) => row.created_by?.username,
|
render: (row) => row.createdBy?.username,
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "created_at",
|
key: "createdAt",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => parseTime(row.created_at!, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createdAt!, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "更新时间",
|
title: "更新时间",
|
||||||
key: "updated_at",
|
key: "updatedAt",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => parseTime(row.updated_at!, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.updatedAt!, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "可见",
|
title: "可见",
|
||||||
key: "is_public",
|
key: "isPublic",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(NSwitch, {
|
h(NSwitch, {
|
||||||
value: row.is_public,
|
value: row.isPublic,
|
||||||
size: "small",
|
size: "small",
|
||||||
rubberBand: false,
|
rubberBand: false,
|
||||||
onUpdateValue: () => toggleVisible(row),
|
onUpdateValue: () => toggleVisible(row),
|
||||||
@@ -58,14 +58,14 @@ const columns: DataTableColumn<Tutorial>[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async function toggleVisible(tutorial: Tutorial) {
|
async function toggleVisible(tutorial: TutorialListItem) {
|
||||||
tutorial.is_public = !tutorial.is_public
|
tutorial.isPublic = !tutorial.isPublic
|
||||||
try {
|
try {
|
||||||
await setTutorialVisibility(tutorial.id, tutorial.is_public)
|
await setTutorialVisibility(tutorial.id, tutorial.isPublic)
|
||||||
message.success("更新成功")
|
message.success("更新成功")
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error(err.data)
|
message.error(err.data)
|
||||||
tutorial.is_public = !tutorial.is_public
|
tutorial.isPublic = !tutorial.isPublic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
async function banUser() {
|
async function banUser() {
|
||||||
props.user.is_disabled = !props.user.is_disabled
|
props.user.isDisabled = !props.user.isDisabled
|
||||||
await editUser(props.user)
|
await editUser(props.user)
|
||||||
emit("userBanned", props.user)
|
emit("userBanned", props.user)
|
||||||
}
|
}
|
||||||
@@ -40,10 +40,10 @@ async function banUser() {
|
|||||||
<n-button
|
<n-button
|
||||||
size="small"
|
size="small"
|
||||||
secondary
|
secondary
|
||||||
:type="props.user.is_disabled ? 'info' : 'error'"
|
:type="props.user.isDisabled ? 'info' : 'error'"
|
||||||
@click="banUser"
|
@click="banUser"
|
||||||
>
|
>
|
||||||
{{ props.user.is_disabled ? "解封" : "封号" }}
|
{{ props.user.isDisabled ? "解封" : "封号" }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-popconfirm @positive-click="$emit('deleteUser', [props.user.id])">
|
<n-popconfirm @positive-click="$emit('deleteUser', [props.user.id])">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
|
|||||||
@@ -9,30 +9,30 @@ interface Props {
|
|||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const isNotRegularUser = computed(
|
const isNotRegularUser = computed(
|
||||||
() => props.user.admin_type !== USER_TYPE.REGULAR_USER,
|
() => props.user.adminType !== USER_TYPE.REGULAR_USER,
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<n-tag v-if="props.user.is_disabled" type="error" size="small">
|
<n-tag v-if="props.user.isDisabled" type="error" size="small">
|
||||||
封号中
|
封号中
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="isNotRegularUser"
|
v-if="isNotRegularUser"
|
||||||
:type="getUserRole(props.user.admin_type).type"
|
:type="getUserRole(props.user.adminType).type"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
{{ getUserRole(props.user.admin_type).label }}
|
{{ getUserRole(props.user.adminType).label }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag
|
<n-tag
|
||||||
size="small"
|
size="small"
|
||||||
v-if="
|
v-if="
|
||||||
props.user.admin_type === USER_TYPE.STUDENT_ADMIN ||
|
props.user.adminType === USER_TYPE.STUDENT_ADMIN ||
|
||||||
props.user.admin_type === USER_TYPE.TEACHER_ADMIN
|
props.user.adminType === USER_TYPE.TEACHER_ADMIN
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
props.user.problem_permission === PROBLEM_PERMISSION.ALL
|
props.user.problemPermission === PROBLEM_PERMISSION.ALL
|
||||||
? "全部"
|
? "全部"
|
||||||
: "仅自己"
|
: "仅自己"
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -66,28 +66,29 @@ const columns: DataTableColumn<User>[] = [
|
|||||||
title: "密码",
|
title: "密码",
|
||||||
key: "raw_password",
|
key: "raw_password",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => h(TextCopy, () => row.raw_password),
|
render: (row) => h(TextCopy, () => row.rawPassword),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) =>
|
||||||
|
row.createTime ? parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss") : "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "上次登录",
|
title: "上次登录",
|
||||||
key: "last_login",
|
key: "last_login",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
row.last_login
|
row.lastLogin
|
||||||
? parseTime(row.last_login, "YYYY-MM-DD HH:mm:ss")
|
? parseTime(row.lastLogin, "YYYY-MM-DD HH:mm:ss")
|
||||||
: "从未登录",
|
: "从未登录",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "真名",
|
title: "真名",
|
||||||
key: "real_name",
|
key: "real_name",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => h(TextCopy, () => row.real_name),
|
render: (row) => h(TextCopy, () => row.realName),
|
||||||
},
|
},
|
||||||
{ title: "邮箱", key: "email", width: 200 },
|
{ title: "邮箱", key: "email", width: 200 },
|
||||||
{
|
{
|
||||||
@@ -145,8 +146,8 @@ async function onResetPassword(user: User) {
|
|||||||
const res = await resetPassword(user.id)
|
const res = await resetPassword(user.id)
|
||||||
message.success(`【${user.username}】的密码已重置成【${res.data}】`)
|
message.success(`【${user.username}】的密码已重置成【${res.data}】`)
|
||||||
users.value = users.value.map((it) => {
|
users.value = users.value.map((it) => {
|
||||||
if (it.id === user.id && user.admin_type === USER_TYPE.REGULAR_USER) {
|
if (it.id === user.id && user.adminType === USER_TYPE.REGULAR_USER) {
|
||||||
it.raw_password = res.data
|
it.rawPassword = res.data
|
||||||
}
|
}
|
||||||
return it
|
return it
|
||||||
})
|
})
|
||||||
@@ -155,7 +156,7 @@ async function onResetPassword(user: User) {
|
|||||||
async function onUserBanned(user: User) {
|
async function onUserBanned(user: User) {
|
||||||
users.value = users.value.map((it) => {
|
users.value = users.value.map((it) => {
|
||||||
if (it.id === user.id) {
|
if (it.id === user.id) {
|
||||||
it.is_disabled = user.is_disabled
|
it.isDisabled = user.isDisabled
|
||||||
}
|
}
|
||||||
return it
|
return it
|
||||||
})
|
})
|
||||||
@@ -166,14 +167,16 @@ function createNewUser() {
|
|||||||
userEditing.value = {
|
userEditing.value = {
|
||||||
id: 0,
|
id: 0,
|
||||||
username: "",
|
username: "",
|
||||||
real_name: "",
|
realName: "",
|
||||||
email: "",
|
email: "",
|
||||||
admin_type: "Student Admin",
|
adminType: "Student Admin",
|
||||||
problem_permission: "None",
|
problemPermission: "None",
|
||||||
create_time: new Date(),
|
createTime: null,
|
||||||
last_login: new Date(),
|
lastLogin: null,
|
||||||
open_api: false,
|
openApi: false,
|
||||||
is_disabled: false,
|
isDisabled: false,
|
||||||
|
rawPassword: null,
|
||||||
|
className: null,
|
||||||
password: "",
|
password: "",
|
||||||
}
|
}
|
||||||
password.value = ""
|
password.value = ""
|
||||||
@@ -204,8 +207,8 @@ async function handleEditUser() {
|
|||||||
[
|
[
|
||||||
userEditing.value.username,
|
userEditing.value.username,
|
||||||
password.value,
|
password.value,
|
||||||
userEditing.value.email,
|
userEditing.value.email ?? "",
|
||||||
userEditing.value.real_name,
|
userEditing.value.realName ?? "",
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
await importUsers(newUser)
|
await importUsers(newUser)
|
||||||
@@ -303,16 +306,16 @@ watch(() => [query.page, query.limit, query.type, query.orderBy], listUsers)
|
|||||||
<n-input v-model:value="userEditing.username" />
|
<n-input v-model:value="userEditing.username" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="1" label="真名">
|
<n-form-item-gi :span="1" label="真名">
|
||||||
<n-input v-model:value="userEditing.real_name" />
|
<n-input v-model:value="userEditing.realName" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi v-if="!create" :span="1" label="班级">
|
<n-form-item-gi v-if="!create" :span="1" label="班级">
|
||||||
<n-input v-model:value="userEditing.class_name" />
|
<n-input v-model:value="userEditing.className" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :span="1" label="邮箱">
|
<n-form-item-gi :span="1" label="邮箱">
|
||||||
<n-input v-model:value="userEditing.email" />
|
<n-input v-model:value="userEditing.email" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi v-if="!create" :span="1" label="类型">
|
<n-form-item-gi v-if="!create" :span="1" label="类型">
|
||||||
<n-select v-model:value="userEditing.admin_type" :options="options" />
|
<n-select v-model:value="userEditing.adminType" :options="options" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi
|
<n-form-item-gi
|
||||||
:span="1"
|
:span="1"
|
||||||
@@ -324,20 +327,20 @@ watch(() => [query.page, query.limit, query.type, query.orderBy], listUsers)
|
|||||||
<n-form-item-gi
|
<n-form-item-gi
|
||||||
v-if="
|
v-if="
|
||||||
!create &&
|
!create &&
|
||||||
(userEditing.admin_type === USER_TYPE.STUDENT_ADMIN ||
|
(userEditing.adminType === USER_TYPE.STUDENT_ADMIN ||
|
||||||
userEditing.admin_type === USER_TYPE.TEACHER_ADMIN)
|
userEditing.adminType === USER_TYPE.TEACHER_ADMIN)
|
||||||
"
|
"
|
||||||
:span="1"
|
:span="1"
|
||||||
label="出题权限"
|
label="出题权限"
|
||||||
>
|
>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="userEditing.problem_permission"
|
v-model:value="userEditing.problemPermission"
|
||||||
:options="problemPermissionOptions"
|
:options="problemPermissionOptions"
|
||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
|
|
||||||
<n-form-item-gi v-if="!create" :span="1" label="是否封禁">
|
<n-form-item-gi v-if="!create" :span="1" label="是否封禁">
|
||||||
<n-switch v-model:value="userEditing.is_disabled">封号</n-switch>
|
<n-switch v-model:value="userEditing.isDisabled">封号</n-switch>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<n-flex justify="end">
|
<n-flex justify="end">
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
import api2 from "utils/api2"
|
import api2 from "utils/api2"
|
||||||
import type {
|
import type {
|
||||||
Achievement,
|
AchievementList,
|
||||||
AchievementSummary,
|
AchievementSummary,
|
||||||
PendingAchievement,
|
PendingAchievement,
|
||||||
} from "utils/types"
|
} from "utils/types"
|
||||||
|
|
||||||
export function getAchievements(name?: string) {
|
export function getAchievements(name?: string) {
|
||||||
return api2
|
return api2.get<AchievementList>("achievements", {
|
||||||
.get<any>("achievements", { params: name ? { username: name } : {} })
|
params: name ? { username: name } : {},
|
||||||
.then((response) => ({
|
})
|
||||||
...response,
|
|
||||||
data: {
|
|
||||||
username: response.data.username,
|
|
||||||
achievements: response.data.achievements.map(
|
|
||||||
(item: any): Achievement => ({
|
|
||||||
...item,
|
|
||||||
unlock_time: item.unlockTime,
|
|
||||||
unlock_rate: item.unlockRate,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAchievementSummary(name?: string) {
|
export function getAchievementSummary(name?: string) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const masked = computed(
|
|||||||
|
|
||||||
// 获得率低于 5% 的加稀有闪光边框
|
// 获得率低于 5% 的加稀有闪光边框
|
||||||
const isRare = computed(
|
const isRare = computed(
|
||||||
() => props.achievement.unlock_rate > 0 && props.achievement.unlock_rate < 5,
|
() => props.achievement.unlockRate > 0 && props.achievement.unlockRate < 5,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 只有"越多越好"的成就画进度条。lte 类(如最短 AC 代码 ≤ 50 字符)
|
// 只有"越多越好"的成就画进度条。lte 类(如最短 AC 代码 ≤ 50 字符)
|
||||||
@@ -45,10 +45,10 @@ const percent = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const unlockDate = computed(() => {
|
const unlockDate = computed(() => {
|
||||||
const { unlock_time, backfilled } = props.achievement
|
const { unlockTime, backfilled } = props.achievement
|
||||||
// 补发的记录不显示具体日期:一次补发会给几百人盖上同一个时间戳
|
// 补发的记录不显示具体日期:一次补发会给几百人盖上同一个时间戳
|
||||||
if (backfilled || !unlock_time) return "已获得"
|
if (backfilled || !unlockTime) return "已获得"
|
||||||
return `${new Date(unlock_time).toLocaleDateString()} 获得`
|
return `${new Date(unlockTime).toLocaleDateString()} 获得`
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ const unlockDate = computed(() => {
|
|||||||
<template v-if="achievement.unlocked">
|
<template v-if="achievement.unlocked">
|
||||||
<n-text depth="3" class="nowrap">{{ unlockDate }}</n-text>
|
<n-text depth="3" class="nowrap">{{ unlockDate }}</n-text>
|
||||||
<n-text depth="3" class="nowrap">
|
<n-text depth="3" class="nowrap">
|
||||||
仅 {{ achievement.unlock_rate }}% 的人获得
|
仅 {{ achievement.unlockRate }}% 的人获得
|
||||||
</n-text>
|
</n-text>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ const unlockDate = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<n-text v-else depth="3" class="nowrap">
|
<n-text v-else depth="3" class="nowrap">
|
||||||
仅 {{ achievement.unlock_rate }}% 的人获得
|
仅 {{ achievement.unlockRate }}% 的人获得
|
||||||
</n-text>
|
</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-thing>
|
</n-thing>
|
||||||
|
|||||||
@@ -7,25 +7,10 @@ import type {
|
|||||||
Achievement,
|
Achievement,
|
||||||
AchievementRarity,
|
AchievementRarity,
|
||||||
AchievementSummary,
|
AchievementSummary,
|
||||||
|
UserBadge,
|
||||||
} from "utils/types"
|
} from "utils/types"
|
||||||
import AchievementCard from "./components/AchievementCard.vue"
|
import AchievementCard from "./components/AchievementCard.vue"
|
||||||
|
|
||||||
interface UserBadge {
|
|
||||||
id: number
|
|
||||||
earned_time: string
|
|
||||||
badge: {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
// 奖章来自哪个题单,接口在 UserBadgeSerializer 里带出来
|
|
||||||
problemset: {
|
|
||||||
id: number
|
|
||||||
title: string
|
|
||||||
} | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const name = computed(() => (route.query.name as string) || undefined)
|
const name = computed(() => (route.query.name as string) || undefined)
|
||||||
|
|
||||||
@@ -72,7 +57,7 @@ async function load() {
|
|||||||
// http 客户端返回 ApiResponse<T>,真实载荷在 .data 里
|
// http 客户端返回 ApiResponse<T>,真实载荷在 .data 里
|
||||||
achievements.value = list.data.achievements
|
achievements.value = list.data.achievements
|
||||||
summary.value = sum.data
|
summary.value = sum.data
|
||||||
badges.value = (badgeRes.data ?? []) as UserBadge[]
|
badges.value = badgeRes.data ?? []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const data = computed(() => {
|
|||||||
// 根据等级返回对应的颜色
|
// 根据等级返回对应的颜色
|
||||||
function getGradeColor(grade: Grade): string {
|
function getGradeColor(grade: Grade): string {
|
||||||
const colors: { [key in Grade]: string } = {
|
const colors: { [key in Grade]: string } = {
|
||||||
|
"": "#C9CDD4", // 无评级:后端在没有可用数据时下发空串
|
||||||
S: "#FF6384",
|
S: "#FF6384",
|
||||||
A: "#FFCE56",
|
A: "#FFCE56",
|
||||||
B: "#36A2EB",
|
B: "#36A2EB",
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ const data = computed<ChartData<"bar" | "line">>(() => {
|
|||||||
{
|
{
|
||||||
type: "bar",
|
type: "bar",
|
||||||
label: "完成题目数",
|
label: "完成题目数",
|
||||||
data: aiStore.durationData.map((duration) => duration.problem_count),
|
data: aiStore.durationData.map((duration) => duration.problemCount),
|
||||||
yAxisID: "y",
|
yAxisID: "y",
|
||||||
order: 2,
|
order: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "bar",
|
type: "bar",
|
||||||
label: "总提交次数",
|
label: "总提交次数",
|
||||||
data: aiStore.durationData.map((duration) => duration.submission_count),
|
data: aiStore.durationData.map((duration) => duration.submissionCount),
|
||||||
yAxisID: "y",
|
yAxisID: "y",
|
||||||
order: 2,
|
order: 2,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ const show = computed(() => {
|
|||||||
// 计算提交效率数据
|
// 计算提交效率数据
|
||||||
const efficiencyData = computed(() => {
|
const efficiencyData = computed(() => {
|
||||||
return aiStore.durationData.map((duration) => {
|
return aiStore.durationData.map((duration) => {
|
||||||
const problemCount = duration.problem_count || 0
|
const problemCount = duration.problemCount || 0
|
||||||
const submissionCount = duration.submission_count || 0
|
const submissionCount = duration.submissionCount || 0
|
||||||
|
|
||||||
// 计算效率:提交次数/完成题目数
|
// 计算效率:提交次数/完成题目数
|
||||||
// 值越接近1,说明一次AC率越高
|
// 值越接近1,说明一次AC率越高
|
||||||
|
|||||||
@@ -28,8 +28,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Grade } from "utils/types"
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
grade: "S" | "A" | "B" | "C"
|
// 空串是「无评级」,四张图都不渲染 —— 后端没有可用数据时会下发它
|
||||||
|
grade: Grade
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<span>你一共解决 </span>
|
<span>你一共解决 </span>
|
||||||
<b class="charming"> {{ aiStore.detailsData.solved.length }} </b>
|
<b class="charming"> {{ aiStore.detailsData.solved.length }} </b>
|
||||||
<span> 道题</span>
|
<span> 道题</span>
|
||||||
<span v-if="aiStore.detailsData.contest_count > 0">
|
<span v-if="aiStore.detailsData.contestCount > 0">
|
||||||
,并且参加
|
,并且参加
|
||||||
<b class="charming"> {{ aiStore.detailsData.contest_count }} </b>
|
<b class="charming"> {{ aiStore.detailsData.contestCount }} </b>
|
||||||
次比赛
|
次比赛
|
||||||
</span>
|
</span>
|
||||||
<span>,综合评价给到</span>
|
<span>,综合评价给到</span>
|
||||||
@@ -49,6 +49,7 @@ const durationLabel = computed(() => {
|
|||||||
|
|
||||||
const greeting = computed(() => {
|
const greeting = computed(() => {
|
||||||
return {
|
return {
|
||||||
|
"": "还没有足够的数据来评级",
|
||||||
S: "要不试试高难度题目?",
|
S: "要不试试高难度题目?",
|
||||||
A: "你很棒,继续保持!",
|
A: "你很棒,继续保持!",
|
||||||
B: "请再接再厉!",
|
B: "请再接再厉!",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const aiStore = useAIStore()
|
|||||||
|
|
||||||
const gradeOrder = ["C", "B", "A", "S"] as const
|
const gradeOrder = ["C", "B", "A", "S"] as const
|
||||||
const gradeColors: Record<Grade, string> = {
|
const gradeColors: Record<Grade, string> = {
|
||||||
|
"": "#C9CDD4", // 无评级:后端在没有可用数据时下发空串
|
||||||
C: "#95F204",
|
C: "#95F204",
|
||||||
B: "#36A2EB",
|
B: "#36A2EB",
|
||||||
A: "#FFCE56",
|
A: "#FFCE56",
|
||||||
@@ -74,7 +75,7 @@ const progressData = computed(() => {
|
|||||||
let totalProblems = 0 // 累计题目总数
|
let totalProblems = 0 // 累计题目总数
|
||||||
|
|
||||||
return aiStore.durationData.map((duration) => {
|
return aiStore.durationData.map((duration) => {
|
||||||
const problemCount = duration.problem_count || 0
|
const problemCount = duration.problemCount || 0
|
||||||
cumulativeCount += problemCount
|
cumulativeCount += problemCount
|
||||||
|
|
||||||
// 计算本期等级的权重值
|
// 计算本期等级的权重值
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ const rankDistribution = computed(() => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
aiStore.detailsData.solved.forEach((item) => {
|
aiStore.detailsData.solved.forEach((item) => {
|
||||||
const rank = item.period_rank
|
const rank = item.periodRank
|
||||||
const acCount = item.period_ac_count
|
const acCount = item.periodAcCount
|
||||||
|
|
||||||
if (rank && acCount && acCount > 0) {
|
if (rank && acCount && acCount > 0) {
|
||||||
const percentile = (rank / acCount) * 100
|
const percentile = (rank / acCount) * 100
|
||||||
@@ -52,7 +52,7 @@ const rankDistribution = computed(() => {
|
|||||||
if (rangeIndex !== -1) {
|
if (rangeIndex !== -1) {
|
||||||
distribution[rangeIndex].count++
|
distribution[rangeIndex].count++
|
||||||
distribution[rangeIndex].problems.push(
|
distribution[rangeIndex].problems.push(
|
||||||
`${item.problem.display_id}: ${item.problem.title}`,
|
`${item.problem.displayId}: ${item.problem.title}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,40 +58,40 @@ const columns: DataTableColumn<SolvedProblem>[] = [
|
|||||||
{
|
{
|
||||||
text: true,
|
text: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (row.problem.contest_id) {
|
if (row.problem.contestId) {
|
||||||
router.push(
|
router.push(
|
||||||
"/contest/" +
|
"/contest/" +
|
||||||
row.problem.contest_id +
|
row.problem.contestId +
|
||||||
"/problem/" +
|
"/problem/" +
|
||||||
row.problem.display_id,
|
row.problem.displayId,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
router.push("/problem/" + row.problem.display_id)
|
router.push("/problem/" + row.problem.displayId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
if (row.problem.contest_id) {
|
if (row.problem.contestId) {
|
||||||
return h(TagTitle, { problem: row.problem })
|
return h(TagTitle, { problem: row.problem })
|
||||||
} else {
|
} else {
|
||||||
return row.problem.display_id + " " + row.problem.title
|
return row.problem.displayId + " " + row.problem.title
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: () => (aiStore.detailsData.class_name ? "班级排名" : "全服排名"),
|
title: () => (aiStore.detailsData.className ? "班级排名" : "全服排名"),
|
||||||
key: "rank",
|
key: "rank",
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => row.rank + " / " + row.ac_count,
|
render: (row) => row.rank + " / " + row.acCount,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "同期排名",
|
title: "同期排名",
|
||||||
key: "period_rank",
|
key: "period_rank",
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => row.period_rank + " / " + row.period_ac_count,
|
render: (row) => row.periodRank + " / " + row.periodAcCount,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: () =>
|
title: () =>
|
||||||
@@ -128,10 +128,10 @@ const flowchartsColumns: DataTableColumn<FlowchartSummary>[] = [
|
|||||||
{
|
{
|
||||||
text: true,
|
text: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push("/problem/" + row.problem__id)
|
router.push("/problem/" + row.problemId)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
() => `${row.problem__id} ${row.problem_title}`,
|
() => `${row.problemId} ${row.problemTitle}`,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ title: "提交次数", key: "submission_count", width: 100, align: "center" },
|
{ title: "提交次数", key: "submission_count", width: 100, align: "center" },
|
||||||
@@ -140,14 +140,14 @@ const flowchartsColumns: DataTableColumn<FlowchartSummary>[] = [
|
|||||||
key: "best",
|
key: "best",
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => `${row.best_score} (${row.best_grade})`,
|
render: (row) => `${row.bestScore} (${row.bestGrade})`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "最新提交时间",
|
title: "最新提交时间",
|
||||||
key: "latest_submission_time",
|
key: "latest_submission_time",
|
||||||
width: 200,
|
width: 200,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => parseTime(row.latest_submission_time),
|
render: (row) => parseTime(row.latestSubmissionTime),
|
||||||
},
|
},
|
||||||
{ title: "平均分", key: "avg_score", width: 100, align: "center" },
|
{ title: "平均分", key: "avg_score", width: 100, align: "center" },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,19 +2,15 @@
|
|||||||
<n-flex vertical align="start">
|
<n-flex vertical align="start">
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<n-tag type="info" size="small" :bordered="false">比赛</n-tag>
|
<n-tag type="info" size="small" :bordered="false">比赛</n-tag>
|
||||||
<span>{{ problem.contest_title }}</span>
|
<span>{{ problem.contestTitle }}</span>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<span>{{ problem.display_id }} {{ problem.title }}</span>
|
<span>{{ problem.displayId }} {{ problem.title }}</span>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SolvedProblem } from "utils/types"
|
||||||
interface Props {
|
interface Props {
|
||||||
problem: {
|
problem: SolvedProblem["problem"]
|
||||||
title: string
|
|
||||||
display_id: string
|
|
||||||
contest_title: string
|
|
||||||
contest_id: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const activityMatrix = computed(() => {
|
|||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
aiStore.detailsData.solved.forEach((item) => {
|
aiStore.detailsData.solved.forEach((item) => {
|
||||||
const date = new Date(item.ac_time)
|
const date = new Date(item.acTime)
|
||||||
const weekday = date.getDay() // 0-6,0是周日
|
const weekday = date.getDay() // 0-6,0是周日
|
||||||
const hour = date.getHours() // 0-23
|
const hour = date.getHours() // 0-23
|
||||||
|
|
||||||
|
|||||||
@@ -33,15 +33,15 @@ const columns: DataTableColumn<Announcement>[] = [
|
|||||||
render: (row) => h(NTag, () => row.tag || "公告"),
|
render: (row) => h(NTag, () => row.tag || "公告"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "create_time",
|
key: "createTime",
|
||||||
title: renderTableTitle("发布时间", "fluent-emoji-flat:eight-oclock"),
|
title: renderTableTitle("发布时间", "fluent-emoji-flat:eight-oclock"),
|
||||||
render: (row) => parseTime(row.create_time),
|
render: (row) => parseTime(row.createTime),
|
||||||
width: 180,
|
width: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "username",
|
key: "username",
|
||||||
title: renderTableTitle("发布人", "streamline-emojis:ghost"),
|
title: renderTableTitle("发布人", "streamline-emojis:ghost"),
|
||||||
render: (row) => row.created_by.username,
|
render: (row) => row.createdBy.username,
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,15 +1,46 @@
|
|||||||
import {
|
import {
|
||||||
createSubmissionResponseSchema,
|
type AiAnalysisRecord,
|
||||||
|
type Contest as OjContest,
|
||||||
|
type ContestList,
|
||||||
|
type ActivityRankItem,
|
||||||
|
type ClassComparisonResponse,
|
||||||
|
type ClassRankItem,
|
||||||
|
type ClassUserRank,
|
||||||
|
type ContestRank,
|
||||||
|
type UserRank,
|
||||||
|
type ProblemRank,
|
||||||
|
type CreateSubmissionResponse,
|
||||||
|
type ProblemAuthor,
|
||||||
|
type ProblemListItem,
|
||||||
|
type YearlyAc,
|
||||||
|
type ProblemList,
|
||||||
|
type CreateFlowchartResponse,
|
||||||
|
type FlowchartCurrent,
|
||||||
|
type FlowchartDetail,
|
||||||
|
type FlowchartList,
|
||||||
|
type FlowchartSubmission,
|
||||||
|
type AiDetail,
|
||||||
|
type DurationData,
|
||||||
|
type HeatmapItem,
|
||||||
|
type LoginSummary,
|
||||||
|
type ProblemSet,
|
||||||
|
type ProblemSetBadge,
|
||||||
|
type ProblemSetList,
|
||||||
|
type ProblemSetProblem,
|
||||||
|
type ProblemSetProgressList,
|
||||||
|
type UserBadge,
|
||||||
problemDetailSchema,
|
problemDetailSchema,
|
||||||
submissionDetailSchema,
|
submissionDetailSchema,
|
||||||
type FlowchartStatistics,
|
type FlowchartStatistics,
|
||||||
type SubmissionStatistics,
|
type SubmissionStatistics,
|
||||||
} from "@oj2/contract"
|
} from "@oj2/contract"
|
||||||
import api2 from "utils/api2"
|
import api2 from "utils/api2"
|
||||||
import { legacyResponse, toLegacy } from "utils/legacy"
|
|
||||||
import type { ApiResponse } from "utils/http"
|
|
||||||
import { filterResult } from "oj/transforms"
|
import { filterResult } from "oj/transforms"
|
||||||
import type {
|
import type {
|
||||||
|
Announcement,
|
||||||
|
Profile,
|
||||||
|
Message,
|
||||||
|
SubmissionListItem,
|
||||||
Exercise,
|
Exercise,
|
||||||
Problem,
|
Problem,
|
||||||
ReactionKey,
|
ReactionKey,
|
||||||
@@ -18,97 +49,39 @@ import type {
|
|||||||
SubmissionListPayload,
|
SubmissionListPayload,
|
||||||
SubmitCodePayload,
|
SubmitCodePayload,
|
||||||
WebsiteConfig,
|
WebsiteConfig,
|
||||||
|
Tutorial,
|
||||||
} from "utils/types"
|
} from "utils/types"
|
||||||
|
|
||||||
function listProblem(value: any): Problem {
|
/**
|
||||||
return {
|
* 题目详情。走契约的 zod 解析,形状即契约 —— 之前这里手抄了一份 camel→snake 的
|
||||||
id: value.id,
|
* 键名映射,抄漏一个字段就是静默 undefined。
|
||||||
_id: value._id,
|
*/
|
||||||
title: value.title,
|
|
||||||
difficulty: value.difficulty,
|
|
||||||
submission_number: value.submissionNumber,
|
|
||||||
accepted_number: value.acceptedNumber,
|
|
||||||
created_by: toLegacy(value.createdBy),
|
|
||||||
tags: value.tags,
|
|
||||||
contest: value.contestId,
|
|
||||||
allow_flowchart: value.allowFlowchart,
|
|
||||||
show_flowchart: value.showFlowchart,
|
|
||||||
has_ast_rules: value.hasAstRules,
|
|
||||||
my_status: value.myStatus,
|
|
||||||
} as Problem
|
|
||||||
}
|
|
||||||
|
|
||||||
function detailProblem(value: unknown): Problem {
|
function detailProblem(value: unknown): Problem {
|
||||||
const problem = problemDetailSchema.parse(value)
|
return problemDetailSchema.parse(value) as Problem
|
||||||
return {
|
|
||||||
id: problem.id,
|
|
||||||
_id: problem._id,
|
|
||||||
title: problem.title,
|
|
||||||
description: problem.description,
|
|
||||||
input_description: problem.inputDescription,
|
|
||||||
output_description: problem.outputDescription,
|
|
||||||
samples: problem.samples,
|
|
||||||
hint: problem.hint ?? "",
|
|
||||||
languages: problem.languages,
|
|
||||||
template: problem.template,
|
|
||||||
create_time: problem.createTime,
|
|
||||||
last_update_time: problem.lastUpdateTime,
|
|
||||||
time_limit: problem.timeLimit,
|
|
||||||
memory_limit: problem.memoryLimit,
|
|
||||||
difficulty: problem.difficulty,
|
|
||||||
source: problem.source ?? "",
|
|
||||||
prompt: problem.prompt ?? "",
|
|
||||||
answers: [],
|
|
||||||
submission_number: problem.submissionNumber,
|
|
||||||
accepted_number: problem.acceptedNumber,
|
|
||||||
statistic_info: problem.statisticInfo,
|
|
||||||
share_submission: problem.shareSubmission,
|
|
||||||
contest: problem.contestId,
|
|
||||||
tags: problem.tags,
|
|
||||||
created_by: {
|
|
||||||
id: problem.createdBy.id,
|
|
||||||
username: problem.createdBy.username,
|
|
||||||
real_name: problem.createdBy.realName,
|
|
||||||
},
|
|
||||||
my_status: problem.myStatus,
|
|
||||||
my_failed_count: problem.myFailedCount,
|
|
||||||
visible: true,
|
|
||||||
allow_flowchart: problem.allowFlowchart,
|
|
||||||
show_flowchart: problem.showFlowchart,
|
|
||||||
mermaid_code: problem.mermaidCode ?? undefined,
|
|
||||||
flowchart_data: problem.flowchartData ?? undefined,
|
|
||||||
flowchart_hint: problem.flowchartHint ?? undefined,
|
|
||||||
sql_config: problem.sqlConfig as Problem["sql_config"],
|
|
||||||
sql_display: problem.sqlDisplay as Problem["sql_display"],
|
|
||||||
} as Problem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebsiteConfig() {
|
export function getWebsiteConfig() {
|
||||||
return legacyResponse<WebsiteConfig>(api2.get("site"))
|
return api2.get<WebsiteConfig>("site")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProblemList(
|
export async function getProblemList(
|
||||||
offset = 0,
|
offset = 0,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
searchParams: any = {},
|
searchParams: Record<string, unknown> = {},
|
||||||
) {
|
) {
|
||||||
const res = await api2.get<{ results: any[]; total: number }>("problems", {
|
const res = await api2.get<ProblemList>("problems", {
|
||||||
params: { paging: true, offset, limit, ...searchParams },
|
params: { paging: true, offset, limit, ...searchParams },
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
results: res.data.results.map(listProblem).map(filterResult),
|
results: res.data.results.map(filterResult),
|
||||||
total: res.data.total,
|
total: res.data.total,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthors(all = false) {
|
export function getAuthors(all = false) {
|
||||||
return legacyResponse(
|
return api2.get<ProblemAuthor[]>("problem-authors", {
|
||||||
api2.get("problem-authors", {
|
params: { all: all ? "1" : "0" },
|
||||||
params: {
|
})
|
||||||
all: all ? "1" : "0",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRandomProblemID() {
|
export function getRandomProblemID() {
|
||||||
@@ -131,41 +104,14 @@ export async function getSubmission(id: string) {
|
|||||||
const response = await api2.get<unknown>(
|
const response = await api2.get<unknown>(
|
||||||
`submissions/${encodeURIComponent(id)}`,
|
`submissions/${encodeURIComponent(id)}`,
|
||||||
)
|
)
|
||||||
const submission = submissionDetailSchema.parse(response.data)
|
|
||||||
return {
|
return {
|
||||||
error: null,
|
error: null,
|
||||||
data: {
|
data: submissionDetailSchema.parse(response.data) as Submission,
|
||||||
id: submission.id,
|
|
||||||
create_time: submission.createTime,
|
|
||||||
user_id: submission.userId,
|
|
||||||
username: submission.username,
|
|
||||||
code: submission.code,
|
|
||||||
result: submission.result,
|
|
||||||
info: submission.info,
|
|
||||||
language: submission.language,
|
|
||||||
shared: submission.shared,
|
|
||||||
show_link: submission.showLink,
|
|
||||||
statistic_info: submission.statisticInfo,
|
|
||||||
ip: submission.ip,
|
|
||||||
contest: submission.contestId,
|
|
||||||
problem: submission.problemId,
|
|
||||||
can_unshare: submission.canUnshare,
|
|
||||||
} as Submission,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitCode(data: SubmitCodePayload) {
|
export function submitCode(data: SubmitCodePayload) {
|
||||||
const response = await api2.post<unknown>("submissions", {
|
return api2.post<CreateSubmissionResponse>("submissions", data)
|
||||||
problemId: data.problem_id,
|
|
||||||
language: data.language,
|
|
||||||
code: data.code,
|
|
||||||
contestId: data.contest_id,
|
|
||||||
})
|
|
||||||
const created = createSubmissionResponseSchema.parse(response.data)
|
|
||||||
return {
|
|
||||||
error: null,
|
|
||||||
data: { submission_id: created.submissionId },
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCode(data: { code: string; language: string }) {
|
export function formatCode(data: { code: string; language: string }) {
|
||||||
@@ -182,30 +128,23 @@ export function formatCode(data: { code: string; language: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSubmissions(params: Partial<SubmissionListPayload>) {
|
export function getSubmissions(params: Partial<SubmissionListPayload>) {
|
||||||
const endpoint = params.contest_id
|
const endpoint = params.contestId
|
||||||
? `contests/${encodeURIComponent(params.contest_id)}/submissions`
|
? `contests/${encodeURIComponent(params.contestId)}/submissions`
|
||||||
: "submissions"
|
: "submissions"
|
||||||
return legacyResponse(
|
// 契约里 language 是 z.string()(语言是配置项,随时可能加,收紧成枚举会让
|
||||||
api2.get(endpoint, {
|
// 新加的语言在后端 parse 时直接抛),前端在这一处收窄成 LANGUAGE
|
||||||
params: {
|
return api2.get<{ results: SubmissionListItem[]; total: number }>(endpoint, {
|
||||||
...params,
|
// contestId 走的是路径,page 只有前端分页器用
|
||||||
problemId: params.problem_id,
|
params: { ...params, contestId: undefined, page: undefined },
|
||||||
contest_id: undefined,
|
})
|
||||||
problem_id: undefined,
|
|
||||||
page: undefined,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRankOfProblem(problem_id: string) {
|
export function getRankOfProblem(problemId: string) {
|
||||||
return legacyResponse(
|
return api2.get<ProblemRank>(`problems/${encodeURIComponent(problemId)}/rank`)
|
||||||
api2.get(`problems/${encodeURIComponent(problem_id)}/rank`),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTodaySubmissionCount(language?: string) {
|
export function getTodaySubmissionCount(language?: string) {
|
||||||
return api2.get("submissions/today-count", { params: { language } })
|
return api2.get<number>("submissions/today-count", { params: { language } })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adminRejudge(id: string) {
|
export function adminRejudge(id: string) {
|
||||||
@@ -228,25 +167,19 @@ export function getRank(
|
|||||||
n: number,
|
n: number,
|
||||||
username?: string,
|
username?: string,
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.get<UserRank>("rankings/users", {
|
||||||
api2.get("rankings/users", {
|
params: { offset, limit, username, top: n },
|
||||||
params: { offset, limit, username, top: n },
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getActivityRank(start: string) {
|
export function getActivityRank(start: string) {
|
||||||
return api2.get("rankings/activity", {
|
return api2.get<ActivityRankItem[]>("rankings/activity", {
|
||||||
params: { start },
|
params: { start },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClassRank(grade?: number | null) {
|
export function getClassRank(grade?: number | null) {
|
||||||
return legacyResponse(
|
return api2.get<ClassRankItem[]>("rankings/classes", { params: { grade } })
|
||||||
api2.get("rankings/classes", {
|
|
||||||
params: { grade },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserClassRank(
|
export function getUserClassRank(
|
||||||
@@ -254,9 +187,9 @@ export function getUserClassRank(
|
|||||||
offset?: number,
|
offset?: number,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.get<ClassUserRank>("me/class-rank", {
|
||||||
api2.get("me/class-rank", { params: { scope, offset, limit } }),
|
params: { scope, offset, limit },
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClassPK(
|
export function getClassPK(
|
||||||
@@ -264,16 +197,11 @@ export function getClassPK(
|
|||||||
startTime?: string,
|
startTime?: string,
|
||||||
endTime?: string,
|
endTime?: string,
|
||||||
) {
|
) {
|
||||||
const payload: any = {
|
return api2.post<ClassComparisonResponse>("classes/comparison", {
|
||||||
classNames,
|
classNames,
|
||||||
}
|
...(startTime ? { startTime } : {}),
|
||||||
if (startTime) {
|
...(endTime ? { endTime } : {}),
|
||||||
payload.startTime = startTime
|
})
|
||||||
}
|
|
||||||
if (endTime) {
|
|
||||||
payload.endTime = endTime
|
|
||||||
}
|
|
||||||
return legacyResponse(api2.post("classes/comparison", payload))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContestList(query: {
|
export function getContestList(query: {
|
||||||
@@ -283,11 +211,11 @@ export function getContestList(query: {
|
|||||||
status: string
|
status: string
|
||||||
tag: string
|
tag: string
|
||||||
}) {
|
}) {
|
||||||
return legacyResponse(api2.get("contests", { params: query }))
|
return api2.get<ContestList>("contests", { params: query })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContest(id: string) {
|
export function getContest(id: string) {
|
||||||
return legacyResponse(api2.get(`contests/${encodeURIComponent(id)}`))
|
return api2.get<OjContest>(`contests/${encodeURIComponent(id)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContestAccess(id: string) {
|
export function getContestAccess(id: string) {
|
||||||
@@ -301,30 +229,20 @@ export function checkContestPassword(contestID: string, password: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getContestProblems(contestID: string) {
|
export async function getContestProblems(contestID: string) {
|
||||||
const res = await api2.get<any[]>(
|
const res = await api2.get<ProblemListItem[]>(
|
||||||
`contests/${encodeURIComponent(contestID)}/problems`,
|
`contests/${encodeURIComponent(contestID)}/problems`,
|
||||||
)
|
)
|
||||||
return res.data.map(listProblem).map(filterResult)
|
return res.data.map(filterResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContestRank(
|
export function getContestRank(
|
||||||
contestID: string,
|
contestID: string,
|
||||||
query: { limit: number; offset: number },
|
query: { limit: number; offset: number },
|
||||||
) {
|
) {
|
||||||
return legacyResponse<any>(
|
return api2.get<ContestRank>(
|
||||||
api2.get(`contests/${encodeURIComponent(contestID)}/rank`, {
|
`contests/${encodeURIComponent(contestID)}/rank`,
|
||||||
params: query,
|
{ params: query },
|
||||||
}),
|
)
|
||||||
).then((response) => ({
|
|
||||||
...response,
|
|
||||||
data: {
|
|
||||||
...response.data,
|
|
||||||
results: response.data.results.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
contest: item.contest_id,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadAvatar(file: File) {
|
export function uploadAvatar(file: File) {
|
||||||
@@ -335,23 +253,18 @@ export function uploadAvatar(file: File) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateProfile(data: { real_name: string; mood: string }) {
|
export function updateProfile(data: { realName: string; mood: string }) {
|
||||||
return legacyResponse(
|
return api2.put<Profile>("me/profile", data)
|
||||||
api2.put("me/profile", {
|
|
||||||
realName: data.real_name,
|
|
||||||
mood: data.mood,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAnnouncementList(offset = 0, limit = 10) {
|
export function getAnnouncementList(offset = 0, limit = 10) {
|
||||||
return legacyResponse(
|
return api2.get<{ results: Announcement[]; total: number }>("announcements", {
|
||||||
api2.get("announcements", { params: { limit, offset } }),
|
params: { limit, offset },
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAnnouncement(id: number) {
|
export function getAnnouncement(id: number) {
|
||||||
return legacyResponse(api2.get(`announcements/${id}`))
|
return api2.get<Announcement>(`announcements/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMessage(data: {
|
export function createMessage(data: {
|
||||||
@@ -367,7 +280,10 @@ export function createMessage(data: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getMessageList(offset = 0, limit = 10) {
|
export function getMessageList(offset = 0, limit = 10) {
|
||||||
return legacyResponse(api2.get("messages", { params: { limit, offset } }))
|
// language 的收窄同 getSubmissions,见那里的说明
|
||||||
|
return api2.get<{ results: Message[]; total: number }>("messages", {
|
||||||
|
params: { limit, offset },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReaction(problemID: number) {
|
export function getReaction(problemID: number) {
|
||||||
@@ -388,7 +304,7 @@ export function getMetrics(userid: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getTutorial(id: number) {
|
export function getTutorial(id: number) {
|
||||||
return legacyResponse(api2.get(`tutorials/${id}`))
|
return api2.get<Tutorial>(`tutorials/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTutorials(type: "python" | "c") {
|
export function getTutorials(type: "python" | "c") {
|
||||||
@@ -396,19 +312,7 @@ export function getTutorials(type: "python" | "c") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAIDetailData(start: string, end: string, username?: string) {
|
export function getAIDetailData(start: string, end: string, username?: string) {
|
||||||
return legacyResponse<any>(
|
return api2.get<AiDetail>("ai/detail", { params: { start, end, username } })
|
||||||
api2.get("ai/detail", { params: { start, end, username } }),
|
|
||||||
).then((response) => ({
|
|
||||||
...response,
|
|
||||||
data: {
|
|
||||||
...response.data,
|
|
||||||
flowcharts:
|
|
||||||
response.data.flowcharts?.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
problem__id: item.problem_id,
|
|
||||||
})) ?? [],
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAIDurationData(
|
export function getAIDurationData(
|
||||||
@@ -416,95 +320,68 @@ export function getAIDurationData(
|
|||||||
duration: string,
|
duration: string,
|
||||||
username?: string,
|
username?: string,
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.get<DurationData[]>("ai/duration", {
|
||||||
api2.get("ai/duration", { params: { end, duration, username } }),
|
params: { end, duration, username },
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAIHeatmapData(username?: string) {
|
export function getAIHeatmapData(username?: string) {
|
||||||
return api2.get("ai/heatmap", { params: username ? { username } : {} })
|
return api2.get<HeatmapItem[]>("ai/heatmap", {
|
||||||
|
params: username ? { username } : {},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAILoginSummary() {
|
export function getAILoginSummary() {
|
||||||
return legacyResponse(api2.get("ai/login-summary"))
|
return api2.get<LoginSummary>("ai/login-summary")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAIPinnedReport() {
|
export function getAIPinnedReport() {
|
||||||
return legacyResponse(api2.get("ai/pinned"))
|
return api2.get<AiAnalysisRecord | null>("ai/pinned")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 相似题目推荐 ====================
|
// ==================== 相似题目推荐 ====================
|
||||||
|
|
||||||
export function getSimilarProblems(problemId: string) {
|
export function getSimilarProblems(problemId: string) {
|
||||||
return api2
|
return api2
|
||||||
.get<any[]>(`problems/${encodeURIComponent(problemId)}/similar`)
|
.get<ProblemListItem[]>(`problems/${encodeURIComponent(problemId)}/similar`)
|
||||||
.then((response) => ({
|
.then((response) => ({
|
||||||
...response,
|
...response,
|
||||||
data: response.data.map(listProblem).map(filterResult),
|
data: response.data.map(filterResult),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YearlyACData {
|
export type { YearlyAc as YearlyACData } from "@oj2/contract"
|
||||||
year: number
|
|
||||||
total: number
|
|
||||||
accepted: number
|
|
||||||
ac_rate: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProblemYearlyAC(problemId: string) {
|
export function getProblemYearlyAC(problemId: string) {
|
||||||
return legacyResponse<YearlyACData[]>(
|
return api2.get<YearlyAc[]>(
|
||||||
api2.get(`problems/${encodeURIComponent(problemId)}/yearly-ac`),
|
`problems/${encodeURIComponent(problemId)}/yearly-ac`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 流程图相关API ====================
|
// ==================== 流程图相关API ====================
|
||||||
|
|
||||||
export function submitFlowchart(data: {
|
export function submitFlowchart(data: {
|
||||||
problem_id: number
|
problemId: number
|
||||||
mermaid_code: string
|
mermaidCode: string
|
||||||
flowchart_data: any // 这个是压缩之后的,元数据太长了
|
flowchartData: Record<string, unknown> // 压缩之后的,元数据太长了
|
||||||
}) {
|
}) {
|
||||||
return legacyResponse(
|
return api2.post<CreateFlowchartResponse>("flowcharts", data)
|
||||||
api2.post("flowcharts", {
|
|
||||||
problemId: data.problem_id,
|
|
||||||
mermaidCode: data.mermaid_code,
|
|
||||||
flowchartData: data.flowchart_data,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyFlowchart(value: unknown) {
|
export function getFlowchartSubmission(id: string) {
|
||||||
const item = toLegacy<any>(value)
|
return api2.get<FlowchartSubmission>(`flowcharts/${encodeURIComponent(id)}`)
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
user: item.user_id ?? 0,
|
|
||||||
problem: item.problem_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFlowchartSubmission(id: string) {
|
|
||||||
const response = await api2.get(`flowcharts/${encodeURIComponent(id)}`)
|
|
||||||
return { ...response, data: legacyFlowchart(response.data) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFlowchartSubmissions(params: {
|
export function getFlowchartSubmissions(params: {
|
||||||
username?: string
|
username?: string
|
||||||
problem_id?: string
|
problemId?: string
|
||||||
myself?: string
|
myself?: string
|
||||||
offset?: number
|
offset?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
today?: string
|
today?: string
|
||||||
grade?: string
|
grade?: string
|
||||||
}) {
|
}) {
|
||||||
return legacyResponse<any>(
|
return api2.get<FlowchartList>("flowcharts", { params })
|
||||||
api2.get("flowcharts", {
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
problemId: params.problem_id,
|
|
||||||
problem_id: undefined,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFlowchartStatistics(
|
export function getFlowchartStatistics(
|
||||||
@@ -518,32 +395,19 @@ export function getFlowchartStatistics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function retryFlowchartSubmission(submissionId: string) {
|
export function retryFlowchartSubmission(submissionId: string) {
|
||||||
return legacyResponse(
|
return api2.post<{ status: string }>(
|
||||||
api2.post(`flowcharts/${encodeURIComponent(submissionId)}/retry`),
|
`flowcharts/${encodeURIComponent(submissionId)}/retry`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentProblemFlowchartSubmission(problemId: number) {
|
export function getCurrentProblemFlowchartSubmission(problemId: number) {
|
||||||
return api2.get(`problems/${problemId}/flowchart/current`)
|
return api2.get<FlowchartCurrent>(`problems/${problemId}/flowchart/current`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFlowchartSubmissionDetail(
|
export function getFlowchartSubmissionDetail(problemId: number, page = 0) {
|
||||||
problemId: number,
|
return api2.get<FlowchartDetail>(`problems/${problemId}/flowchart/history`, {
|
||||||
page = 0,
|
params: { page },
|
||||||
) {
|
})
|
||||||
const response = await api2.get<any>(
|
|
||||||
`problems/${problemId}/flowchart/history`,
|
|
||||||
{ params: { page } },
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
...response,
|
|
||||||
data: {
|
|
||||||
...response.data,
|
|
||||||
submission: response.data.submission
|
|
||||||
? legacyFlowchart(response.data.submission)
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 题单相关API ====================
|
// ==================== 题单相关API ====================
|
||||||
@@ -555,64 +419,17 @@ export function getProblemSetList(
|
|||||||
difficulty = "",
|
difficulty = "",
|
||||||
status = "",
|
status = "",
|
||||||
) {
|
) {
|
||||||
return legacyResponse<any>(
|
return api2.get<ProblemSetList>("problem-sets", {
|
||||||
api2.get("problem-sets", {
|
params: { offset, limit, keyword, difficulty, status },
|
||||||
params: {
|
})
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
keyword,
|
|
||||||
difficulty,
|
|
||||||
status,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).then(mapProblemSetResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProblemSetDetail(id: number) {
|
export function getProblemSetDetail(id: number) {
|
||||||
return legacyResponse<any>(api2.get(`problem-sets/${id}`)).then(
|
return api2.get<ProblemSet>(`problem-sets/${id}`)
|
||||||
(response) => ({
|
|
||||||
...response,
|
|
||||||
data: legacyProblemSet(response.data),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyBadge(value: any) {
|
export function getProblemSetProblems(problemSetId: number) {
|
||||||
return { ...value, problemset: value.problemset_id }
|
return api2.get<ProblemSetProblem[]>(`problem-sets/${problemSetId}/problems`)
|
||||||
}
|
|
||||||
|
|
||||||
function legacyProblemSet(value: any) {
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
badges: value.badges?.map(legacyBadge),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapProblemSetResponse(response: ApiResponse<any>) {
|
|
||||||
return {
|
|
||||||
...response,
|
|
||||||
data: {
|
|
||||||
...response.data,
|
|
||||||
results: response.data.results.map(legacyProblemSet),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProblemSetProblems(problemSetId: number) {
|
|
||||||
const response = await legacyResponse<any[]>(
|
|
||||||
api2.get(`problem-sets/${problemSetId}/problems`),
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
...response,
|
|
||||||
data: response.data.map((item) => ({
|
|
||||||
...item,
|
|
||||||
problemset: item.problemset_id,
|
|
||||||
problem: {
|
|
||||||
...item.problem,
|
|
||||||
contest: item.problem.contest_id,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function joinProblemSet(problemSetId: number) {
|
export function joinProblemSet(problemSetId: number) {
|
||||||
@@ -624,57 +441,35 @@ export function updateProblemSetProgress(
|
|||||||
problemId: number,
|
problemId: number,
|
||||||
submissionId: string,
|
submissionId: string,
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.put("problem-set-progress", {
|
||||||
api2.put("problem-set-progress", {
|
problemSetId,
|
||||||
problemSetId,
|
problemId,
|
||||||
problemId,
|
submissionId,
|
||||||
submissionId,
|
})
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
export function getUserBadges(username?: string) {
|
||||||
|
return api2.get<UserBadge[]>(
|
||||||
|
`users/${encodeURIComponent(username ?? "me")}/badges`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户徽章列表
|
export function getProblemSetBadges(problemSetId: number) {
|
||||||
export async function getUserBadges(username?: string) {
|
return api2.get<ProblemSetBadge[]>(`problem-sets/${problemSetId}/badges`)
|
||||||
const response = await legacyResponse<any[]>(
|
|
||||||
api2.get(`users/${encodeURIComponent(username ?? "me")}/badges`),
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
...response,
|
|
||||||
data: response.data.map((item) => ({
|
|
||||||
...item,
|
|
||||||
user: item.user_id,
|
|
||||||
badge: legacyBadge(item.badge),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取题单徽章列表
|
|
||||||
export async function getProblemSetBadges(problemSetId: number) {
|
|
||||||
const response = await legacyResponse<any[]>(
|
|
||||||
api2.get(`problem-sets/${problemSetId}/badges`),
|
|
||||||
)
|
|
||||||
return { ...response, data: response.data.map(legacyBadge) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取题单用户进度列表
|
|
||||||
export function getProblemSetUserProgress(
|
export function getProblemSetUserProgress(
|
||||||
problemSetId: number,
|
problemSetId: number,
|
||||||
params?: {
|
params?: {
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
class_name?: string
|
className?: string
|
||||||
completion_status?: "" | "completed" | "in_progress" | "not_started"
|
completionStatus?: "" | "completed" | "in_progress" | "not_started"
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return legacyResponse(
|
return api2.get<ProblemSetProgressList>(
|
||||||
api2.get(`problem-sets/${problemSetId}/user-progress`, {
|
`problem-sets/${problemSetId}/user-progress`,
|
||||||
params: {
|
{ params },
|
||||||
limit: params?.limit,
|
|
||||||
offset: params?.offset,
|
|
||||||
className: params?.class_name,
|
|
||||||
completionStatus: params?.completion_status,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ClassComparison } from "utils/types"
|
||||||
import { h } from "vue"
|
import { h } from "vue"
|
||||||
import { formatISO, sub, type Duration } from "date-fns"
|
import { formatISO, sub, type Duration } from "date-fns"
|
||||||
import { getClassPK } from "oj/api"
|
import { getClassPK } from "oj/api"
|
||||||
@@ -46,32 +47,6 @@ const { isTeacherOrAbove } = useUserStore()
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
interface ClassComparison {
|
|
||||||
class_name: string
|
|
||||||
user_count: number
|
|
||||||
total_ac: number
|
|
||||||
total_submission: number
|
|
||||||
avg_ac: number
|
|
||||||
median_ac: number
|
|
||||||
q1_ac: number
|
|
||||||
q3_ac: number
|
|
||||||
iqr: number
|
|
||||||
std_dev: number
|
|
||||||
top10_avg: number
|
|
||||||
middle80_avg: number
|
|
||||||
bottom10_avg: number
|
|
||||||
excellent_rate: number
|
|
||||||
pass_rate: number
|
|
||||||
active_rate: number
|
|
||||||
ac_rate: number
|
|
||||||
composite_score: number
|
|
||||||
recent_total_ac?: number
|
|
||||||
recent_avg_ac?: number
|
|
||||||
recent_median_ac?: number
|
|
||||||
recent_top10_avg?: number
|
|
||||||
recent_active_count?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedClasses = ref<string[]>([])
|
const selectedClasses = ref<string[]>([])
|
||||||
const comparisons = ref<ClassComparison[]>([])
|
const comparisons = ref<ClassComparison[]>([])
|
||||||
const duration = ref<string>("")
|
const duration = ref<string>("")
|
||||||
@@ -129,7 +104,7 @@ function getTimeRange(): {
|
|||||||
|
|
||||||
const classOptions = computed(() => {
|
const classOptions = computed(() => {
|
||||||
return (
|
return (
|
||||||
configStore.config?.class_list.map((item) => ({
|
configStore.config?.classList.map((item) => ({
|
||||||
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
||||||
value: item,
|
value: item,
|
||||||
})) ?? []
|
})) ?? []
|
||||||
@@ -148,7 +123,7 @@ async function compare() {
|
|||||||
|
|
||||||
const res = await getClassPK(selectedClasses.value, startTime, endTime)
|
const res = await getClassPK(selectedClasses.value, startTime, endTime)
|
||||||
comparisons.value = res.data.comparisons
|
comparisons.value = res.data.comparisons
|
||||||
hasTimeRange.value = res.data.has_time_range || false
|
hasTimeRange.value = res.data.hasTimeRange || false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error("获取数据失败")
|
message.error("获取数据失败")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -252,11 +227,11 @@ function getClassColor(index: number) {
|
|||||||
const compositeScoreChartData = computed(() => {
|
const compositeScoreChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "综合分",
|
label: "综合分",
|
||||||
data: comparisons.value.map((c) => c.composite_score),
|
data: comparisons.value.map((c) => c.compositeScore),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -270,11 +245,11 @@ const compositeScoreChartData = computed(() => {
|
|||||||
const totalAcChartData = computed(() => {
|
const totalAcChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "总AC数",
|
label: "总AC数",
|
||||||
data: comparisons.value.map((c) => c.total_ac),
|
data: comparisons.value.map((c) => c.totalAc),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -288,11 +263,11 @@ const totalAcChartData = computed(() => {
|
|||||||
const avgAcChartData = computed(() => {
|
const avgAcChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "平均AC数",
|
label: "平均AC数",
|
||||||
data: comparisons.value.map((c) => c.avg_ac),
|
data: comparisons.value.map((c) => c.avgAc),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -306,11 +281,11 @@ const avgAcChartData = computed(() => {
|
|||||||
const medianAcChartData = computed(() => {
|
const medianAcChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "中位数AC数",
|
label: "中位数AC数",
|
||||||
data: comparisons.value.map((c) => c.median_ac),
|
data: comparisons.value.map((c) => c.medianAc),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -324,11 +299,11 @@ const medianAcChartData = computed(() => {
|
|||||||
const excellentRateChartData = computed(() => {
|
const excellentRateChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "优秀率",
|
label: "优秀率",
|
||||||
data: comparisons.value.map((c) => c.excellent_rate),
|
data: comparisons.value.map((c) => c.excellentRate),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -342,11 +317,11 @@ const excellentRateChartData = computed(() => {
|
|||||||
const passRateChartData = computed(() => {
|
const passRateChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "及格率",
|
label: "及格率",
|
||||||
data: comparisons.value.map((c) => c.pass_rate),
|
data: comparisons.value.map((c) => c.passRate),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -360,11 +335,11 @@ const passRateChartData = computed(() => {
|
|||||||
const activeRateChartData = computed(() => {
|
const activeRateChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "参与度",
|
label: "参与度",
|
||||||
data: comparisons.value.map((c) => c.active_rate),
|
data: comparisons.value.map((c) => c.activeRate),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -378,11 +353,11 @@ const activeRateChartData = computed(() => {
|
|||||||
const top10AvgChartData = computed(() => {
|
const top10AvgChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "前10%平均",
|
label: "前10%平均",
|
||||||
data: comparisons.value.map((c) => c.top10_avg),
|
data: comparisons.value.map((c) => c.top10Avg),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -396,11 +371,11 @@ const top10AvgChartData = computed(() => {
|
|||||||
const bottom10AvgChartData = computed(() => {
|
const bottom10AvgChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "后10%平均",
|
label: "后10%平均",
|
||||||
data: comparisons.value.map((c) => c.bottom10_avg),
|
data: comparisons.value.map((c) => c.bottom10Avg),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -414,11 +389,11 @@ const bottom10AvgChartData = computed(() => {
|
|||||||
const middle80AvgChartData = computed(() => {
|
const middle80AvgChartData = computed(() => {
|
||||||
if (comparisons.value.length === 0) return null
|
if (comparisons.value.length === 0) return null
|
||||||
|
|
||||||
const labels = comparisons.value.map((c) => c.class_name)
|
const labels = comparisons.value.map((c) => c.className)
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "中间80%均值",
|
label: "中间80%均值",
|
||||||
data: comparisons.value.map((c) => c.middle80_avg),
|
data: comparisons.value.map((c) => c.middle80Avg),
|
||||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -449,18 +424,18 @@ const radarChartData = computed(() => {
|
|||||||
|
|
||||||
// 计算每个指标的最大最小值
|
// 计算每个指标的最大最小值
|
||||||
const maxValues = [
|
const maxValues = [
|
||||||
Math.max(...comparisons.value.map((c) => c.total_ac)),
|
Math.max(...comparisons.value.map((c) => c.totalAc)),
|
||||||
Math.max(...comparisons.value.map((c) => c.avg_ac)),
|
Math.max(...comparisons.value.map((c) => c.avgAc)),
|
||||||
Math.max(...comparisons.value.map((c) => c.median_ac)),
|
Math.max(...comparisons.value.map((c) => c.medianAc)),
|
||||||
100, // 优秀率最大值
|
100, // 优秀率最大值
|
||||||
100, // 及格率最大值
|
100, // 及格率最大值
|
||||||
100, // 参与度最大值
|
100, // 参与度最大值
|
||||||
]
|
]
|
||||||
|
|
||||||
const minValues = [
|
const minValues = [
|
||||||
Math.min(...comparisons.value.map((c) => c.total_ac)),
|
Math.min(...comparisons.value.map((c) => c.totalAc)),
|
||||||
Math.min(...comparisons.value.map((c) => c.avg_ac)),
|
Math.min(...comparisons.value.map((c) => c.avgAc)),
|
||||||
Math.min(...comparisons.value.map((c) => c.median_ac)),
|
Math.min(...comparisons.value.map((c) => c.medianAc)),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -469,22 +444,22 @@ const radarChartData = computed(() => {
|
|||||||
const datasets = comparisons.value.map((c, index) => {
|
const datasets = comparisons.value.map((c, index) => {
|
||||||
const color = getClassColor(index)
|
const color = getClassColor(index)
|
||||||
const rawData = [
|
const rawData = [
|
||||||
c.total_ac,
|
c.totalAc,
|
||||||
c.avg_ac,
|
c.avgAc,
|
||||||
c.median_ac,
|
c.medianAc,
|
||||||
c.excellent_rate,
|
c.excellentRate,
|
||||||
c.pass_rate,
|
c.passRate,
|
||||||
c.active_rate,
|
c.activeRate,
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
label: c.class_name,
|
label: c.className,
|
||||||
data: [
|
data: [
|
||||||
normalize(c.total_ac, maxValues[0], minValues[0]),
|
normalize(c.totalAc, maxValues[0], minValues[0]),
|
||||||
normalize(c.avg_ac, maxValues[1], minValues[1]),
|
normalize(c.avgAc, maxValues[1], minValues[1]),
|
||||||
normalize(c.median_ac, maxValues[2], minValues[2]),
|
normalize(c.medianAc, maxValues[2], minValues[2]),
|
||||||
c.excellent_rate,
|
c.excellentRate,
|
||||||
c.pass_rate,
|
c.passRate,
|
||||||
c.active_rate,
|
c.activeRate,
|
||||||
],
|
],
|
||||||
rawData,
|
rawData,
|
||||||
backgroundColor: color.bg,
|
backgroundColor: color.bg,
|
||||||
@@ -584,14 +559,14 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
fontSize: "15px",
|
fontSize: "15px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
row.composite_score.toFixed(1),
|
row.compositeScore.toFixed(1),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "班级",
|
title: "班级",
|
||||||
key: "class_name",
|
key: "class_name",
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
`${row.class_name.slice(0, 2)}计算机${row.class_name.slice(2)}班`,
|
`${row.className.slice(0, 2)}计算机${row.className.slice(2)}班`,
|
||||||
width: 160,
|
width: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -602,7 +577,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#1890ff", fontWeight: "600" } },
|
{ style: { color: "#1890ff", fontWeight: "600" } },
|
||||||
row.user_count,
|
row.userCount,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -613,7 +588,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#ff4d4f", fontWeight: "600" } },
|
{ style: { color: "#ff4d4f", fontWeight: "600" } },
|
||||||
row.total_ac,
|
row.totalAc,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -624,7 +599,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#52c41a", fontWeight: "600" } },
|
{ style: { color: "#52c41a", fontWeight: "600" } },
|
||||||
row.avg_ac.toFixed(2),
|
row.avgAc.toFixed(2),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -635,7 +610,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#fa8c16", fontWeight: "600" } },
|
{ style: { color: "#fa8c16", fontWeight: "600" } },
|
||||||
row.median_ac.toFixed(2),
|
row.medianAc.toFixed(2),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -646,7 +621,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#cf1322", fontWeight: "600" } },
|
{ style: { color: "#cf1322", fontWeight: "600" } },
|
||||||
row.top10_avg.toFixed(2),
|
row.top10Avg.toFixed(2),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -657,7 +632,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#389e0d", fontWeight: "600" } },
|
{ style: { color: "#389e0d", fontWeight: "600" } },
|
||||||
row.middle80_avg.toFixed(2),
|
row.middle80Avg.toFixed(2),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -668,7 +643,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#096dd9", fontWeight: "500" } },
|
{ style: { color: "#096dd9", fontWeight: "500" } },
|
||||||
row.bottom10_avg.toFixed(2),
|
row.bottom10Avg.toFixed(2),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -679,7 +654,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#faad14", fontWeight: "600" } },
|
{ style: { color: "#faad14", fontWeight: "600" } },
|
||||||
row.excellent_rate.toFixed(1) + "%",
|
row.excellentRate.toFixed(1) + "%",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -690,7 +665,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#52c41a", fontWeight: "600" } },
|
{ style: { color: "#52c41a", fontWeight: "600" } },
|
||||||
row.pass_rate.toFixed(1) + "%",
|
row.passRate.toFixed(1) + "%",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -701,7 +676,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#1890ff", fontWeight: "600" } },
|
{ style: { color: "#1890ff", fontWeight: "600" } },
|
||||||
row.active_rate.toFixed(1) + "%",
|
row.activeRate.toFixed(1) + "%",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -827,11 +802,11 @@ const radarChartOptions = {
|
|||||||
<n-grid v-if="comparisons.length > 0" :cols="2" :x-gap="16" :y-gap="16">
|
<n-grid v-if="comparisons.length > 0" :cols="2" :x-gap="16" :y-gap="16">
|
||||||
<n-gi
|
<n-gi
|
||||||
v-for="(classData, index) in comparisons"
|
v-for="(classData, index) in comparisons"
|
||||||
:key="classData.class_name"
|
:key="classData.className"
|
||||||
:span="isDesktop ? 1 : 2"
|
:span="isDesktop ? 1 : 2"
|
||||||
>
|
>
|
||||||
<n-card
|
<n-card
|
||||||
:title="`${classData.class_name.slice(0, 2)}计算机${classData.class_name.slice(2)}班`"
|
:title="`${classData.className.slice(0, 2)}计算机${classData.className.slice(2)}班`"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
hoverable
|
hoverable
|
||||||
:style="{
|
:style="{
|
||||||
@@ -842,7 +817,7 @@ const radarChartOptions = {
|
|||||||
<n-tag :type="getRankColor(index).type" size="large">
|
<n-tag :type="getRankColor(index).type" size="large">
|
||||||
#{{ getRankColor(index).text }}
|
#{{ getRankColor(index).text }}
|
||||||
<span style="margin-left: 6px; font-size: 12px; opacity: 0.85">
|
<span style="margin-left: 6px; font-size: 12px; opacity: 0.85">
|
||||||
{{ classData.composite_score }} 分
|
{{ classData.compositeScore }} 分
|
||||||
</span>
|
</span>
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
@@ -854,7 +829,7 @@ const radarChartOptions = {
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="总AC数"
|
label="总AC数"
|
||||||
:value="classData.total_ac"
|
:value="classData.totalAc"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-total-ac"
|
class="stat-total-ac"
|
||||||
>
|
>
|
||||||
@@ -866,7 +841,7 @@ const radarChartOptions = {
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="平均AC数"
|
label="平均AC数"
|
||||||
:value="classData.avg_ac.toFixed(2)"
|
:value="classData.avgAc.toFixed(2)"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-avg-ac"
|
class="stat-avg-ac"
|
||||||
>
|
>
|
||||||
@@ -881,7 +856,7 @@ const radarChartOptions = {
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="中位数AC数"
|
label="中位数AC数"
|
||||||
:value="classData.median_ac.toFixed(2)"
|
:value="classData.medianAc.toFixed(2)"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-median-ac"
|
class="stat-median-ac"
|
||||||
>
|
>
|
||||||
@@ -896,7 +871,7 @@ const radarChartOptions = {
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="总提交数"
|
label="总提交数"
|
||||||
:value="classData.total_submission"
|
:value="classData.totalSubmission"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-total-submission"
|
class="stat-total-submission"
|
||||||
>
|
>
|
||||||
@@ -911,7 +886,7 @@ const radarChartOptions = {
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="AC率"
|
label="AC率"
|
||||||
:value="classData.ac_rate.toFixed(1) + '%'"
|
:value="classData.acRate.toFixed(1) + '%'"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-ac-rate"
|
class="stat-ac-rate"
|
||||||
>
|
>
|
||||||
@@ -934,12 +909,12 @@ const radarChartOptions = {
|
|||||||
<!-- 分位数统计 -->
|
<!-- 分位数统计 -->
|
||||||
<n-descriptions-item label="第一四分位数(Q1)">
|
<n-descriptions-item label="第一四分位数(Q1)">
|
||||||
<span style="color: #9254de; font-weight: 500">{{
|
<span style="color: #9254de; font-weight: 500">{{
|
||||||
classData.q1_ac.toFixed(2)
|
classData.q1Ac.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="第三四分位数(Q3)">
|
<n-descriptions-item label="第三四分位数(Q3)">
|
||||||
<span style="color: #f759ab; font-weight: 500">{{
|
<span style="color: #f759ab; font-weight: 500">{{
|
||||||
classData.q3_ac.toFixed(2)
|
classData.q3Ac.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="四分位距(IQR)">
|
<n-descriptions-item label="四分位距(IQR)">
|
||||||
@@ -949,31 +924,31 @@ const radarChartOptions = {
|
|||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="标准差">
|
<n-descriptions-item label="标准差">
|
||||||
<span style="color: #fa8c16; font-weight: 500">{{
|
<span style="color: #fa8c16; font-weight: 500">{{
|
||||||
classData.std_dev.toFixed(2)
|
classData.stdDev.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
|
|
||||||
<!-- 分层统计 -->
|
<!-- 分层统计 -->
|
||||||
<n-descriptions-item label="前10%均值">
|
<n-descriptions-item label="前10%均值">
|
||||||
<span style="color: #cf1322; font-weight: 600">{{
|
<span style="color: #cf1322; font-weight: 600">{{
|
||||||
classData.top10_avg.toFixed(2)
|
classData.top10Avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="中间80%均值">
|
<n-descriptions-item label="中间80%均值">
|
||||||
<span style="color: #389e0d; font-weight: 600">{{
|
<span style="color: #389e0d; font-weight: 600">{{
|
||||||
classData.middle80_avg.toFixed(2)
|
classData.middle80Avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="后10%均值">
|
<n-descriptions-item label="后10%均值">
|
||||||
<span style="color: #096dd9; font-weight: 500">{{
|
<span style="color: #096dd9; font-weight: 500">{{
|
||||||
classData.bottom10_avg.toFixed(2)
|
classData.bottom10Avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
|
|
||||||
<!-- 人数 -->
|
<!-- 人数 -->
|
||||||
<n-descriptions-item label="人数">
|
<n-descriptions-item label="人数">
|
||||||
<span style="color: #1890ff; font-weight: 600">{{
|
<span style="color: #1890ff; font-weight: 600">{{
|
||||||
classData.user_count
|
classData.userCount
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
@@ -988,34 +963,34 @@ const radarChartOptions = {
|
|||||||
<n-space vertical :size="10">
|
<n-space vertical :size="10">
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="classData.excellent_rate"
|
:percentage="classData.excellentRate"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
:border-radius="4"
|
:border-radius="4"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
优秀率: {{ classData.excellent_rate.toFixed(1) }}%
|
优秀率: {{ classData.excellentRate.toFixed(1) }}%
|
||||||
</template>
|
</template>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="classData.pass_rate"
|
:percentage="classData.passRate"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
:border-radius="4"
|
:border-radius="4"
|
||||||
status="success"
|
status="success"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
及格率: {{ classData.pass_rate.toFixed(1) }}%
|
及格率: {{ classData.passRate.toFixed(1) }}%
|
||||||
</template>
|
</template>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="classData.active_rate"
|
:percentage="classData.activeRate"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
:border-radius="4"
|
:border-radius="4"
|
||||||
status="info"
|
status="info"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
参与度: {{ classData.active_rate.toFixed(1) }}%
|
参与度: {{ classData.activeRate.toFixed(1) }}%
|
||||||
</template>
|
</template>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -1023,7 +998,7 @@ const radarChartOptions = {
|
|||||||
|
|
||||||
<!-- 时间段统计(如果有) -->
|
<!-- 时间段统计(如果有) -->
|
||||||
<template
|
<template
|
||||||
v-if="hasTimeRange && classData.recent_total_ac !== undefined"
|
v-if="hasTimeRange && classData.recentTotalAc !== undefined"
|
||||||
>
|
>
|
||||||
<n-descriptions
|
<n-descriptions
|
||||||
bordered
|
bordered
|
||||||
@@ -1034,27 +1009,27 @@ const radarChartOptions = {
|
|||||||
>
|
>
|
||||||
<n-descriptions-item label="时间段总AC">
|
<n-descriptions-item label="时间段总AC">
|
||||||
<span style="color: #ff7875; font-weight: 600">{{
|
<span style="color: #ff7875; font-weight: 600">{{
|
||||||
classData.recent_total_ac
|
classData.recentTotalAc
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="时间段平均AC">
|
<n-descriptions-item label="时间段平均AC">
|
||||||
<span style="color: #73d13d; font-weight: 600">{{
|
<span style="color: #73d13d; font-weight: 600">{{
|
||||||
classData.recent_avg_ac?.toFixed(2)
|
classData.recentAvgAc?.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="时间段中位数AC">
|
<n-descriptions-item label="时间段中位数AC">
|
||||||
<span style="color: #ffc53d; font-weight: 600">{{
|
<span style="color: #ffc53d; font-weight: 600">{{
|
||||||
classData.recent_median_ac?.toFixed(2)
|
classData.recentMedianAc?.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="时间段前10名平均">
|
<n-descriptions-item label="时间段前10名平均">
|
||||||
<span style="color: #ff4d4f; font-weight: 600">{{
|
<span style="color: #ff4d4f; font-weight: 600">{{
|
||||||
classData.recent_top10_avg?.toFixed(2)
|
classData.recentTop10Avg?.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="活跃学生数" :span="2">
|
<n-descriptions-item label="活跃学生数" :span="2">
|
||||||
<span style="color: #1890ff; font-weight: 600">{{
|
<span style="color: #1890ff; font-weight: 600">{{
|
||||||
classData.recent_active_count
|
classData.recentActiveCount
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ function goto() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{ rank.accepted_number }} /
|
{{ rank.acceptedNumber }} /
|
||||||
<n-button text type="primary" @click="goto">
|
<n-button text type="primary" @click="goto">
|
||||||
{{ rank.submission_number }}
|
{{ rank.submissionNumber }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -24,18 +24,16 @@ const contestStore = useContestStore()
|
|||||||
<div v-html="contestStore.contest.description"></div>
|
<div v-html="contestStore.contest.description"></div>
|
||||||
<n-descriptions bordered label-placement="left" :column="1">
|
<n-descriptions bordered label-placement="left" :column="1">
|
||||||
<n-descriptions-item label="开始时间">
|
<n-descriptions-item label="开始时间">
|
||||||
{{
|
{{ parseTime(contestStore.contest.startTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||||
parseTime(contestStore.contest.start_time, "YYYY年M月D日 HH:mm:ss")
|
|
||||||
}}
|
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="结束时间">
|
<n-descriptions-item label="结束时间">
|
||||||
{{ parseTime(contestStore.contest.end_time, "YYYY年M月D日 HH:mm:ss") }}
|
{{ parseTime(contestStore.contest.endTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="比赛类型">
|
<n-descriptions-item label="比赛类型">
|
||||||
<ContestType :contest="contestStore.contest" />
|
<ContestType :contest="contestStore.contest" />
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="发起人">
|
<n-descriptions-item label="发起人">
|
||||||
{{ contestStore.contest.created_by.username }}
|
{{ contestStore.contest.createdBy.username }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</n-popover>
|
</n-popover>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const chartData = computed(() => {
|
|||||||
// 收集所有AC事件并按时间排序
|
// 收集所有AC事件并按时间排序
|
||||||
const events: AcEvent[] = []
|
const events: AcEvent[] = []
|
||||||
topUsers.forEach((rank, userIndex) => {
|
topUsers.forEach((rank, userIndex) => {
|
||||||
Object.entries(rank.submission_info).forEach(([problemId, info]) => {
|
Object.entries(rank.submissionInfo).forEach(([problemId, info]) => {
|
||||||
if (info.is_ac) {
|
if (info.is_ac) {
|
||||||
events.push({ time: info.ac_time, userIndex, problemId })
|
events.push({ time: info.ac_time, userIndex, problemId })
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ const chartData = computed(() => {
|
|||||||
// 用于记录每个用户每道题的错误次数
|
// 用于记录每个用户每道题的错误次数
|
||||||
const userErrors: Map<string, number>[] = topUsers.map(() => new Map())
|
const userErrors: Map<string, number>[] = topUsers.map(() => new Map())
|
||||||
topUsers.forEach((rank, i) => {
|
topUsers.forEach((rank, i) => {
|
||||||
Object.entries(rank.submission_info).forEach(([problemId, info]) => {
|
Object.entries(rank.submissionInfo).forEach(([problemId, info]) => {
|
||||||
if (info.error_number > 0) {
|
if (info.error_number > 0) {
|
||||||
userErrors[i].set(problemId, info.error_number)
|
userErrors[i].set(problemId, info.error_number)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useRouteQuery } from "@vueuse/router"
|
|||||||
import { NTag } from "naive-ui"
|
import { NTag } from "naive-ui"
|
||||||
import { getContestList } from "oj/api"
|
import { getContestList } from "oj/api"
|
||||||
import { duration, parseTime } from "utils/functions"
|
import { duration, parseTime } from "utils/functions"
|
||||||
import type { Contest } from "utils/types"
|
import type { OjContest } from "utils/types"
|
||||||
import ContestTitle from "shared/components/ContestTitle.vue"
|
import ContestTitle from "shared/components/ContestTitle.vue"
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
@@ -29,7 +29,7 @@ const { query, clearQuery } = usePagination<ContestQuery>({
|
|||||||
tag: useRouteQuery("tag", "").value,
|
tag: useRouteQuery("tag", "").value,
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = ref<Contest[]>([])
|
const data = ref<OjContest[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|
||||||
const options: SelectOption[] = [
|
const options: SelectOption[] = [
|
||||||
@@ -46,7 +46,7 @@ const tags: SelectOption[] = [
|
|||||||
{ label: "期末", value: "期末" },
|
{ label: "期末", value: "期末" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const columns: DataTableColumn<Contest>[] = [
|
const columns: DataTableColumn<OjContest>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("状态", "streamline-emojis:collision"),
|
title: renderTableTitle("状态", "streamline-emojis:collision"),
|
||||||
key: "status",
|
key: "status",
|
||||||
@@ -74,13 +74,13 @@ const columns: DataTableColumn<Contest>[] = [
|
|||||||
title: renderTableTitle("开始时间", "fluent-emoji-flat:eleven-thirty"),
|
title: renderTableTitle("开始时间", "fluent-emoji-flat:eleven-thirty"),
|
||||||
key: "start_time",
|
key: "start_time",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => parseTime(row.start_time),
|
render: (row) => parseTime(row.startTime),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("比赛时长", "streamline-emojis:fishing-pole"),
|
title: renderTableTitle("比赛时长", "streamline-emojis:fishing-pole"),
|
||||||
key: "duration",
|
key: "duration",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => duration(row.start_time, row.end_time),
|
render: (row) => duration(row.startTime, row.endTime),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -116,11 +116,11 @@ watchDebounced(() => query.keyword, listContests, {
|
|||||||
// 监听其他查询条件变化
|
// 监听其他查询条件变化
|
||||||
watch(() => [query.page, query.limit, query.status, query.tag], listContests)
|
watch(() => [query.page, query.limit, query.status, query.tag], listContests)
|
||||||
|
|
||||||
function rowProps(row: Contest) {
|
function rowProps(row: OjContest) {
|
||||||
return {
|
return {
|
||||||
style: "cursor: pointer",
|
style: "cursor: pointer",
|
||||||
onClick() {
|
onClick() {
|
||||||
if (!userStore.isAuthed && row.contest_type === ContestType.private) {
|
if (!userStore.isAuthed && row.contestType === ContestType.private) {
|
||||||
authStore.openLoginModal()
|
authStore.openLoginModal()
|
||||||
} else {
|
} else {
|
||||||
router.push("/contest/" + row.id)
|
router.push("/contest/" + row.id)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const columns = ref<DataTableColumn<ContestRank>[]>([
|
|||||||
key: "total_time",
|
key: "total_time",
|
||||||
width: 120,
|
width: 120,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => secondsToDuration(row.total_time),
|
render: (row) => secondsToDuration(row.totalTime),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -129,8 +129,8 @@ async function addColumns() {
|
|||||||
() => problem.title,
|
() => problem.title,
|
||||||
),
|
),
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
if (row.submission_info[problem.id]) {
|
if (row.submissionInfo[problem.id]) {
|
||||||
const status = row.submission_info[problem.id]
|
const status = row.submissionInfo[problem.id]
|
||||||
let acTime
|
let acTime
|
||||||
let errorNumber
|
let errorNumber
|
||||||
if (status.is_ac) {
|
if (status.is_ac) {
|
||||||
@@ -162,8 +162,8 @@ async function addColumns() {
|
|||||||
cellProps: (row) => {
|
cellProps: (row) => {
|
||||||
let backgroundColor = ""
|
let backgroundColor = ""
|
||||||
let color = theme.value.textColorBase
|
let color = theme.value.textColorBase
|
||||||
if (row.submission_info[problem.id]) {
|
if (row.submissionInfo[problem.id]) {
|
||||||
const status = row.submission_info[problem.id]
|
const status = row.submissionInfo[problem.id]
|
||||||
if (status.is_first_ac) {
|
if (status.is_first_ac) {
|
||||||
backgroundColor = theme.value.primaryColor
|
backgroundColor = theme.value.primaryColor
|
||||||
color = theme.value.baseColor
|
color = theme.value.baseColor
|
||||||
|
|||||||
@@ -185,8 +185,8 @@ const goSubmissions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const goEdit = () => {
|
const goEdit = () => {
|
||||||
const url = problem.value!.contest
|
const url = problem.value!.contestId
|
||||||
? `/admin/contest/${problem.value!.contest}/problem/edit/${problem.value!.id}`
|
? `/admin/contest/${problem.value!.contestId}/problem/edit/${problem.value!.id}`
|
||||||
: `/admin/problem/edit/${problem.value!.id}`
|
: `/admin/problem/edit/${problem.value!.id}`
|
||||||
window.open(router.resolve(url).href, "_blank")
|
window.open(router.resolve(url).href, "_blank")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const { problem } = storeToRefs(problemStore)
|
|||||||
const problemSetId = computed(() => route.params.problemSetId)
|
const problemSetId = computed(() => route.params.problemSetId)
|
||||||
|
|
||||||
// SQL 题:隐藏输入/输出/例子,改为渲染数据表与期望结果
|
// SQL 题:隐藏输入/输出/例子,改为渲染数据表与期望结果
|
||||||
const isSQL = computed(() => !!problem.value?.sql_config)
|
const isSQL = computed(() => !!problem.value?.sqlConfig)
|
||||||
const sqlDisplay = computed(() => problem.value?.sql_display ?? null)
|
const sqlDisplay = computed(() => problem.value?.sqlDisplay ?? null)
|
||||||
const sqlExpectedQuery = computed(() => {
|
const sqlExpectedQuery = computed(() => {
|
||||||
const exp = sqlDisplay.value?.expected
|
const exp = sqlDisplay.value?.expected
|
||||||
return exp && "columns" in exp ? exp : null
|
return exp && "columns" in exp ? exp : null
|
||||||
@@ -71,7 +71,7 @@ watch(
|
|||||||
|
|
||||||
// AC 或失败次数 >= 3 时加载推荐
|
// AC 或失败次数 >= 3 时加载推荐
|
||||||
watch(
|
watch(
|
||||||
() => [problem.value?._id, problem.value?.my_status, problemStore.failCount],
|
() => [problem.value?._id, problem.value?.myStatus, problemStore.failCount],
|
||||||
([, status, failCount]) => {
|
([, status, failCount]) => {
|
||||||
if (status === 0 || (failCount as number) >= 3) {
|
if (status === 0 || (failCount as number) >= 3) {
|
||||||
loadSimilarProblems()
|
loadSimilarProblems()
|
||||||
@@ -82,9 +82,9 @@ watch(
|
|||||||
|
|
||||||
const hasTriedButNotPassed = computed(() => {
|
const hasTriedButNotPassed = computed(() => {
|
||||||
return (
|
return (
|
||||||
problem.value?.my_status !== undefined &&
|
problem.value?.myStatus !== undefined &&
|
||||||
problem.value?.my_status !== null &&
|
problem.value?.myStatus !== null &&
|
||||||
problem.value?.my_status !== 0
|
problem.value?.myStatus !== 0
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -177,8 +177,8 @@ function ruleTagType(engine: string): "error" | "success" | "info" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const astRulesForDisplay = computed(() => {
|
const astRulesForDisplay = computed(() => {
|
||||||
if (!problem.value?.ast_rules) return []
|
if (!problem.value?.astRules) return []
|
||||||
return Object.entries(problem.value.ast_rules).filter(
|
return Object.entries(problem.value.astRules).filter(
|
||||||
([, rules]) => rules.length > 0,
|
([, rules]) => rules.length > 0,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -249,7 +249,7 @@ function type(status: ProblemStatus) {
|
|||||||
<!-- 已通过 -->
|
<!-- 已通过 -->
|
||||||
<n-alert
|
<n-alert
|
||||||
class="status-alert"
|
class="status-alert"
|
||||||
v-if="problem.my_status === 0"
|
v-if="problem.myStatus === 0"
|
||||||
type="success"
|
type="success"
|
||||||
title="🎉 本 题 已 经 被 你 解 决 啦"
|
title="🎉 本 题 已 经 被 你 解 决 啦"
|
||||||
>
|
>
|
||||||
@@ -291,7 +291,7 @@ function type(status: ProblemStatus) {
|
|||||||
</p>
|
</p>
|
||||||
<MdPreview
|
<MdPreview
|
||||||
preview-theme="vuepress"
|
preview-theme="vuepress"
|
||||||
:model-value="problem.input_description"
|
:model-value="problem.inputDescription"
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ function type(status: ProblemStatus) {
|
|||||||
</p>
|
</p>
|
||||||
<MdPreview
|
<MdPreview
|
||||||
preview-theme="vuepress"
|
preview-theme="vuepress"
|
||||||
:model-value="problem.output_description"
|
:model-value="problem.outputDescription"
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -338,7 +338,7 @@ function type(status: ProblemStatus) {
|
|||||||
:total-rows="sqlExpectedQuery.total_rows"
|
:total-rows="sqlExpectedQuery.total_rows"
|
||||||
:truncated="sqlExpectedQuery.truncated"
|
:truncated="sqlExpectedQuery.truncated"
|
||||||
/>
|
/>
|
||||||
<p v-if="!problem.sql_config?.order_sensitive" class="sqlNote">
|
<p v-if="!problem.sqlConfig?.order_sensitive" class="sqlNote">
|
||||||
结果顺序不限
|
结果顺序不限
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ const { renderError, renderFlowchart } = useMermaid()
|
|||||||
const renderProblemFlowchart = async () => {
|
const renderProblemFlowchart = async () => {
|
||||||
await renderFlowchart(
|
await renderFlowchart(
|
||||||
mermaidContainer.value,
|
mermaidContainer.value,
|
||||||
problem.value?.mermaid_code ?? "",
|
problem.value?.mermaidCode ?? "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(renderProblemFlowchart)
|
onMounted(renderProblemFlowchart)
|
||||||
|
|
||||||
watch(() => problem.value?.mermaid_code, renderProblemFlowchart)
|
watch(() => problem.value?.mermaidCode, renderProblemFlowchart)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const beatRate = ref("0")
|
|||||||
const yearlyACData = ref<YearlyACData[]>([])
|
const yearlyACData = ref<YearlyACData[]>([])
|
||||||
|
|
||||||
const data = computed(() => {
|
const data = computed(() => {
|
||||||
const status = problem.value!.statistic_info
|
const status = problem.value!.statisticInfo
|
||||||
const labels = []
|
const labels = []
|
||||||
for (let i in status) {
|
for (let i in status) {
|
||||||
if (status[i] !== 0) {
|
if (status[i] !== 0) {
|
||||||
@@ -50,14 +50,14 @@ const numbers = computed(() => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
icon: "streamline-ultimate-color:checklist",
|
icon: "streamline-ultimate-color:checklist",
|
||||||
title: problem.value?.submission_number ?? 0,
|
title: problem.value?.submissionNumber ?? 0,
|
||||||
content: "总提交",
|
content: "总提交",
|
||||||
int: true,
|
int: true,
|
||||||
suffix: "",
|
suffix: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "streamline-emojis:woman-raising-hand-2",
|
icon: "streamline-emojis:woman-raising-hand-2",
|
||||||
title: problem.value?.accepted_number ?? 0,
|
title: problem.value?.acceptedNumber ?? 0,
|
||||||
content: "通过数",
|
content: "通过数",
|
||||||
int: true,
|
int: true,
|
||||||
suffix: "",
|
suffix: "",
|
||||||
@@ -65,8 +65,8 @@ const numbers = computed(() => {
|
|||||||
{
|
{
|
||||||
icon: "fluent-emoji:chart-increasing",
|
icon: "fluent-emoji:chart-increasing",
|
||||||
title: getACRateNumber(
|
title: getACRateNumber(
|
||||||
problem.value?.accepted_number ?? 0,
|
problem.value?.acceptedNumber ?? 0,
|
||||||
problem.value?.submission_number ?? 0,
|
problem.value?.submissionNumber ?? 0,
|
||||||
),
|
),
|
||||||
content: "通过率",
|
content: "通过率",
|
||||||
int: false,
|
int: false,
|
||||||
@@ -115,10 +115,10 @@ onMounted(() => {
|
|||||||
{{ problem._id }}
|
{{ problem._id }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="出题人">
|
<n-descriptions-item label="出题人">
|
||||||
{{ problem.created_by.username }}
|
{{ problem.createdBy.username }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="创建时间">
|
<n-descriptions-item label="创建时间">
|
||||||
{{ parseTime(problem.create_time) }}
|
{{ parseTime(problem.createTime) }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="难度">
|
<n-descriptions-item label="难度">
|
||||||
<n-tag :type="getTagColor(problem.difficulty)">
|
<n-tag :type="getTagColor(problem.difficulty)">
|
||||||
@@ -150,7 +150,7 @@ onMounted(() => {
|
|||||||
</n-card>
|
</n-card>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<div class="pie" v-if="problem && problem.submission_number > 0">
|
<div class="pie" v-if="problem && problem.submissionNumber > 0">
|
||||||
<Pie :data="data" :options="options" />
|
<Pie :data="data" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
<ProblemYearlyChart :data="yearlyACData" />
|
<ProblemYearlyChart :data="yearlyACData" />
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ defineProps<{
|
|||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<span>{{ problem.title }}</span>
|
<span>{{ problem.title }}</span>
|
||||||
<Icon
|
<Icon
|
||||||
v-if="problem.allow_flowchart"
|
v-if="problem.allowFlowchart"
|
||||||
width="18"
|
width="18"
|
||||||
icon="vscode-icons:file-type-drawio"
|
icon="vscode-icons:file-type-drawio"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
v-else-if="problem.show_flowchart"
|
v-else-if="problem.showFlowchart"
|
||||||
width="18"
|
width="18"
|
||||||
icon="vscode-icons:file-type-graphql"
|
icon="vscode-icons:file-type-graphql"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
v-if="problem.has_ast_rules"
|
v-if="problem.hasAstRules"
|
||||||
width="18"
|
width="18"
|
||||||
icon="vscode-icons:file-type-light-todo"
|
icon="vscode-icons:file-type-light-todo"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const wheelItems = REACTIONS.map((item, index) => ({
|
|||||||
style: getWheelItemStyle(index),
|
style: getWheelItemStyle(index),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const solved = computed(() => problem.value?.my_status === 0)
|
const solved = computed(() => problem.value?.myStatus === 0)
|
||||||
const locked = computed(() => mine.value !== null)
|
const locked = computed(() => mine.value !== null)
|
||||||
const canInteract = computed(
|
const canInteract = computed(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useUserStore } from "shared/store/user"
|
|||||||
import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
|
import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import { renderTableTitle } from "utils/renders"
|
import { renderTableTitle } from "utils/renders"
|
||||||
import type { Submission } from "utils/types"
|
import type { SubmissionListItem } from "utils/types"
|
||||||
import SubmissionDetail from "oj/submission/detail.vue"
|
import SubmissionDetail from "oj/submission/detail.vue"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
|
|
||||||
@@ -29,19 +29,19 @@ function showCodePanel(id: string, problem: string) {
|
|||||||
toggleCodePanel(true)
|
toggleCodePanel(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: DataTableColumn<Submission>[] = [
|
const columns: DataTableColumn<SubmissionListItem>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"),
|
title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"),
|
||||||
key: "id",
|
key: "id",
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
if (!row.show_link)
|
if (!row.showLink)
|
||||||
return h(NFlex, { align: "center" }, () => [
|
return h(NFlex, { align: "center" }, () => [
|
||||||
h("span", row.id.slice(0, 12)),
|
h("span", row.id.slice(0, 12)),
|
||||||
h(
|
h(
|
||||||
@@ -90,7 +90,7 @@ const class_ac_count = ref(0)
|
|||||||
const all_ac_count = ref(0)
|
const all_ac_count = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const submissions = ref<Submission[]>([])
|
const submissions = ref<SubmissionListItem[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@@ -126,8 +126,8 @@ async function listSubmissions() {
|
|||||||
...query,
|
...query,
|
||||||
myself: "1",
|
myself: "1",
|
||||||
offset,
|
offset,
|
||||||
problem_id: (route.params.problemID as string) ?? "",
|
problemId: (route.params.problemID as string) ?? "",
|
||||||
contest_id: (route.params.contestID as string) ?? "",
|
contestId: (route.params.contestID as string) ?? "",
|
||||||
})
|
})
|
||||||
submissions.value = res.data.results
|
submissions.value = res.data.results
|
||||||
total.value = res.data.total
|
total.value = res.data.total
|
||||||
@@ -138,10 +138,10 @@ async function getRankOfThisProblem() {
|
|||||||
const res = await getRankOfProblem((route.params.problemID as string) ?? "")
|
const res = await getRankOfProblem((route.params.problemID as string) ?? "")
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
||||||
class_name.value = res.data.class_name
|
class_name.value = res.data.className
|
||||||
rank.value = res.data.rank
|
rank.value = res.data.rank
|
||||||
class_ac_count.value = res.data.class_ac_count
|
class_ac_count.value = res.data.classAcCount
|
||||||
all_ac_count.value = res.data.all_ac_count
|
all_ac_count.value = res.data.allAcCount
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const chartData = computed(() => ({
|
|||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "AC 率",
|
label: "AC 率",
|
||||||
data: props.data.map((d) => d.ac_rate),
|
data: props.data.map((d) => d.acRate),
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
||||||
@@ -58,7 +58,7 @@ const chartOptions = computed(() => ({
|
|||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context: any) => {
|
label: (context: any) => {
|
||||||
const d = props.data[context.dataIndex]
|
const d = props.data[context.dataIndex]
|
||||||
return [`AC 率: ${d.ac_rate}%`, `通过: ${d.accepted} / ${d.total}`]
|
return [`AC 率: ${d.acRate}%`, `通过: ${d.accepted} / ${d.total}`]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ const msg = computed(() => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
result !== SubmissionStatus.ast_check_failed &&
|
result !== SubmissionStatus.ast_check_failed &&
|
||||||
props.submission.statistic_info?.err_info
|
props.submission.statisticInfo?.err_info
|
||||||
) {
|
) {
|
||||||
msg += props.submission.statistic_info.err_info
|
msg += props.submission.statisticInfo.err_info
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
@@ -161,15 +161,13 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
|||||||
<n-flex
|
<n-flex
|
||||||
vertical
|
vertical
|
||||||
v-if="
|
v-if="
|
||||||
msg ||
|
msg || infoTable.length || submission.statisticInfo?.ast_results?.length
|
||||||
infoTable.length ||
|
|
||||||
submission.statistic_info?.ast_results?.length
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<n-card v-if="submission.statistic_info?.ast_results?.length" embedded>
|
<n-card v-if="submission.statisticInfo?.ast_results?.length" embedded>
|
||||||
<n-flex vertical :size="8">
|
<n-flex vertical :size="8">
|
||||||
<n-flex
|
<n-flex
|
||||||
v-for="(rule, i) in submission.statistic_info.ast_results"
|
v-for="(rule, i) in submission.statisticInfo.ast_results"
|
||||||
:key="i"
|
:key="i"
|
||||||
align="center"
|
align="center"
|
||||||
:size="6"
|
:size="6"
|
||||||
|
|||||||
@@ -130,22 +130,22 @@ async function submit() {
|
|||||||
|
|
||||||
// 1. 构建提交数据
|
// 1. 构建提交数据
|
||||||
const data: SubmitCodePayload = {
|
const data: SubmitCodePayload = {
|
||||||
problem_id: problem.value!.id,
|
problemId: problem.value!.id,
|
||||||
language: codeStore.code.language,
|
language: codeStore.code.language,
|
||||||
code: codeStore.code.value,
|
code: codeStore.code.value,
|
||||||
}
|
}
|
||||||
if (contestID) {
|
if (contestID) {
|
||||||
data.contest_id = parseInt(contestID)
|
data.contestId = parseInt(contestID)
|
||||||
}
|
}
|
||||||
// 2. 提交代码到后端
|
// 2. 提交代码到后端
|
||||||
isSubmittingRequest.value = true
|
isSubmittingRequest.value = true
|
||||||
try {
|
try {
|
||||||
const res = await submitCode(data)
|
const res = await submitCode(data)
|
||||||
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
|
console.log(`[Submit] 代码已提交: ID=${res.data.submissionId}`)
|
||||||
|
|
||||||
// 3. 启动冷却 + 监控
|
// 3. 启动冷却 + 监控
|
||||||
startCooldown()
|
startCooldown()
|
||||||
startMonitoring(res.data.submission_id)
|
startMonitoring(res.data.submissionId)
|
||||||
showResult.value = true
|
showResult.value = true
|
||||||
} finally {
|
} finally {
|
||||||
isSubmittingRequest.value = false
|
isSubmittingRequest.value = false
|
||||||
@@ -183,7 +183,7 @@ watch(
|
|||||||
return
|
return
|
||||||
|
|
||||||
// 1. 刷新题目状态
|
// 1. 刷新题目状态
|
||||||
problem.value!.my_status = 0
|
problem.value!.myStatus = 0
|
||||||
|
|
||||||
// 2. 创建ProblemSetSubmission记录,更新题单进度
|
// 2. 创建ProblemSetSubmission记录,更新题单进度
|
||||||
if (problemSetId) {
|
if (problemSetId) {
|
||||||
|
|||||||
@@ -135,16 +135,16 @@ async function submitFlowchartData() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await submitFlowchart({
|
const response = await submitFlowchart({
|
||||||
problem_id: problem.value!.id,
|
problemId: problem.value!.id,
|
||||||
mermaid_code: mermaidCode,
|
mermaidCode,
|
||||||
flowchart_data: {
|
flowchartData: {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
data: compressed,
|
data: compressed,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取提交ID并订阅更新
|
// 获取提交ID并订阅更新
|
||||||
const submissionId = response.data.submission_id
|
const submissionId = response.data.submissionId
|
||||||
|
|
||||||
if (submissionId) {
|
if (submissionId) {
|
||||||
subscribeToSubmission(submissionId)
|
subscribeToSubmission(submissionId)
|
||||||
@@ -183,18 +183,34 @@ async function getSubmission(submissionPage = 0) {
|
|||||||
)
|
)
|
||||||
submissionCount.value = data.count
|
submissionCount.value = data.count
|
||||||
const submission = data.submission
|
const submission = data.submission
|
||||||
myFlowchartZippedStr.value = submission.flowchart_data.data
|
// 翻到没有提交的页时后端返回 null(契约里 submission 是 nullable)——
|
||||||
myMermaidCode.value = submission.mermaid_code || ""
|
// 原来的 any 让这里看起来非空,真翻到那一页会直接抛
|
||||||
|
if (!submission) {
|
||||||
|
myFlowchartZippedStr.value = ""
|
||||||
|
myMermaidCode.value = ""
|
||||||
|
modalRating.value = { score: 0, grade: "" }
|
||||||
|
evaluation.value = {
|
||||||
|
score: 0,
|
||||||
|
grade: "",
|
||||||
|
feedback: "",
|
||||||
|
suggestions: "",
|
||||||
|
criteria_details: {},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
myFlowchartZippedStr.value = String(submission.flowchartData.data ?? "")
|
||||||
|
myMermaidCode.value = submission.mermaidCode || ""
|
||||||
modalRating.value = {
|
modalRating.value = {
|
||||||
score: submission.ai_score,
|
score: submission.aiScore ?? 0,
|
||||||
grade: submission.ai_grade,
|
grade: (submission.aiGrade ?? "") as Rating["grade"],
|
||||||
}
|
}
|
||||||
evaluation.value = {
|
evaluation.value = {
|
||||||
score: submission.ai_score,
|
score: submission.aiScore ?? 0,
|
||||||
grade: submission.ai_grade,
|
grade: (submission.aiGrade ?? "") as Rating["grade"],
|
||||||
feedback: submission.ai_feedback,
|
feedback: submission.aiFeedback ?? "",
|
||||||
suggestions: submission.ai_suggestions,
|
suggestions: submission.aiSuggestions ?? "",
|
||||||
criteria_details: submission.ai_criteria_details,
|
criteria_details:
|
||||||
|
submission.aiCriteriaDetails as Evaluation["criteria_details"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const { isMobile, isDesktop } = useBreakpoints()
|
|||||||
|
|
||||||
const tabOptions = computed(() => {
|
const tabOptions = computed(() => {
|
||||||
const options: string[] = ["content"]
|
const options: string[] = ["content"]
|
||||||
if (problem.value?.show_flowchart) {
|
if (problem.value?.showFlowchart) {
|
||||||
options.push("flowchart")
|
options.push("flowchart")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ async function init() {
|
|||||||
problem.value = res.data
|
problem.value = res.data
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
problem.value = null
|
problem.value = null
|
||||||
if (err.data === "Contest has not started yet.") {
|
if (err.error === "contest-not-started") {
|
||||||
errMsg.value = "比赛还没有开始"
|
errMsg.value = "比赛还没有开始"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ watch(
|
|||||||
<ProblemContent />
|
<ProblemContent />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-if="problem.show_flowchart && problem.mermaid_code"
|
v-if="problem.showFlowchart && problem.mermaidCode"
|
||||||
name="flowchart"
|
name="flowchart"
|
||||||
tab="流程图表"
|
tab="流程图表"
|
||||||
>
|
>
|
||||||
@@ -211,7 +211,7 @@ watch(
|
|||||||
<ProblemContent />
|
<ProblemContent />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-if="problem.show_flowchart && problem.mermaid_code"
|
v-if="problem.showFlowchart && problem.mermaidCode"
|
||||||
name="flowchart"
|
name="flowchart"
|
||||||
tab="流程图表"
|
tab="流程图表"
|
||||||
>
|
>
|
||||||
@@ -251,7 +251,7 @@ watch(
|
|||||||
<n-tab-pane name="content" tab="描述">
|
<n-tab-pane name="content" tab="描述">
|
||||||
<ProblemContent />
|
<ProblemContent />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane v-if="problem.show_flowchart" name="flowchart" tab="流程">
|
<n-tab-pane v-if="problem.showFlowchart" name="flowchart" tab="流程">
|
||||||
<ProblemFlowchart />
|
<ProblemFlowchart />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="editor" tab="代码">
|
<n-tab-pane name="editor" tab="代码">
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ function getDifficultyTag(difficulty: string) {
|
|||||||
function getProgressPercentage() {
|
function getProgressPercentage() {
|
||||||
if (!props.problemSet) return 0
|
if (!props.problemSet) return 0
|
||||||
return Math.round(
|
return Math.round(
|
||||||
(props.problemSet.completed_count / props.problemSet.problems_count) * 100,
|
((props.problemSet.completedCount ?? 0) / props.problemSet.problemsCount) *
|
||||||
|
100,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ function handleJoin() {
|
|||||||
<n-flex align="center" v-if="isJoined">
|
<n-flex align="center" v-if="isJoined">
|
||||||
<n-text strong>完成进度</n-text>
|
<n-text strong>完成进度</n-text>
|
||||||
<n-text>
|
<n-text>
|
||||||
{{ problemSet.completed_count }} / {{ problemSet.problems_count }}
|
{{ problemSet.completedCount }} / {{ problemSet.problemsCount }}
|
||||||
</n-text>
|
</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-progress
|
<n-progress
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function handleProblemClick(problemId: string) {
|
|||||||
style="margin-right: 10px"
|
style="margin-right: 10px"
|
||||||
width="48"
|
width="48"
|
||||||
icon="fluent-emoji:check-mark-button"
|
icon="fluent-emoji:check-mark-button"
|
||||||
v-if="problemSetProblem.is_completed"
|
v-if="problemSetProblem.isCompleted"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<n-flex vertical style="flex: 1">
|
<n-flex vertical style="flex: 1">
|
||||||
@@ -60,7 +60,7 @@ function handleProblemClick(problemId: string) {
|
|||||||
{{ DIFFICULTY[problemSetProblem.problem.difficulty] }}
|
{{ DIFFICULTY[problemSetProblem.problem.difficulty] }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-text type="info">分数:{{ problemSetProblem.score }}</n-text>
|
<n-text type="info">分数:{{ problemSetProblem.score }}</n-text>
|
||||||
<n-text v-if="!problemSetProblem.is_required">(选做)</n-text>
|
<n-text v-if="!problemSetProblem.isRequired">(选做)</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const total = ref(0)
|
|||||||
const statistics = ref<{
|
const statistics = ref<{
|
||||||
total: number
|
total: number
|
||||||
completed: number
|
completed: number
|
||||||
avg_progress: number
|
avgProgress: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const classFilter = ref<string>("")
|
const classFilter = ref<string>("")
|
||||||
const completionFilter = ref<"" | "completed" | "in_progress" | "not_started">(
|
const completionFilter = ref<"" | "completed" | "in_progress" | "not_started">(
|
||||||
@@ -42,17 +42,17 @@ async function loadUserProgress() {
|
|||||||
const params: {
|
const params: {
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
class_name?: string
|
className?: string
|
||||||
completion_status?: "" | "completed" | "in_progress" | "not_started"
|
completionStatus?: "" | "completed" | "in_progress" | "not_started"
|
||||||
} = {
|
} = {
|
||||||
limit: query.limit,
|
limit: query.limit,
|
||||||
offset,
|
offset,
|
||||||
}
|
}
|
||||||
if (classFilter.value.trim()) {
|
if (classFilter.value.trim()) {
|
||||||
params.class_name = classFilter.value.trim()
|
params.className = classFilter.value.trim()
|
||||||
}
|
}
|
||||||
if (completionFilter.value) {
|
if (completionFilter.value) {
|
||||||
params.completion_status = completionFilter.value
|
params.completionStatus = completionFilter.value
|
||||||
}
|
}
|
||||||
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ const stats = computed(() => {
|
|||||||
return {
|
return {
|
||||||
total: statistics.value.total,
|
total: statistics.value.total,
|
||||||
completed: statistics.value.completed,
|
completed: statistics.value.completed,
|
||||||
avgProgress: Math.round(statistics.value.avg_progress),
|
avgProgress: Math.round(statistics.value.avgProgress),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果后端还没有返回统计数据,使用默认值
|
// 如果后端还没有返回统计数据,使用默认值
|
||||||
@@ -128,7 +128,7 @@ const progressColumns = [
|
|||||||
key: "join_time",
|
key: "join_time",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row: ProblemSetProgress) =>
|
render: (row: ProblemSetProgress) =>
|
||||||
parseTime(row.join_time, "YYYY-MM-DD HH:mm:ss"),
|
parseTime(row.joinTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "已完成数量",
|
title: "已完成数量",
|
||||||
@@ -140,12 +140,12 @@ const progressColumns = [
|
|||||||
key: "completed_problems",
|
key: "completed_problems",
|
||||||
width: 300,
|
width: 300,
|
||||||
render: (row: ProblemSetProgress) => {
|
render: (row: ProblemSetProgress) => {
|
||||||
if (row.progress_percentage === 100) {
|
if (row.progressPercentage === 100) {
|
||||||
return "全部题目已完成"
|
return "全部题目已完成"
|
||||||
}
|
}
|
||||||
if (row.progress_percentage > 50 && row.progress_percentage < 100) {
|
if (row.progressPercentage > 50 && row.progressPercentage < 100) {
|
||||||
const completedProblemIds = new Set(
|
const completedProblemIds = new Set(
|
||||||
row.completed_problems.map((p: any) => p.id),
|
row.completedProblems.map((p: any) => p.id),
|
||||||
)
|
)
|
||||||
const incompleteProblems = allProblems.value.filter(
|
const incompleteProblems = allProblems.value.filter(
|
||||||
(p) => !completedProblemIds.has(p.id),
|
(p) => !completedProblemIds.has(p.id),
|
||||||
@@ -164,7 +164,7 @@ const progressColumns = [
|
|||||||
}
|
}
|
||||||
return h("div", { style: "max-height: 120px; overflow-y: auto" }, [
|
return h("div", { style: "max-height: 120px; overflow-y: auto" }, [
|
||||||
h(NFlex, {}, () =>
|
h(NFlex, {}, () =>
|
||||||
row.completed_problems.map((problem: any) =>
|
row.completedProblems.map((problem: any) =>
|
||||||
h(
|
h(
|
||||||
NTag,
|
NTag,
|
||||||
{
|
{
|
||||||
@@ -184,7 +184,7 @@ const progressColumns = [
|
|||||||
key: "progress_percentage",
|
key: "progress_percentage",
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (row: ProblemSetProgress) => {
|
render: (row: ProblemSetProgress) => {
|
||||||
return `${row.progress_percentage.toFixed(0)}%`
|
return `${row.progressPercentage.toFixed(0)}%`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -192,7 +192,7 @@ const progressColumns = [
|
|||||||
key: "is_completed",
|
key: "is_completed",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row: ProblemSetProgress) => {
|
render: (row: ProblemSetProgress) => {
|
||||||
if (row.is_completed) {
|
if (row.isCompleted) {
|
||||||
return h(NTag, { type: "success" }, () => "已完成")
|
return h(NTag, { type: "success" }, () => "已完成")
|
||||||
} else {
|
} else {
|
||||||
return h(NTag, { type: "warning" }, () => "进行中")
|
return h(NTag, { type: "warning" }, () => "进行中")
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const activeTab = ref("problems")
|
|||||||
async function loadProblemSetDetail() {
|
async function loadProblemSetDetail() {
|
||||||
const res = await getProblemSetDetail(problemSetId.value)
|
const res = await getProblemSetDetail(problemSetId.value)
|
||||||
problemSet.value = res.data
|
problemSet.value = res.data
|
||||||
isJoined.value = res.data.user_progress?.is_joined || false
|
isJoined.value = res.data.userProgress?.isJoined || false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProblems() {
|
async function loadProblems() {
|
||||||
@@ -48,14 +48,14 @@ async function loadUserBadges() {
|
|||||||
|
|
||||||
const res = await getUserBadges()
|
const res = await getUserBadges()
|
||||||
userBadges.value = res.data.filter(
|
userBadges.value = res.data.filter(
|
||||||
(badge: UserBadgeType) => badge.badge.problemset === problemSetId.value,
|
(badge: UserBadgeType) => badge.badge.problemsetId === problemSetId.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await Promise.all([loadProblemSetDetail(), loadProblems()])
|
await Promise.all([loadProblemSetDetail(), loadProblems()])
|
||||||
if (isJoined.value) {
|
if (isJoined.value) {
|
||||||
if (problemSet.value?.user_progress?.is_completed) {
|
if (problemSet.value?.userProgress?.isCompleted) {
|
||||||
celebrate()
|
celebrate()
|
||||||
}
|
}
|
||||||
loadUserBadges()
|
loadUserBadges()
|
||||||
@@ -100,7 +100,7 @@ async function handleJoinProblemSet() {
|
|||||||
const showTabs = computed(
|
const showTabs = computed(
|
||||||
() =>
|
() =>
|
||||||
userStore.isSuperAdmin ||
|
userStore.isSuperAdmin ||
|
||||||
(isJoined.value && problemSet.value?.user_progress?.is_completed),
|
(isJoined.value && problemSet.value?.userProgress?.isCompleted),
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Icon } from "@iconify/vue"
|
|||||||
import { useRouteQuery } from "@vueuse/router"
|
import { useRouteQuery } from "@vueuse/router"
|
||||||
import { getProblemSetList } from "../api"
|
import { getProblemSetList } from "../api"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import type { ProblemSetList } from "utils/types"
|
import type { ProblemSet } from "utils/types"
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
@@ -12,7 +12,7 @@ const router = useRouter()
|
|||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const problemSets = ref<ProblemSetList[]>([])
|
const problemSets = ref<ProblemSet[]>([])
|
||||||
|
|
||||||
interface ProblemSetQuery {
|
interface ProblemSetQuery {
|
||||||
keyword: string
|
keyword: string
|
||||||
@@ -164,27 +164,25 @@ watch(
|
|||||||
<n-flex justify="space-between" align="center">
|
<n-flex justify="space-between" align="center">
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<Icon width="20" icon="streamline-emojis:blossom" />
|
<Icon width="20" icon="streamline-emojis:blossom" />
|
||||||
<n-text>{{ problemSet.problems_count }} 道题目</n-text>
|
<n-text>{{ problemSet.problemsCount }} 道题目</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<n-flex align="center" style="height: 28px">
|
<n-flex align="center" style="height: 28px">
|
||||||
<!-- 用户进度显示 -->
|
<!-- 用户进度显示 -->
|
||||||
<n-progress
|
<n-progress
|
||||||
v-if="
|
v-if="
|
||||||
problemSet.user_progress?.is_joined &&
|
problemSet.userProgress?.isJoined &&
|
||||||
!problemSet.user_progress?.is_completed
|
!problemSet.userProgress?.isCompleted
|
||||||
"
|
"
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="
|
:percentage="
|
||||||
Math.round(problemSet.user_progress.progress_percentage)
|
Math.round(problemSet.userProgress.progressPercentage)
|
||||||
"
|
"
|
||||||
:height="4"
|
:height="4"
|
||||||
:border-radius="2"
|
:border-radius="2"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
:color="
|
:color="
|
||||||
getProgressColor(
|
getProgressColor(problemSet.userProgress.progressPercentage)
|
||||||
problemSet.user_progress.progress_percentage,
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<n-tag type="warning" v-if="problemSet.status === 'archived'">
|
<n-tag type="warning" v-if="problemSet.status === 'archived'">
|
||||||
@@ -192,17 +190,14 @@ watch(
|
|||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="
|
v-if="
|
||||||
problemSet.user_progress?.is_joined &&
|
problemSet.userProgress?.isJoined &&
|
||||||
!problemSet.user_progress?.is_completed
|
!problemSet.userProgress?.isCompleted
|
||||||
"
|
"
|
||||||
type="warning"
|
type="warning"
|
||||||
>
|
>
|
||||||
已加入
|
已加入
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag
|
<n-tag v-if="problemSet.userProgress?.isCompleted" type="error">
|
||||||
v-if="problemSet.user_progress?.is_completed"
|
|
||||||
type="error"
|
|
||||||
>
|
|
||||||
已完成
|
已完成
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
@@ -212,7 +207,7 @@ watch(
|
|||||||
<n-flex align="center" justify="space-between">
|
<n-flex align="center" justify="space-between">
|
||||||
<n-text depth="3">
|
<n-text depth="3">
|
||||||
创建于
|
创建于
|
||||||
{{ parseTime(problemSet.create_time, "YYYY-MM-DD") }}
|
{{ parseTime(problemSet.createTime, "YYYY-MM-DD") }}
|
||||||
</n-text>
|
</n-text>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<n-tooltip
|
<n-tooltip
|
||||||
@@ -227,7 +222,7 @@ watch(
|
|||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
object-fit="cover"
|
object-fit="cover"
|
||||||
:class="{ 'earned-badge': badge.is_earned }"
|
:class="{ 'earned-badge': badge.isEarned }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<n-flex vertical size="small">
|
<n-flex vertical size="small">
|
||||||
@@ -238,12 +233,12 @@ watch(
|
|||||||
获取条件:
|
获取条件:
|
||||||
{{
|
{{
|
||||||
getConditionText(
|
getConditionText(
|
||||||
badge.condition_type,
|
badge.conditionType,
|
||||||
badge.condition_value,
|
badge.conditionValue,
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<n-text type="primary" v-if="badge.is_earned">
|
<n-text type="primary" v-if="badge.isEarned">
|
||||||
✓ 已获得
|
✓ 已获得
|
||||||
</n-text>
|
</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const data = computed(() => {
|
|||||||
const datasets: any[] = [
|
const datasets: any[] = [
|
||||||
{
|
{
|
||||||
label: props.type === ChartType.Rank ? "已解决" : "做题数",
|
label: props.type === ChartType.Rank ? "已解决" : "做题数",
|
||||||
data: props.rankData.map((rank) => rank.accepted_number),
|
data: props.rankData.map((rank) => rank.acceptedNumber),
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
"rgba(255, 99, 132, 0.2)",
|
"rgba(255, 99, 132, 0.2)",
|
||||||
"rgba(255, 159, 64, 0.2)",
|
"rgba(255, 159, 64, 0.2)",
|
||||||
@@ -87,7 +87,7 @@ const data = computed(() => {
|
|||||||
if (props.type === ChartType.Rank) {
|
if (props.type === ChartType.Rank) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: "总提交数",
|
label: "总提交数",
|
||||||
data: props.rankData.map((rank) => rank.submission_number),
|
data: props.rankData.map((rank) => rank.submissionNumber),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type {
|
||||||
|
ClassComparison,
|
||||||
|
ClassRankItem as ClassRank,
|
||||||
|
ClassUserRank,
|
||||||
|
Rank,
|
||||||
|
} from "utils/types"
|
||||||
import { formatISO, sub, type Duration } from "date-fns"
|
import { formatISO, sub, type Duration } from "date-fns"
|
||||||
import { NButton, NFlex } from "naive-ui"
|
import { NButton, NFlex } from "naive-ui"
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +16,6 @@ import {
|
|||||||
} from "oj/api"
|
} from "oj/api"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { getACRate, getCSRFToken } from "utils/functions"
|
import { getACRate, getCSRFToken } from "utils/functions"
|
||||||
import type { Rank } from "utils/types"
|
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { ChartType } from "utils/constants"
|
import { ChartType } from "utils/constants"
|
||||||
import { renderTableTitle } from "utils/renders"
|
import { renderTableTitle } from "utils/renders"
|
||||||
@@ -39,6 +44,7 @@ const query = reactive({
|
|||||||
limit: 10,
|
limit: 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
})
|
})
|
||||||
|
const message = useMessage()
|
||||||
const rankChart = ref<Rank[]>([])
|
const rankChart = ref<Rank[]>([])
|
||||||
const activityChart = ref<Rank[]>([])
|
const activityChart = ref<Rank[]>([])
|
||||||
const duration = ref("months:1")
|
const duration = ref("months:1")
|
||||||
@@ -46,7 +52,7 @@ const classData = ref<ClassRank[]>([])
|
|||||||
const classQuery = reactive({
|
const classQuery = reactive({
|
||||||
grade: gradeOptions[0].value,
|
grade: gradeOptions[0].value,
|
||||||
})
|
})
|
||||||
const myClassData = ref<UserRank[]>([])
|
const myClassData = ref<ClassUserRank["ranks"]>([])
|
||||||
const myRank = ref(-1)
|
const myRank = ref(-1)
|
||||||
const myClassName = ref("")
|
const myClassName = ref("")
|
||||||
const myClassScope = ref<"window" | "all">("window")
|
const myClassScope = ref<"window" | "all">("window")
|
||||||
@@ -137,44 +143,6 @@ async function analyzeSingleClassWithAI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClassRank {
|
|
||||||
rank: number
|
|
||||||
class_name: string
|
|
||||||
user_count: number
|
|
||||||
total_ac: number
|
|
||||||
total_submission: number
|
|
||||||
avg_ac: number
|
|
||||||
ac_rate: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClassComparison {
|
|
||||||
class_name: string
|
|
||||||
user_count: number
|
|
||||||
total_ac: number
|
|
||||||
total_submission: number
|
|
||||||
avg_ac: number
|
|
||||||
median_ac: number
|
|
||||||
q1_ac: number
|
|
||||||
q3_ac: number
|
|
||||||
iqr: number
|
|
||||||
std_dev: number
|
|
||||||
top10_avg: number
|
|
||||||
middle80_avg: number
|
|
||||||
bottom10_avg: number
|
|
||||||
excellent_rate: number
|
|
||||||
pass_rate: number
|
|
||||||
active_rate: number
|
|
||||||
ac_rate: number
|
|
||||||
composite_score: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserRank {
|
|
||||||
rank: number
|
|
||||||
username: string
|
|
||||||
accepted_number: number
|
|
||||||
submission_number: number
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const offset = (query.page - 1) * query.limit
|
const offset = (query.page - 1) * query.limit
|
||||||
const res = await getRank(offset, query.limit, 100)
|
const res = await getRank(offset, query.limit, 100)
|
||||||
@@ -251,7 +219,7 @@ const columns: DataTableColumn<Rank>[] = [
|
|||||||
key: "rate",
|
key: "rate",
|
||||||
width: 120,
|
width: 120,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => getACRate(row.accepted_number, row.submission_number),
|
render: (row) => getACRate(row.acceptedNumber, row.submissionNumber),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -269,15 +237,14 @@ async function listActivity() {
|
|||||||
const current = Date.now()
|
const current = Date.now()
|
||||||
const start = formatISO(sub(current, subOptions.value))
|
const start = formatISO(sub(current, subOptions.value))
|
||||||
const res = await getActivityRank(start)
|
const res = await getActivityRank(start)
|
||||||
activityChart.value = res.data.map(
|
// 活动榜只有「用户名 + 做题数」,塞进榜单图表复用的 Rank 形状里
|
||||||
(d: { username: string; count: number }) => ({
|
activityChart.value = res.data.map((d, index) => ({
|
||||||
user: {
|
id: index,
|
||||||
username: d.username,
|
user: { id: index, username: d.username, realName: null },
|
||||||
},
|
acceptedNumber: d.count,
|
||||||
accepted_number: d.count,
|
submissionNumber: 0,
|
||||||
submission_number: 0,
|
mood: null,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listRank() {
|
async function listRank() {
|
||||||
@@ -321,7 +288,7 @@ const classColumns: DataTableColumn<ClassRank>[] = [
|
|||||||
title: "班级",
|
title: "班级",
|
||||||
key: "class_name",
|
key: "class_name",
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
`${row.class_name.slice(0, 2)}计算机${row.class_name.slice(2)}班`,
|
`${row.className.slice(0, 2)}计算机${row.className.slice(2)}班`,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
titleAlign: "center",
|
titleAlign: "center",
|
||||||
align: "center",
|
align: "center",
|
||||||
@@ -360,7 +327,7 @@ const classColumns: DataTableColumn<ClassRank>[] = [
|
|||||||
width: 90,
|
width: 90,
|
||||||
titleAlign: "center",
|
titleAlign: "center",
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => `${row.ac_rate}%`,
|
render: (row) => `${row.acRate}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "详情",
|
title: "详情",
|
||||||
@@ -374,14 +341,14 @@ const classColumns: DataTableColumn<ClassRank>[] = [
|
|||||||
{
|
{
|
||||||
text: true,
|
text: true,
|
||||||
type: "info",
|
type: "info",
|
||||||
onClick: () => loadClassDetail(row.class_name),
|
onClick: () => loadClassDetail(row.className),
|
||||||
},
|
},
|
||||||
() => "查看",
|
() => "查看",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const myClassColumns: DataTableColumn<UserRank>[] = [
|
const myClassColumns: DataTableColumn<ClassUserRank["ranks"][number]>[] = [
|
||||||
{
|
{
|
||||||
title: "排名",
|
title: "排名",
|
||||||
key: "rank",
|
key: "rank",
|
||||||
@@ -448,7 +415,7 @@ async function listClassRank() {
|
|||||||
if (!userStore.user) {
|
if (!userStore.user) {
|
||||||
await userStore.getMyProfile()
|
await userStore.getMyProfile()
|
||||||
}
|
}
|
||||||
const className = userStore.user?.class_name
|
const className = userStore.user?.className
|
||||||
if (className) {
|
if (className) {
|
||||||
classQuery.grade = parseInt(className.slice(0, 2))
|
classQuery.grade = parseInt(className.slice(0, 2))
|
||||||
}
|
}
|
||||||
@@ -464,8 +431,8 @@ async function listMyClassRank() {
|
|||||||
: 0
|
: 0
|
||||||
const limit = myClassScope.value === "all" ? myClassQuery.limit : undefined
|
const limit = myClassScope.value === "all" ? myClassQuery.limit : undefined
|
||||||
const res = await getUserClassRank(myClassScope.value, offset, limit)
|
const res = await getUserClassRank(myClassScope.value, offset, limit)
|
||||||
myRank.value = res.data.my_rank
|
myRank.value = res.data.myRank
|
||||||
myClassName.value = res.data.class_name
|
myClassName.value = res.data.className
|
||||||
myClassData.value = res.data.ranks
|
myClassData.value = res.data.ranks
|
||||||
myClassTotal.value = res.data.total ?? res.data.ranks.length
|
myClassTotal.value = res.data.total ?? res.data.ranks.length
|
||||||
if (myClassScope.value === "window") {
|
if (myClassScope.value === "window") {
|
||||||
@@ -610,7 +577,7 @@ watch(
|
|||||||
preset="card"
|
preset="card"
|
||||||
:title="
|
:title="
|
||||||
classDetailData
|
classDetailData
|
||||||
? `${classDetailData.class_name.slice(0, 2)}计算机${classDetailData.class_name.slice(2)}班`
|
? `${classDetailData.className.slice(0, 2)}计算机${classDetailData.className.slice(2)}班`
|
||||||
: '班级详情'
|
: '班级详情'
|
||||||
"
|
"
|
||||||
:style="{ width: '700px', maxWidth: '95vw' }"
|
:style="{ width: '700px', maxWidth: '95vw' }"
|
||||||
@@ -621,7 +588,7 @@ watch(
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="总AC数"
|
label="总AC数"
|
||||||
:value="classDetailData.total_ac"
|
:value="classDetailData.totalAc"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-total-ac"
|
class="stat-total-ac"
|
||||||
>
|
>
|
||||||
@@ -633,7 +600,7 @@ watch(
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="平均AC数"
|
label="平均AC数"
|
||||||
:value="classDetailData.avg_ac.toFixed(2)"
|
:value="classDetailData.avgAc.toFixed(2)"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-avg-ac"
|
class="stat-avg-ac"
|
||||||
>
|
>
|
||||||
@@ -648,7 +615,7 @@ watch(
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="中位数AC数"
|
label="中位数AC数"
|
||||||
:value="classDetailData.median_ac.toFixed(2)"
|
:value="classDetailData.medianAc.toFixed(2)"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-median-ac"
|
class="stat-median-ac"
|
||||||
>
|
>
|
||||||
@@ -663,7 +630,7 @@ watch(
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="总提交数"
|
label="总提交数"
|
||||||
:value="classDetailData.total_submission"
|
:value="classDetailData.totalSubmission"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-total-submission"
|
class="stat-total-submission"
|
||||||
>
|
>
|
||||||
@@ -678,7 +645,7 @@ watch(
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="AC率"
|
label="AC率"
|
||||||
:value="classDetailData.ac_rate.toFixed(1) + '%'"
|
:value="classDetailData.acRate.toFixed(1) + '%'"
|
||||||
size="large"
|
size="large"
|
||||||
class="stat-ac-rate"
|
class="stat-ac-rate"
|
||||||
>
|
>
|
||||||
@@ -699,12 +666,12 @@ watch(
|
|||||||
>
|
>
|
||||||
<n-descriptions-item label="第一四分位数(Q1)">
|
<n-descriptions-item label="第一四分位数(Q1)">
|
||||||
<span style="color: #9254de; font-weight: 500">{{
|
<span style="color: #9254de; font-weight: 500">{{
|
||||||
classDetailData.q1_ac.toFixed(2)
|
classDetailData.q1Ac.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="第三四分位数(Q3)">
|
<n-descriptions-item label="第三四分位数(Q3)">
|
||||||
<span style="color: #f759ab; font-weight: 500">{{
|
<span style="color: #f759ab; font-weight: 500">{{
|
||||||
classDetailData.q3_ac.toFixed(2)
|
classDetailData.q3Ac.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="四分位距(IQR)">
|
<n-descriptions-item label="四分位距(IQR)">
|
||||||
@@ -714,27 +681,27 @@ watch(
|
|||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="标准差">
|
<n-descriptions-item label="标准差">
|
||||||
<span style="color: #fa8c16; font-weight: 500">{{
|
<span style="color: #fa8c16; font-weight: 500">{{
|
||||||
classDetailData.std_dev.toFixed(2)
|
classDetailData.stdDev.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="前10%均值">
|
<n-descriptions-item label="前10%均值">
|
||||||
<span style="color: #cf1322; font-weight: 600">{{
|
<span style="color: #cf1322; font-weight: 600">{{
|
||||||
classDetailData.top10_avg.toFixed(2)
|
classDetailData.top10Avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="中间80%均值">
|
<n-descriptions-item label="中间80%均值">
|
||||||
<span style="color: #389e0d; font-weight: 600">{{
|
<span style="color: #389e0d; font-weight: 600">{{
|
||||||
classDetailData.middle80_avg.toFixed(2)
|
classDetailData.middle80Avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="后10%均值">
|
<n-descriptions-item label="后10%均值">
|
||||||
<span style="color: #096dd9; font-weight: 500">{{
|
<span style="color: #096dd9; font-weight: 500">{{
|
||||||
classDetailData.bottom10_avg.toFixed(2)
|
classDetailData.bottom10Avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="人数">
|
<n-descriptions-item label="人数">
|
||||||
<span style="color: #1890ff; font-weight: 600">{{
|
<span style="color: #1890ff; font-weight: 600">{{
|
||||||
classDetailData.user_count
|
classDetailData.userCount
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
@@ -743,35 +710,35 @@ watch(
|
|||||||
<n-space vertical :size="10">
|
<n-space vertical :size="10">
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="classDetailData.excellent_rate"
|
:percentage="classDetailData.excellentRate"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
:border-radius="4"
|
:border-radius="4"
|
||||||
>
|
>
|
||||||
<template #default
|
<template #default
|
||||||
>优秀率:
|
>优秀率:
|
||||||
{{ classDetailData.excellent_rate.toFixed(1) }}%</template
|
{{ classDetailData.excellentRate.toFixed(1) }}%</template
|
||||||
>
|
>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="classDetailData.pass_rate"
|
:percentage="classDetailData.passRate"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
:border-radius="4"
|
:border-radius="4"
|
||||||
status="success"
|
status="success"
|
||||||
>
|
>
|
||||||
<template #default
|
<template #default
|
||||||
>及格率: {{ classDetailData.pass_rate.toFixed(1) }}%</template
|
>及格率: {{ classDetailData.passRate.toFixed(1) }}%</template
|
||||||
>
|
>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="classDetailData.active_rate"
|
:percentage="classDetailData.activeRate"
|
||||||
:show-indicator="true"
|
:show-indicator="true"
|
||||||
:border-radius="4"
|
:border-radius="4"
|
||||||
status="info"
|
status="info"
|
||||||
>
|
>
|
||||||
<template #default
|
<template #default
|
||||||
>参与度: {{ classDetailData.active_rate.toFixed(1) }}%</template
|
>参与度: {{ classDetailData.activeRate.toFixed(1) }}%</template
|
||||||
>
|
>
|
||||||
</n-progress>
|
</n-progress>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -784,7 +751,7 @@ watch(
|
|||||||
style="margin-top: 12px"
|
style="margin-top: 12px"
|
||||||
>
|
>
|
||||||
<n-tag type="success" size="large">
|
<n-tag type="success" size="large">
|
||||||
综合分: {{ classDetailData.composite_score.toFixed(1) }}
|
综合分: {{ classDetailData.compositeScore.toFixed(1) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-button
|
<n-button
|
||||||
type="info"
|
type="info"
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ export const useAIStore = defineStore("ai", () => {
|
|||||||
const targetUsername = ref("")
|
const targetUsername = ref("")
|
||||||
const durationData = ref<DurationData[]>([])
|
const durationData = ref<DurationData[]>([])
|
||||||
const detailsData = reactive<DetailsData>({
|
const detailsData = reactive<DetailsData>({
|
||||||
|
user: "",
|
||||||
start: "",
|
start: "",
|
||||||
end: "",
|
end: "",
|
||||||
grade: "B",
|
grade: "",
|
||||||
class_name: "",
|
className: null,
|
||||||
tags: {},
|
tags: {},
|
||||||
difficulty: {},
|
difficulty: {},
|
||||||
contest_count: 0,
|
contestCount: 0,
|
||||||
solved: [],
|
solved: [],
|
||||||
flowcharts: [],
|
flowcharts: [],
|
||||||
})
|
})
|
||||||
@@ -44,10 +45,10 @@ export const useAIStore = defineStore("ai", () => {
|
|||||||
detailsData.end = res.data.end
|
detailsData.end = res.data.end
|
||||||
detailsData.solved = res.data.solved
|
detailsData.solved = res.data.solved
|
||||||
detailsData.grade = res.data.grade
|
detailsData.grade = res.data.grade
|
||||||
detailsData.class_name = res.data.class_name
|
detailsData.className = res.data.className
|
||||||
detailsData.tags = res.data.tags
|
detailsData.tags = res.data.tags
|
||||||
detailsData.difficulty = res.data.difficulty
|
detailsData.difficulty = res.data.difficulty
|
||||||
detailsData.contest_count = res.data.contest_count
|
detailsData.contestCount = res.data.contestCount
|
||||||
detailsData.flowcharts = res.data.flowcharts
|
detailsData.flowcharts = res.data.flowcharts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export const useContestStore = defineStore("contest", () => {
|
|||||||
|
|
||||||
const contestStatus = computed<ContestStatus>(() => {
|
const contestStatus = computed<ContestStatus>(() => {
|
||||||
if (!contest.value) return ContestStatus.initial
|
if (!contest.value) return ContestStatus.initial
|
||||||
const start = getTime(parseISO(contest.value.start_time.toString()))
|
const start = getTime(parseISO(contest.value.startTime.toString()))
|
||||||
const end = getTime(parseISO(contest.value.end_time.toString()))
|
const end = getTime(parseISO(contest.value.endTime.toString()))
|
||||||
if (start > now.value) {
|
if (start > now.value) {
|
||||||
return ContestStatus.not_started
|
return ContestStatus.not_started
|
||||||
} else if (end < now.value) {
|
} else if (end < now.value) {
|
||||||
@@ -36,10 +36,10 @@ export const useContestStore = defineStore("contest", () => {
|
|||||||
if (contestStatus.value === ContestStatus.finished) {
|
if (contestStatus.value === ContestStatus.finished) {
|
||||||
return "已结束"
|
return "已结束"
|
||||||
} else if (contestStatus.value === ContestStatus.not_started) {
|
} else if (contestStatus.value === ContestStatus.not_started) {
|
||||||
const d = duration(formatISO(now.value), contest.value!.start_time, true)
|
const d = duration(formatISO(now.value), contest.value!.startTime, true)
|
||||||
return "距离比赛开始 " + d
|
return "距离比赛开始 " + d
|
||||||
} else {
|
} else {
|
||||||
const d = duration(formatISO(now.value), contest.value!.end_time, true)
|
const d = duration(formatISO(now.value), contest.value!.endTime, true)
|
||||||
return "距离比赛结束 " + d
|
return "距离比赛结束 " + d
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -48,11 +48,11 @@ export const useContestStore = defineStore("contest", () => {
|
|||||||
() =>
|
() =>
|
||||||
userStore.isSuperAdmin ||
|
userStore.isSuperAdmin ||
|
||||||
(userStore.isAuthed &&
|
(userStore.isAuthed &&
|
||||||
contest.value?.created_by.id === userStore.user!.id),
|
contest.value?.createdBy.id === userStore.user!.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
const isPrivate = computed(
|
const isPrivate = computed(
|
||||||
() => contest.value!.contest_type === ContestType.private,
|
() => contest.value!.contestType === ContestType.private,
|
||||||
)
|
)
|
||||||
|
|
||||||
async function init(contestID: string) {
|
async function init(contestID: string) {
|
||||||
@@ -65,7 +65,7 @@ export const useContestStore = defineStore("contest", () => {
|
|||||||
now.value = now.value + 1000
|
now.value = now.value + 1000
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
if (contest.value?.contest_type === ContestType.private) {
|
if (contest.value?.contestType === ContestType.private) {
|
||||||
const res = await getContestAccess(contestID)
|
const res = await getContestAccess(contestID)
|
||||||
toggleAccess(res.data.access)
|
toggleAccess(res.data.access)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const useProblemStore = defineStore("problem", () => {
|
|||||||
const failCount = ref(0)
|
const failCount = ref(0)
|
||||||
|
|
||||||
const languages = computed<LANGUAGE[]>(() => {
|
const languages = computed<LANGUAGE[]>(() => {
|
||||||
if (route.name === "problem" && problem.value?.allow_flowchart) {
|
if (route.name === "problem" && problem.value?.allowFlowchart) {
|
||||||
return ["Flowchart", ...problem.value?.languages]
|
return ["Flowchart", ...problem.value?.languages]
|
||||||
}
|
}
|
||||||
return problem.value?.languages ?? []
|
return problem.value?.languages ?? []
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<n-card title="流程图预览">
|
<n-card title="流程图预览">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="!renderError && submission?.mermaid_code"
|
v-if="!renderError && submission?.mermaidCode"
|
||||||
quaternary
|
quaternary
|
||||||
size="small"
|
size="small"
|
||||||
@click="showLargeImage = true"
|
@click="showLargeImage = true"
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
<n-gi :span="2">
|
<n-gi :span="2">
|
||||||
<!-- AI反馈 -->
|
<!-- AI反馈 -->
|
||||||
<n-card
|
<n-card
|
||||||
v-if="submission.ai_feedback"
|
v-if="submission.aiFeedback"
|
||||||
size="small"
|
size="small"
|
||||||
title="AI反馈"
|
title="AI反馈"
|
||||||
style="margin-bottom: 16px"
|
style="margin-bottom: 16px"
|
||||||
>
|
>
|
||||||
<n-text>{{ submission.ai_feedback }}</n-text>
|
<n-text>{{ submission.aiFeedback }}</n-text>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 改进建议 -->
|
<!-- 改进建议 -->
|
||||||
@@ -69,15 +69,12 @@
|
|||||||
|
|
||||||
<!-- 详细评分 -->
|
<!-- 详细评分 -->
|
||||||
<n-card
|
<n-card
|
||||||
v-if="
|
v-if="Object.keys(criteriaDetails).length > 0"
|
||||||
submission.ai_criteria_details &&
|
|
||||||
Object.keys(submission.ai_criteria_details).length > 0
|
|
||||||
"
|
|
||||||
size="small"
|
size="small"
|
||||||
title="详细评分"
|
title="详细评分"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(detail, key) in submission.ai_criteria_details"
|
v-for="(detail, key) in criteriaDetails"
|
||||||
:key="key"
|
:key="key"
|
||||||
style="margin-bottom: 12px"
|
style="margin-bottom: 12px"
|
||||||
>
|
>
|
||||||
@@ -121,11 +118,38 @@ const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
|
|||||||
const { renderError, renderFlowchart } = useMermaid()
|
const { renderError, renderFlowchart } = useMermaid()
|
||||||
|
|
||||||
const submission = ref<FlowchartSubmission | null>(null)
|
const submission = ref<FlowchartSubmission | null>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评分项明细。契约里是 `Record<string, unknown>` —— 内容是 AI 模型原样吐出的 JSON,
|
||||||
|
* 后端不校验形状,所以这里只能按约定断言,字段缺失时用 0 / 空串兜底。
|
||||||
|
*/
|
||||||
|
const criteriaDetails = computed<
|
||||||
|
Record<string, { score: number; max: number; comment: string }>
|
||||||
|
>(() => {
|
||||||
|
const raw = submission.value?.aiCriteriaDetails ?? {}
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(raw).map(([key, value]) => {
|
||||||
|
const item = (value ?? {}) as Partial<{
|
||||||
|
score: number
|
||||||
|
max: number
|
||||||
|
comment: string
|
||||||
|
}>
|
||||||
|
return [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
score: item.score ?? 0,
|
||||||
|
max: item.max ?? 0,
|
||||||
|
comment: item.comment ?? "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rendering = ref(false)
|
const rendering = ref(false)
|
||||||
const showLargeImage = ref(false)
|
const showLargeImage = ref(false)
|
||||||
const suggestionLines = computed(() =>
|
const suggestionLines = computed(() =>
|
||||||
splitSuggestionLines(submission.value?.ai_suggestions),
|
splitSuggestionLines(submission.value?.aiSuggestions),
|
||||||
)
|
)
|
||||||
|
|
||||||
function splitSuggestionLines(suggestions?: string | null) {
|
function splitSuggestionLines(suggestions?: string | null) {
|
||||||
@@ -154,12 +178,12 @@ async function loadSubmission() {
|
|||||||
submission.value = res.data
|
submission.value = res.data
|
||||||
|
|
||||||
// 渲染流程图
|
// 渲染流程图
|
||||||
if (submission.value?.mermaid_code) {
|
if (submission.value?.mermaidCode) {
|
||||||
rendering.value = true
|
rendering.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await renderFlowchart(
|
await renderFlowchart(
|
||||||
mermaidContainer.value,
|
mermaidContainer.value,
|
||||||
submission.value.mermaid_code,
|
submission.value.mermaidCode,
|
||||||
)
|
)
|
||||||
rendering.value = false
|
rendering.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ defineProps<{
|
|||||||
function gradeType(grade: Grade) {
|
function gradeType(grade: Grade) {
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
|
"": "default",
|
||||||
S: "success",
|
S: "success",
|
||||||
A: "info",
|
A: "info",
|
||||||
B: "warning",
|
B: "warning",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-flex v-if="props.submission.show_link" align="center">
|
<n-flex v-if="props.submission.showLink" align="center">
|
||||||
<n-button text type="info" @click="$emit('showCode')">
|
<n-button text type="info" @click="$emit('showCode')">
|
||||||
{{ props.submission.id.slice(0, 12) }}
|
{{ props.submission.id.slice(0, 12) }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -43,7 +43,7 @@ defineEmits(["showCode"])
|
|||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const isOwnSubmission = computed(
|
const isOwnSubmission = computed(
|
||||||
() => userStore.profile?.user?.id === props.submission.user_id,
|
() => userStore.profile?.user?.id === props.submission.userId,
|
||||||
)
|
)
|
||||||
|
|
||||||
function goto() {
|
function goto() {
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ function copyToCat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copyToProblem() {
|
function copyToProblem() {
|
||||||
const { code, language, contest } = submission.value!
|
const { code, language, contestId } = submission.value!
|
||||||
// 编辑器的 storageKey 用 display id(problem._id),等于 props.problemID,
|
// 编辑器的 storageKey 用 display id(problem._id),等于 props.problemID,
|
||||||
// 而非 submission.problem(内部数字 id)
|
// 而非 submission.problem(内部数字 id)
|
||||||
const contestIDForKey = contest || null
|
const contestIDForKey = contestId || null
|
||||||
const storageKey = `problem_${props.problemID}_contest_${contestIDForKey}_lang_${language}`
|
const storageKey = `problem_${props.problemID}_contest_${contestIDForKey}_lang_${language}`
|
||||||
storage.set(storageKey, code)
|
storage.set(storageKey, code)
|
||||||
// 设置语言 + 代码:localStorage 覆盖全新挂载的编辑器,
|
// 设置语言 + 代码:localStorage 覆盖全新挂载的编辑器,
|
||||||
@@ -89,10 +89,10 @@ function copyToProblem() {
|
|||||||
codeStore.setCode(code)
|
codeStore.setCode(code)
|
||||||
|
|
||||||
const problemSetId = (route.params.problemSetId as string) ?? ""
|
const problemSetId = (route.params.problemSetId as string) ?? ""
|
||||||
if (contest) {
|
if (contestId) {
|
||||||
router.push({
|
router.push({
|
||||||
name: "contest problem",
|
name: "contest problem",
|
||||||
params: { contestID: String(contest), problemID: props.problemID },
|
params: { contestID: String(contestId), problemID: props.problemID },
|
||||||
})
|
})
|
||||||
} else if (problemSetId) {
|
} else if (problemSetId) {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -121,7 +121,7 @@ onMounted(init)
|
|||||||
:title="JUDGE_STATUS[submission.result]['title']"
|
:title="JUDGE_STATUS[submission.result]['title']"
|
||||||
>
|
>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<span>提交时间:{{ parseTime(submission.create_time) }}</span>
|
<span>提交时间:{{ parseTime(submission.createTime) }}</span>
|
||||||
<span>编程语言:{{ LANGUAGE_SHOW_VALUE[submission.language] }}</span>
|
<span>编程语言:{{ LANGUAGE_SHOW_VALUE[submission.language] }}</span>
|
||||||
<span>用户:{{ submission.username }}</span>
|
<span>用户:{{ submission.username }}</span>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
retryFlowchartSubmission,
|
retryFlowchartSubmission,
|
||||||
} from "oj/api"
|
} from "oj/api"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
|
import type { Grade as GradeValue } from "utils/types"
|
||||||
import type {
|
import type {
|
||||||
FlowchartSubmissionListItem,
|
FlowchartSubmissionListItem,
|
||||||
LANGUAGE,
|
LANGUAGE,
|
||||||
@@ -101,7 +102,7 @@ async function listSubmissions() {
|
|||||||
if (query.language === "Flowchart") {
|
if (query.language === "Flowchart") {
|
||||||
const res = await getFlowchartSubmissions({
|
const res = await getFlowchartSubmissions({
|
||||||
username: query.username,
|
username: query.username,
|
||||||
problem_id: query.problem,
|
problemId: query.problem,
|
||||||
myself: query.myself,
|
myself: query.myself,
|
||||||
offset,
|
offset,
|
||||||
limit: query.limit,
|
limit: query.limit,
|
||||||
@@ -114,8 +115,8 @@ async function listSubmissions() {
|
|||||||
const res = await getSubmissions({
|
const res = await getSubmissions({
|
||||||
...query,
|
...query,
|
||||||
offset,
|
offset,
|
||||||
problem_id: query.problem,
|
problemId: query.problem,
|
||||||
contest_id: (route.params.contestID as string) ?? "",
|
contestId: (route.params.contestID as string) ?? "",
|
||||||
language: query.language,
|
language: query.language,
|
||||||
today: query.today,
|
today: query.today,
|
||||||
})
|
})
|
||||||
@@ -233,7 +234,7 @@ const columns = computed(() => {
|
|||||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
|
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
|
||||||
@@ -263,7 +264,7 @@ const columns = computed(() => {
|
|||||||
onClick: () => problemClicked(row),
|
onClick: () => problemClicked(row),
|
||||||
onSearch: () => (query.problem = row.problem),
|
onSearch: () => (query.problem = row.problem),
|
||||||
},
|
},
|
||||||
() => `${row.problem} ${row.problem_title}`,
|
() => `${row.problem} ${row.problemTitle}`,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -318,7 +319,7 @@ const flowchartColumns = computed(() => {
|
|||||||
{
|
{
|
||||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
|
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
|
||||||
@@ -340,7 +341,7 @@ const flowchartColumns = computed(() => {
|
|||||||
onClick: () => problemClicked(row),
|
onClick: () => problemClicked(row),
|
||||||
onSearch: () => (query.problem = row.problem),
|
onSearch: () => (query.problem = row.problem),
|
||||||
},
|
},
|
||||||
() => `${row.problem} ${row.problem_title}`,
|
() => `${row.problem} ${row.problemTitle}`,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -349,7 +350,11 @@ const flowchartColumns = computed(() => {
|
|||||||
"streamline-ultimate-color:analytics-bars-3d",
|
"streamline-ultimate-color:analytics-bars-3d",
|
||||||
),
|
),
|
||||||
key: "ai_score",
|
key: "ai_score",
|
||||||
render: (row) => h(Grade, { score: row.ai_score, grade: row.ai_grade }),
|
render: (row) =>
|
||||||
|
h(Grade, {
|
||||||
|
score: row.aiScore ?? 0,
|
||||||
|
grade: (row.aiGrade ?? "") as GradeValue,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle(
|
title: renderTableTitle(
|
||||||
@@ -545,9 +550,9 @@ const flowchartColumns = computed(() => {
|
|||||||
<n-text>流程图评分详情</n-text>
|
<n-text>流程图评分详情</n-text>
|
||||||
<n-text
|
<n-text
|
||||||
v-if="selectedFlowchart"
|
v-if="selectedFlowchart"
|
||||||
:type="getGradeType(selectedFlowchart.ai_grade)"
|
:type="getGradeType(selectedFlowchart.aiGrade ?? '')"
|
||||||
>
|
>
|
||||||
{{ selectedFlowchart.ai_score }}分 {{ selectedFlowchart.ai_grade }}级
|
{{ selectedFlowchart.aiScore }}分 {{ selectedFlowchart.aiGrade }}级
|
||||||
</n-text>
|
</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import { DIFFICULTY } from "utils/constants"
|
import { DIFFICULTY } from "utils/constants"
|
||||||
import { getACRate } from "utils/functions"
|
import { getACRate } from "utils/functions"
|
||||||
import type { Problem } from "utils/types"
|
import type { ProblemFiltered, ProblemListItem } from "utils/types"
|
||||||
|
|
||||||
// 把后端的 Problem 塑形成列表项需要的形状,与请求逻辑解耦。
|
// 把后端的列表项塑形成列表页需要的形状,与请求逻辑解耦。
|
||||||
export function filterResult(result: Problem) {
|
export function filterResult(result: ProblemListItem): ProblemFiltered {
|
||||||
const newResult = {
|
return {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
_id: result._id,
|
_id: result._id,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
difficulty: DIFFICULTY[result.difficulty],
|
difficulty: DIFFICULTY[result.difficulty],
|
||||||
tags: result.tags,
|
tags: result.tags,
|
||||||
submission: result.submission_number,
|
submission: result.submissionNumber,
|
||||||
rate: getACRate(result.accepted_number, result.submission_number),
|
rate: getACRate(result.acceptedNumber, result.submissionNumber),
|
||||||
status: "",
|
// null / undefined 都表示「没做过」
|
||||||
author: result.created_by.username,
|
status:
|
||||||
allow_flowchart: result.allow_flowchart,
|
result.myStatus === null || result.myStatus === undefined
|
||||||
show_flowchart: result.show_flowchart,
|
? "not_test"
|
||||||
has_ast_rules: result.has_ast_rules,
|
: result.myStatus === 0
|
||||||
|
? "passed"
|
||||||
|
: "failed",
|
||||||
|
author: result.createdBy.username,
|
||||||
|
allowFlowchart: result.allowFlowchart,
|
||||||
|
showFlowchart: result.showFlowchart,
|
||||||
|
hasAstRules: result.hasAstRules,
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ async function init() {
|
|||||||
try {
|
try {
|
||||||
const res = await getProfile(route.query.name as string)
|
const res = await getProfile(route.query.name as string)
|
||||||
profile.value = res.data
|
profile.value = res.data
|
||||||
const acm = res.data.acm_problems_status.problems || {}
|
const acm = res.data!.acmProblemsStatus.problems || {}
|
||||||
const ac: string[] = []
|
const ac: string[] = []
|
||||||
Object.keys(acm).forEach((id) => {
|
Object.keys(acm).forEach((id) => {
|
||||||
if (acm[id]["status"] === 0) {
|
if (acm[id]["status"] === 0) {
|
||||||
@@ -74,7 +74,7 @@ async function init() {
|
|||||||
ac.sort()
|
ac.sort()
|
||||||
problems.value = ac
|
problems.value = ac
|
||||||
|
|
||||||
if (profile.value.submission_number > 0) {
|
if (profile.value.submissionNumber > 0) {
|
||||||
const metricsRes = await getMetrics(profile.value.user.id)
|
const metricsRes = await getMetrics(profile.value.user.id)
|
||||||
firstSubmissionAt.value = parseTime(metricsRes.data.first)
|
firstSubmissionAt.value = parseTime(metricsRes.data.first)
|
||||||
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
|
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
|
||||||
@@ -130,13 +130,13 @@ const metrics = computed(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "fluent-emoji:candy",
|
icon: "fluent-emoji:candy",
|
||||||
title: profile.value?.accepted_number ?? 0,
|
title: profile.value?.acceptedNumber ?? 0,
|
||||||
content: "已解决的题目数量",
|
content: "已解决的题目数量",
|
||||||
animate: true,
|
animate: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "fluent-emoji:thinking-face",
|
icon: "fluent-emoji:thinking-face",
|
||||||
title: profile.value?.submission_number ?? 0,
|
title: profile.value?.submissionNumber ?? 0,
|
||||||
content: "总提交数量",
|
content: "总提交数量",
|
||||||
animate: true,
|
animate: true,
|
||||||
},
|
},
|
||||||
@@ -186,7 +186,7 @@ onMounted(() => {
|
|||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<n-grid
|
<n-grid
|
||||||
v-if="profile && profile.submission_number > 0"
|
v-if="profile && profile.submissionNumber > 0"
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
:cols="isDesktop ? 2 : 1"
|
:cols="isDesktop ? 2 : 1"
|
||||||
:x-gap="10"
|
:x-gap="10"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<n-flex size="large" vertical>
|
<n-flex size="large" vertical>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<div>发送时间</div>
|
<div>发送时间</div>
|
||||||
<div>{{ parseTime(item.create_time, "YYYY年M月D日 HH:mm:ss") }}</div>
|
<div>{{ parseTime(item.createTime, "YYYY年M月D日 HH:mm:ss") }}</div>
|
||||||
<div>发送者</div>
|
<div>发送者</div>
|
||||||
<div>{{ item.sender.username }}</div>
|
<div>{{ item.sender.username }}</div>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ async function upload({ file }: UploadCustomRequestOptions) {
|
|||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
try {
|
try {
|
||||||
await updateProfile({
|
await updateProfile({
|
||||||
real_name: userStore.profile?.real_name ?? "",
|
realName: userStore.profile?.realName ?? "",
|
||||||
mood: userStore.profile?.mood ?? "",
|
mood: userStore.profile?.mood ?? "",
|
||||||
})
|
})
|
||||||
message.success("更改成功")
|
message.success("更改成功")
|
||||||
@@ -55,7 +55,7 @@ async function saveProfile() {
|
|||||||
</n-upload>
|
</n-upload>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<!-- <n-form-item label="真名">
|
<!-- <n-form-item label="真名">
|
||||||
<n-input v-model:value="userStore.profile.real_name" />
|
<n-input v-model:value="userStore.profile.realName" />
|
||||||
</n-form-item> -->
|
</n-form-item> -->
|
||||||
<n-form-item label="个性签名">
|
<n-form-item label="个性签名">
|
||||||
<n-input v-model:value="userStore.profile.mood" />
|
<n-input v-model:value="userStore.profile.mood" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { userProfileSchema } from "@oj2/contract"
|
import { userProfileSchema } from "@oj2/contract"
|
||||||
import api2 from "utils/api2"
|
import api2 from "utils/api2"
|
||||||
import type { ApiResponse } from "utils/http"
|
import type { ApiResponse } from "utils/api2"
|
||||||
import type { Profile, Tag } from "utils/types"
|
import type { Profile, Tag } from "utils/types"
|
||||||
|
|
||||||
export function login(data: { username: string; password: string }) {
|
export function login(data: { username: string; password: string }) {
|
||||||
@@ -26,37 +26,10 @@ export async function getProfile(
|
|||||||
username ? `profiles/${encodeURIComponent(username)}` : "me",
|
username ? `profiles/${encodeURIComponent(username)}` : "me",
|
||||||
)
|
)
|
||||||
if (response.data === null) return { error: null, data: null }
|
if (response.data === null) return { error: null, data: null }
|
||||||
const profile = userProfileSchema.parse(response.data)
|
// 形状与契约一致,不再逐字段搬运;zod 解析仍保留,形状对不上要当场炸
|
||||||
return {
|
return {
|
||||||
error: null,
|
error: null,
|
||||||
data: {
|
data: userProfileSchema.parse(response.data) as Profile,
|
||||||
id: profile.id,
|
|
||||||
user: {
|
|
||||||
id: profile.user.id,
|
|
||||||
username: profile.user.username,
|
|
||||||
real_name: profile.realName ?? "",
|
|
||||||
email: profile.user.email ?? "",
|
|
||||||
admin_type: profile.user.adminType as Profile["user"]["admin_type"],
|
|
||||||
problem_permission: profile.user.problemPermission,
|
|
||||||
create_time: profile.user.createTime as unknown as Date,
|
|
||||||
last_login: profile.user.lastLogin as unknown as Date,
|
|
||||||
open_api: profile.user.openApi,
|
|
||||||
is_disabled: profile.user.isDisabled,
|
|
||||||
class_name: profile.user.className,
|
|
||||||
},
|
|
||||||
real_name: profile.realName ?? "",
|
|
||||||
acm_problems_status:
|
|
||||||
profile.acmProblemsStatus as Profile["acm_problems_status"],
|
|
||||||
avatar: profile.avatar,
|
|
||||||
blog: profile.blog as null,
|
|
||||||
mood: profile.mood ?? "",
|
|
||||||
github: profile.github ?? "",
|
|
||||||
school: profile.school ?? "",
|
|
||||||
major: profile.major ?? "",
|
|
||||||
language: profile.language ?? "",
|
|
||||||
accepted_number: profile.acceptedNumber,
|
|
||||||
submission_number: profile.submissionNumber,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,10 @@ const authorOptions = ref([{ label: "全部", value: "" }])
|
|||||||
async function getAuthorOptions() {
|
async function getAuthorOptions() {
|
||||||
authorOptions.value = [{ label: "全部", value: "" }]
|
authorOptions.value = [{ label: "全部", value: "" }]
|
||||||
const res = await getAuthors(all)
|
const res = await getAuthors(all)
|
||||||
const remotes = res.data.map(
|
const remotes = res.data.map((item) => ({
|
||||||
(item: { username: string; problem_count: number }) => ({
|
label: `${item.username} (${item.problemCount})`,
|
||||||
label: `${item.username} (${item.problem_count})`,
|
value: item.username,
|
||||||
value: item.username,
|
}))
|
||||||
}),
|
|
||||||
)
|
|
||||||
authorOptions.value = [...authorOptions.value, ...remotes]
|
authorOptions.value = [...authorOptions.value, ...remotes]
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import { ContestType } from "utils/constants"
|
import { ContestType } from "utils/constants"
|
||||||
import type { Contest } from "utils/types"
|
import type { Contest, OjContest } from "utils/types"
|
||||||
|
|
||||||
defineProps<{ contest: Contest }>()
|
defineProps<{ contest: Contest | OjContest }>()
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<Icon
|
<Icon
|
||||||
v-if="contest.contest_type === ContestType.private"
|
v-if="contest.contestType === ContestType.private"
|
||||||
:height="24"
|
:height="24"
|
||||||
icon="streamline-ultimate-color:shield-lock"
|
icon="streamline-ultimate-color:shield-lock"
|
||||||
></Icon>
|
></Icon>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ContestType } from "utils/constants"
|
import { ContestType } from "utils/constants"
|
||||||
import type { Contest } from "utils/types"
|
import type { Contest, OjContest } from "utils/types"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
contest: Contest
|
contest: Contest | OjContest
|
||||||
size?: "small"
|
size?: "small"
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const isPrivate = computed(
|
const isPrivate = computed(
|
||||||
() => props.contest.contest_type === ContestType.private,
|
() => props.contest.contestType === ContestType.private,
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ function handleMenuSelect(key: string) {
|
|||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<n-flex align="center" class="title" @click="goHome">
|
<n-flex align="center" class="title" @click="goHome">
|
||||||
<Icon icon="streamline-emojis:dog" :height="30"></Icon>
|
<Icon icon="streamline-emojis:dog" :height="30"></Icon>
|
||||||
<div>{{ configStore.config?.website_name }}</div>
|
<div>{{ configStore.config?.websiteName }}</div>
|
||||||
<div v-if="showEnvVersion">({{ envVersion }})</div>
|
<div v-if="showEnvVersion">({{ envVersion }})</div>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<div>
|
<div>
|
||||||
@@ -331,7 +331,7 @@ function handleMenuSelect(key: string) {
|
|||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
tertiary
|
tertiary
|
||||||
v-if="configStore.config?.allow_register"
|
v-if="configStore.config?.allowRegister"
|
||||||
@click="authStore.openSignupModal()"
|
@click="authStore.openSignupModal()"
|
||||||
>
|
>
|
||||||
注册
|
注册
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const isClassLogin = computed(() => Boolean(form.value.class))
|
|||||||
const classList = computed<SelectOption[]>(() => {
|
const classList = computed<SelectOption[]>(() => {
|
||||||
const defaults = [{ label: "没有我所在的班级", value: "" }]
|
const defaults = [{ label: "没有我所在的班级", value: "" }]
|
||||||
const configs =
|
const configs =
|
||||||
configStore.config?.class_list.map((item) => ({
|
configStore.config?.classList.map((item) => ({
|
||||||
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
||||||
value: `ks${item}`,
|
value: `ks${item}`,
|
||||||
})) ?? []
|
})) ?? []
|
||||||
@@ -53,9 +53,10 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
await login(merged)
|
await login(merged)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.data === "Your account has been disabled") {
|
// 判错误码而不是错误文案:文案在后端,改一个字这里就静默掉进「无法登录」
|
||||||
|
if (err.error === "account-disabled") {
|
||||||
authStore.setLoginError("此账号已被封禁")
|
authStore.setLoginError("此账号已被封禁")
|
||||||
} else if (err.data === "Invalid username or password") {
|
} else if (err.error === "invalid-credentials") {
|
||||||
authStore.setLoginError("用户名或密码不正确")
|
authStore.setLoginError("用户名或密码不正确")
|
||||||
} else {
|
} else {
|
||||||
authStore.setLoginError("无法登录")
|
authStore.setLoginError("无法登录")
|
||||||
@@ -177,12 +178,12 @@ onMounted(() => {
|
|||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
:style="{
|
:style="{
|
||||||
flex: configStore.config?.allow_register ? '0 0 auto' : '1',
|
flex: configStore.config?.allowRegister ? '0 0 auto' : '1',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="configStore.config?.allow_register" @click="goSignup">
|
<n-button v-if="configStore.config?.allowRegister" @click="goSignup">
|
||||||
没有账号?立即注册
|
没有账号?立即注册
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|||||||
@@ -33,33 +33,31 @@ const hasAnalysis = computed(() => !!loginSummaryStore.analysis)
|
|||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="新增题目"
|
label="新增题目"
|
||||||
:value="loginSummaryStore.summary?.new_problem_count ?? 0"
|
:value="loginSummaryStore.summary?.newProblemCount ?? 0"
|
||||||
/>
|
/>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="提交次数"
|
label="提交次数"
|
||||||
:value="loginSummaryStore.summary?.submission_count ?? 0"
|
:value="loginSummaryStore.summary?.submissionCount ?? 0"
|
||||||
/>
|
/>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="AC 次数"
|
label="AC 次数"
|
||||||
:value="loginSummaryStore.summary?.accepted_count ?? 0"
|
:value="loginSummaryStore.summary?.acceptedCount ?? 0"
|
||||||
/>
|
/>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="AC 题目数"
|
label="AC 题目数"
|
||||||
:value="loginSummaryStore.summary?.solved_count ?? 0"
|
:value="loginSummaryStore.summary?.solvedCount ?? 0"
|
||||||
/>
|
/>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="流程图提交"
|
label="流程图提交"
|
||||||
:value="
|
:value="loginSummaryStore.summary?.flowchartSubmissionCount ?? 0"
|
||||||
loginSummaryStore.summary?.flowchart_submission_count ?? 0
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ function submit() {
|
|||||||
password: form.value.password,
|
password: form.value.password,
|
||||||
})
|
})
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.data === "Username already exists") {
|
if (err.error === "username-exists") {
|
||||||
authStore.setSignupError("用户名已存在")
|
authStore.setSignupError("用户名已存在")
|
||||||
} else if (err.data === "Email already exists") {
|
} else if (err.error === "email-exists") {
|
||||||
authStore.setSignupError("邮箱已存在")
|
authStore.setSignupError("邮箱已存在")
|
||||||
} else {
|
} else {
|
||||||
authStore.setSignupError("无法注册")
|
authStore.setSignupError("无法注册")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<n-text strong>{{ badge.badge.name }}</n-text>
|
<n-text strong>{{ badge.badge.name }}</n-text>
|
||||||
<n-tag type="info"> 获得条件:{{ getConditionText() }} </n-tag>
|
<n-tag type="info"> 获得条件:{{ getConditionText() }} </n-tag>
|
||||||
<n-text depth="3">
|
<n-text depth="3">
|
||||||
获得时间:{{ parseTime(badge.earned_time, "YYYY-MM-DD HH:mm:ss") }}
|
获得时间:{{ parseTime(badge.earnedTime, "YYYY-MM-DD HH:mm:ss") }}
|
||||||
</n-text>
|
</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -38,15 +38,15 @@ function handleImageError(event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getConditionText() {
|
function getConditionText() {
|
||||||
const { condition_type, condition_value } = props.badge.badge
|
const { conditionType, conditionValue } = props.badge.badge
|
||||||
|
|
||||||
switch (condition_type) {
|
switch (conditionType) {
|
||||||
case "all_problems":
|
case "all_problems":
|
||||||
return "完成所有题目"
|
return "完成所有题目"
|
||||||
case "problem_count":
|
case "problem_count":
|
||||||
return `完成 ${condition_value} 道题目`
|
return `完成 ${conditionValue} 道题目`
|
||||||
case "score":
|
case "score":
|
||||||
return `获得 ${condition_value} 分`
|
return `获得 ${conditionValue} 分`
|
||||||
default:
|
default:
|
||||||
return "未知条件"
|
return "未知条件"
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user