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:
@@ -1,26 +1,14 @@
|
||||
import api2 from "utils/api2"
|
||||
import type {
|
||||
Achievement,
|
||||
AchievementList,
|
||||
AchievementSummary,
|
||||
PendingAchievement,
|
||||
} from "utils/types"
|
||||
|
||||
export function getAchievements(name?: string) {
|
||||
return api2
|
||||
.get<any>("achievements", { 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,
|
||||
}),
|
||||
),
|
||||
},
|
||||
}))
|
||||
return api2.get<AchievementList>("achievements", {
|
||||
params: name ? { username: name } : {},
|
||||
})
|
||||
}
|
||||
|
||||
export function getAchievementSummary(name?: string) {
|
||||
|
||||
@@ -17,7 +17,7 @@ const masked = computed(
|
||||
|
||||
// 获得率低于 5% 的加稀有闪光边框
|
||||
const isRare = computed(
|
||||
() => props.achievement.unlock_rate > 0 && props.achievement.unlock_rate < 5,
|
||||
() => props.achievement.unlockRate > 0 && props.achievement.unlockRate < 5,
|
||||
)
|
||||
|
||||
// 只有"越多越好"的成就画进度条。lte 类(如最短 AC 代码 ≤ 50 字符)
|
||||
@@ -45,10 +45,10 @@ const percent = computed(() => {
|
||||
})
|
||||
|
||||
const unlockDate = computed(() => {
|
||||
const { unlock_time, backfilled } = props.achievement
|
||||
const { unlockTime, backfilled } = props.achievement
|
||||
// 补发的记录不显示具体日期:一次补发会给几百人盖上同一个时间戳
|
||||
if (backfilled || !unlock_time) return "已获得"
|
||||
return `${new Date(unlock_time).toLocaleDateString()} 获得`
|
||||
if (backfilled || !unlockTime) return "已获得"
|
||||
return `${new Date(unlockTime).toLocaleDateString()} 获得`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -86,7 +86,7 @@ const unlockDate = computed(() => {
|
||||
<template v-if="achievement.unlocked">
|
||||
<n-text depth="3" class="nowrap">{{ unlockDate }}</n-text>
|
||||
<n-text depth="3" class="nowrap">
|
||||
仅 {{ achievement.unlock_rate }}% 的人获得
|
||||
仅 {{ achievement.unlockRate }}% 的人获得
|
||||
</n-text>
|
||||
</template>
|
||||
|
||||
@@ -113,7 +113,7 @@ const unlockDate = computed(() => {
|
||||
</template>
|
||||
|
||||
<n-text v-else depth="3" class="nowrap">
|
||||
仅 {{ achievement.unlock_rate }}% 的人获得
|
||||
仅 {{ achievement.unlockRate }}% 的人获得
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-thing>
|
||||
|
||||
@@ -7,25 +7,10 @@ import type {
|
||||
Achievement,
|
||||
AchievementRarity,
|
||||
AchievementSummary,
|
||||
UserBadge,
|
||||
} from "utils/types"
|
||||
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 name = computed(() => (route.query.name as string) || undefined)
|
||||
|
||||
@@ -72,7 +57,7 @@ async function load() {
|
||||
// http 客户端返回 ApiResponse<T>,真实载荷在 .data 里
|
||||
achievements.value = list.data.achievements
|
||||
summary.value = sum.data
|
||||
badges.value = (badgeRes.data ?? []) as UserBadge[]
|
||||
badges.value = badgeRes.data ?? []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ const data = computed(() => {
|
||||
// 根据等级返回对应的颜色
|
||||
function getGradeColor(grade: Grade): string {
|
||||
const colors: { [key in Grade]: string } = {
|
||||
"": "#C9CDD4", // 无评级:后端在没有可用数据时下发空串
|
||||
S: "#FF6384",
|
||||
A: "#FFCE56",
|
||||
B: "#36A2EB",
|
||||
|
||||
@@ -73,14 +73,14 @@ const data = computed<ChartData<"bar" | "line">>(() => {
|
||||
{
|
||||
type: "bar",
|
||||
label: "完成题目数",
|
||||
data: aiStore.durationData.map((duration) => duration.problem_count),
|
||||
data: aiStore.durationData.map((duration) => duration.problemCount),
|
||||
yAxisID: "y",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
label: "总提交次数",
|
||||
data: aiStore.durationData.map((duration) => duration.submission_count),
|
||||
data: aiStore.durationData.map((duration) => duration.submissionCount),
|
||||
yAxisID: "y",
|
||||
order: 2,
|
||||
},
|
||||
|
||||
@@ -59,8 +59,8 @@ const show = computed(() => {
|
||||
// 计算提交效率数据
|
||||
const efficiencyData = computed(() => {
|
||||
return aiStore.durationData.map((duration) => {
|
||||
const problemCount = duration.problem_count || 0
|
||||
const submissionCount = duration.submission_count || 0
|
||||
const problemCount = duration.problemCount || 0
|
||||
const submissionCount = duration.submissionCount || 0
|
||||
|
||||
// 计算效率:提交次数/完成题目数
|
||||
// 值越接近1,说明一次AC率越高
|
||||
|
||||
@@ -28,8 +28,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { Grade } from "utils/types"
|
||||
const props = defineProps<{
|
||||
grade: "S" | "A" | "B" | "C"
|
||||
// 空串是「无评级」,四张图都不渲染 —— 后端没有可用数据时会下发它
|
||||
grade: Grade
|
||||
}>()
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<span>你一共解决 </span>
|
||||
<b class="charming"> {{ aiStore.detailsData.solved.length }} </b>
|
||||
<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>
|
||||
@@ -49,6 +49,7 @@ const durationLabel = computed(() => {
|
||||
|
||||
const greeting = computed(() => {
|
||||
return {
|
||||
"": "还没有足够的数据来评级",
|
||||
S: "要不试试高难度题目?",
|
||||
A: "你很棒,继续保持!",
|
||||
B: "请再接再厉!",
|
||||
|
||||
@@ -44,6 +44,7 @@ const aiStore = useAIStore()
|
||||
|
||||
const gradeOrder = ["C", "B", "A", "S"] as const
|
||||
const gradeColors: Record<Grade, string> = {
|
||||
"": "#C9CDD4", // 无评级:后端在没有可用数据时下发空串
|
||||
C: "#95F204",
|
||||
B: "#36A2EB",
|
||||
A: "#FFCE56",
|
||||
@@ -74,7 +75,7 @@ const progressData = computed(() => {
|
||||
let totalProblems = 0 // 累计题目总数
|
||||
|
||||
return aiStore.durationData.map((duration) => {
|
||||
const problemCount = duration.problem_count || 0
|
||||
const problemCount = duration.problemCount || 0
|
||||
cumulativeCount += problemCount
|
||||
|
||||
// 计算本期等级的权重值
|
||||
|
||||
@@ -38,8 +38,8 @@ const rankDistribution = computed(() => {
|
||||
}))
|
||||
|
||||
aiStore.detailsData.solved.forEach((item) => {
|
||||
const rank = item.period_rank
|
||||
const acCount = item.period_ac_count
|
||||
const rank = item.periodRank
|
||||
const acCount = item.periodAcCount
|
||||
|
||||
if (rank && acCount && acCount > 0) {
|
||||
const percentile = (rank / acCount) * 100
|
||||
@@ -52,7 +52,7 @@ const rankDistribution = computed(() => {
|
||||
if (rangeIndex !== -1) {
|
||||
distribution[rangeIndex].count++
|
||||
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,
|
||||
onClick: () => {
|
||||
if (row.problem.contest_id) {
|
||||
if (row.problem.contestId) {
|
||||
router.push(
|
||||
"/contest/" +
|
||||
row.problem.contest_id +
|
||||
row.problem.contestId +
|
||||
"/problem/" +
|
||||
row.problem.display_id,
|
||||
row.problem.displayId,
|
||||
)
|
||||
} 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 })
|
||||
} 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",
|
||||
width: 100,
|
||||
align: "center",
|
||||
render: (row) => row.rank + " / " + row.ac_count,
|
||||
render: (row) => row.rank + " / " + row.acCount,
|
||||
},
|
||||
{
|
||||
title: "同期排名",
|
||||
key: "period_rank",
|
||||
width: 100,
|
||||
align: "center",
|
||||
render: (row) => row.period_rank + " / " + row.period_ac_count,
|
||||
render: (row) => row.periodRank + " / " + row.periodAcCount,
|
||||
},
|
||||
{
|
||||
title: () =>
|
||||
@@ -128,10 +128,10 @@ const flowchartsColumns: DataTableColumn<FlowchartSummary>[] = [
|
||||
{
|
||||
text: true,
|
||||
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" },
|
||||
@@ -140,14 +140,14 @@ const flowchartsColumns: DataTableColumn<FlowchartSummary>[] = [
|
||||
key: "best",
|
||||
width: 100,
|
||||
align: "center",
|
||||
render: (row) => `${row.best_score} (${row.best_grade})`,
|
||||
render: (row) => `${row.bestScore} (${row.bestGrade})`,
|
||||
},
|
||||
{
|
||||
title: "最新提交时间",
|
||||
key: "latest_submission_time",
|
||||
width: 200,
|
||||
align: "center",
|
||||
render: (row) => parseTime(row.latest_submission_time),
|
||||
render: (row) => parseTime(row.latestSubmissionTime),
|
||||
},
|
||||
{ title: "平均分", key: "avg_score", width: 100, align: "center" },
|
||||
]
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
<n-flex vertical align="start">
|
||||
<n-flex align="center">
|
||||
<n-tag type="info" size="small" :bordered="false">比赛</n-tag>
|
||||
<span>{{ problem.contest_title }}</span>
|
||||
<span>{{ problem.contestTitle }}</span>
|
||||
</n-flex>
|
||||
<span>{{ problem.display_id }} {{ problem.title }}</span>
|
||||
<span>{{ problem.displayId }} {{ problem.title }}</span>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { SolvedProblem } from "utils/types"
|
||||
interface Props {
|
||||
problem: {
|
||||
title: string
|
||||
display_id: string
|
||||
contest_title: string
|
||||
contest_id: number
|
||||
}
|
||||
problem: SolvedProblem["problem"]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
@@ -50,7 +50,7 @@ const activityMatrix = computed(() => {
|
||||
|
||||
// 统计数据
|
||||
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 hour = date.getHours() // 0-23
|
||||
|
||||
|
||||
@@ -33,15 +33,15 @@ const columns: DataTableColumn<Announcement>[] = [
|
||||
render: (row) => h(NTag, () => row.tag || "公告"),
|
||||
},
|
||||
{
|
||||
key: "create_time",
|
||||
key: "createTime",
|
||||
title: renderTableTitle("发布时间", "fluent-emoji-flat:eight-oclock"),
|
||||
render: (row) => parseTime(row.create_time),
|
||||
render: (row) => parseTime(row.createTime),
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
key: "username",
|
||||
title: renderTableTitle("发布人", "streamline-emojis:ghost"),
|
||||
render: (row) => row.created_by.username,
|
||||
render: (row) => row.createdBy.username,
|
||||
width: 120,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,15 +1,46 @@
|
||||
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,
|
||||
submissionDetailSchema,
|
||||
type FlowchartStatistics,
|
||||
type SubmissionStatistics,
|
||||
} from "@oj2/contract"
|
||||
import api2 from "utils/api2"
|
||||
import { legacyResponse, toLegacy } from "utils/legacy"
|
||||
import type { ApiResponse } from "utils/http"
|
||||
import { filterResult } from "oj/transforms"
|
||||
import type {
|
||||
Announcement,
|
||||
Profile,
|
||||
Message,
|
||||
SubmissionListItem,
|
||||
Exercise,
|
||||
Problem,
|
||||
ReactionKey,
|
||||
@@ -18,97 +49,39 @@ import type {
|
||||
SubmissionListPayload,
|
||||
SubmitCodePayload,
|
||||
WebsiteConfig,
|
||||
Tutorial,
|
||||
} from "utils/types"
|
||||
|
||||
function listProblem(value: any): Problem {
|
||||
return {
|
||||
id: value.id,
|
||||
_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
|
||||
}
|
||||
|
||||
/**
|
||||
* 题目详情。走契约的 zod 解析,形状即契约 —— 之前这里手抄了一份 camel→snake 的
|
||||
* 键名映射,抄漏一个字段就是静默 undefined。
|
||||
*/
|
||||
function detailProblem(value: unknown): Problem {
|
||||
const problem = problemDetailSchema.parse(value)
|
||||
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
|
||||
return problemDetailSchema.parse(value) as Problem
|
||||
}
|
||||
|
||||
export function getWebsiteConfig() {
|
||||
return legacyResponse<WebsiteConfig>(api2.get("site"))
|
||||
return api2.get<WebsiteConfig>("site")
|
||||
}
|
||||
|
||||
export async function getProblemList(
|
||||
offset = 0,
|
||||
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 },
|
||||
})
|
||||
return {
|
||||
results: res.data.results.map(listProblem).map(filterResult),
|
||||
results: res.data.results.map(filterResult),
|
||||
total: res.data.total,
|
||||
}
|
||||
}
|
||||
|
||||
export function getAuthors(all = false) {
|
||||
return legacyResponse(
|
||||
api2.get("problem-authors", {
|
||||
params: {
|
||||
all: all ? "1" : "0",
|
||||
},
|
||||
}),
|
||||
)
|
||||
return api2.get<ProblemAuthor[]>("problem-authors", {
|
||||
params: { all: all ? "1" : "0" },
|
||||
})
|
||||
}
|
||||
|
||||
export function getRandomProblemID() {
|
||||
@@ -131,41 +104,14 @@ export async function getSubmission(id: string) {
|
||||
const response = await api2.get<unknown>(
|
||||
`submissions/${encodeURIComponent(id)}`,
|
||||
)
|
||||
const submission = submissionDetailSchema.parse(response.data)
|
||||
return {
|
||||
error: null,
|
||||
data: {
|
||||
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,
|
||||
data: submissionDetailSchema.parse(response.data) as Submission,
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitCode(data: SubmitCodePayload) {
|
||||
const response = await api2.post<unknown>("submissions", {
|
||||
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 submitCode(data: SubmitCodePayload) {
|
||||
return api2.post<CreateSubmissionResponse>("submissions", data)
|
||||
}
|
||||
|
||||
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>) {
|
||||
const endpoint = params.contest_id
|
||||
? `contests/${encodeURIComponent(params.contest_id)}/submissions`
|
||||
const endpoint = params.contestId
|
||||
? `contests/${encodeURIComponent(params.contestId)}/submissions`
|
||||
: "submissions"
|
||||
return legacyResponse(
|
||||
api2.get(endpoint, {
|
||||
params: {
|
||||
...params,
|
||||
problemId: params.problem_id,
|
||||
contest_id: undefined,
|
||||
problem_id: undefined,
|
||||
page: undefined,
|
||||
},
|
||||
}),
|
||||
)
|
||||
// 契约里 language 是 z.string()(语言是配置项,随时可能加,收紧成枚举会让
|
||||
// 新加的语言在后端 parse 时直接抛),前端在这一处收窄成 LANGUAGE
|
||||
return api2.get<{ results: SubmissionListItem[]; total: number }>(endpoint, {
|
||||
// contestId 走的是路径,page 只有前端分页器用
|
||||
params: { ...params, contestId: undefined, page: undefined },
|
||||
})
|
||||
}
|
||||
|
||||
export function getRankOfProblem(problem_id: string) {
|
||||
return legacyResponse(
|
||||
api2.get(`problems/${encodeURIComponent(problem_id)}/rank`),
|
||||
)
|
||||
export function getRankOfProblem(problemId: string) {
|
||||
return api2.get<ProblemRank>(`problems/${encodeURIComponent(problemId)}/rank`)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -228,25 +167,19 @@ export function getRank(
|
||||
n: number,
|
||||
username?: string,
|
||||
) {
|
||||
return legacyResponse(
|
||||
api2.get("rankings/users", {
|
||||
params: { offset, limit, username, top: n },
|
||||
}),
|
||||
)
|
||||
return api2.get<UserRank>("rankings/users", {
|
||||
params: { offset, limit, username, top: n },
|
||||
})
|
||||
}
|
||||
|
||||
export function getActivityRank(start: string) {
|
||||
return api2.get("rankings/activity", {
|
||||
return api2.get<ActivityRankItem[]>("rankings/activity", {
|
||||
params: { start },
|
||||
})
|
||||
}
|
||||
|
||||
export function getClassRank(grade?: number | null) {
|
||||
return legacyResponse(
|
||||
api2.get("rankings/classes", {
|
||||
params: { grade },
|
||||
}),
|
||||
)
|
||||
return api2.get<ClassRankItem[]>("rankings/classes", { params: { grade } })
|
||||
}
|
||||
|
||||
export function getUserClassRank(
|
||||
@@ -254,9 +187,9 @@ export function getUserClassRank(
|
||||
offset?: number,
|
||||
limit?: number,
|
||||
) {
|
||||
return legacyResponse(
|
||||
api2.get("me/class-rank", { params: { scope, offset, limit } }),
|
||||
)
|
||||
return api2.get<ClassUserRank>("me/class-rank", {
|
||||
params: { scope, offset, limit },
|
||||
})
|
||||
}
|
||||
|
||||
export function getClassPK(
|
||||
@@ -264,16 +197,11 @@ export function getClassPK(
|
||||
startTime?: string,
|
||||
endTime?: string,
|
||||
) {
|
||||
const payload: any = {
|
||||
return api2.post<ClassComparisonResponse>("classes/comparison", {
|
||||
classNames,
|
||||
}
|
||||
if (startTime) {
|
||||
payload.startTime = startTime
|
||||
}
|
||||
if (endTime) {
|
||||
payload.endTime = endTime
|
||||
}
|
||||
return legacyResponse(api2.post("classes/comparison", payload))
|
||||
...(startTime ? { startTime } : {}),
|
||||
...(endTime ? { endTime } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function getContestList(query: {
|
||||
@@ -283,11 +211,11 @@ export function getContestList(query: {
|
||||
status: string
|
||||
tag: string
|
||||
}) {
|
||||
return legacyResponse(api2.get("contests", { params: query }))
|
||||
return api2.get<ContestList>("contests", { params: query })
|
||||
}
|
||||
|
||||
export function getContest(id: string) {
|
||||
return legacyResponse(api2.get(`contests/${encodeURIComponent(id)}`))
|
||||
return api2.get<OjContest>(`contests/${encodeURIComponent(id)}`)
|
||||
}
|
||||
|
||||
export function getContestAccess(id: string) {
|
||||
@@ -301,30 +229,20 @@ export function checkContestPassword(contestID: string, password: string) {
|
||||
}
|
||||
|
||||
export async function getContestProblems(contestID: string) {
|
||||
const res = await api2.get<any[]>(
|
||||
const res = await api2.get<ProblemListItem[]>(
|
||||
`contests/${encodeURIComponent(contestID)}/problems`,
|
||||
)
|
||||
return res.data.map(listProblem).map(filterResult)
|
||||
return res.data.map(filterResult)
|
||||
}
|
||||
|
||||
export function getContestRank(
|
||||
contestID: string,
|
||||
query: { limit: number; offset: number },
|
||||
) {
|
||||
return legacyResponse<any>(
|
||||
api2.get(`contests/${encodeURIComponent(contestID)}/rank`, {
|
||||
params: query,
|
||||
}),
|
||||
).then((response) => ({
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
results: response.data.results.map((item: any) => ({
|
||||
...item,
|
||||
contest: item.contest_id,
|
||||
})),
|
||||
},
|
||||
}))
|
||||
return api2.get<ContestRank>(
|
||||
`contests/${encodeURIComponent(contestID)}/rank`,
|
||||
{ params: query },
|
||||
)
|
||||
}
|
||||
|
||||
export function uploadAvatar(file: File) {
|
||||
@@ -335,23 +253,18 @@ export function uploadAvatar(file: File) {
|
||||
})
|
||||
}
|
||||
|
||||
export function updateProfile(data: { real_name: string; mood: string }) {
|
||||
return legacyResponse(
|
||||
api2.put("me/profile", {
|
||||
realName: data.real_name,
|
||||
mood: data.mood,
|
||||
}),
|
||||
)
|
||||
export function updateProfile(data: { realName: string; mood: string }) {
|
||||
return api2.put<Profile>("me/profile", data)
|
||||
}
|
||||
|
||||
export function getAnnouncementList(offset = 0, limit = 10) {
|
||||
return legacyResponse(
|
||||
api2.get("announcements", { params: { limit, offset } }),
|
||||
)
|
||||
return api2.get<{ results: Announcement[]; total: number }>("announcements", {
|
||||
params: { limit, offset },
|
||||
})
|
||||
}
|
||||
|
||||
export function getAnnouncement(id: number) {
|
||||
return legacyResponse(api2.get(`announcements/${id}`))
|
||||
return api2.get<Announcement>(`announcements/${id}`)
|
||||
}
|
||||
|
||||
export function createMessage(data: {
|
||||
@@ -367,7 +280,10 @@ export function createMessage(data: {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -388,7 +304,7 @@ export function getMetrics(userid: number) {
|
||||
}
|
||||
|
||||
export function getTutorial(id: number) {
|
||||
return legacyResponse(api2.get(`tutorials/${id}`))
|
||||
return api2.get<Tutorial>(`tutorials/${id}`)
|
||||
}
|
||||
|
||||
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) {
|
||||
return legacyResponse<any>(
|
||||
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,
|
||||
})) ?? [],
|
||||
},
|
||||
}))
|
||||
return api2.get<AiDetail>("ai/detail", { params: { start, end, username } })
|
||||
}
|
||||
|
||||
export function getAIDurationData(
|
||||
@@ -416,95 +320,68 @@ export function getAIDurationData(
|
||||
duration: string,
|
||||
username?: string,
|
||||
) {
|
||||
return legacyResponse(
|
||||
api2.get("ai/duration", { params: { end, duration, username } }),
|
||||
)
|
||||
return api2.get<DurationData[]>("ai/duration", {
|
||||
params: { end, duration, username },
|
||||
})
|
||||
}
|
||||
|
||||
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() {
|
||||
return legacyResponse(api2.get("ai/login-summary"))
|
||||
return api2.get<LoginSummary>("ai/login-summary")
|
||||
}
|
||||
|
||||
export function getAIPinnedReport() {
|
||||
return legacyResponse(api2.get("ai/pinned"))
|
||||
return api2.get<AiAnalysisRecord | null>("ai/pinned")
|
||||
}
|
||||
|
||||
// ==================== 相似题目推荐 ====================
|
||||
|
||||
export function getSimilarProblems(problemId: string) {
|
||||
return api2
|
||||
.get<any[]>(`problems/${encodeURIComponent(problemId)}/similar`)
|
||||
.get<ProblemListItem[]>(`problems/${encodeURIComponent(problemId)}/similar`)
|
||||
.then((response) => ({
|
||||
...response,
|
||||
data: response.data.map(listProblem).map(filterResult),
|
||||
data: response.data.map(filterResult),
|
||||
}))
|
||||
}
|
||||
|
||||
export interface YearlyACData {
|
||||
year: number
|
||||
total: number
|
||||
accepted: number
|
||||
ac_rate: number
|
||||
}
|
||||
export type { YearlyAc as YearlyACData } from "@oj2/contract"
|
||||
|
||||
export function getProblemYearlyAC(problemId: string) {
|
||||
return legacyResponse<YearlyACData[]>(
|
||||
api2.get(`problems/${encodeURIComponent(problemId)}/yearly-ac`),
|
||||
return api2.get<YearlyAc[]>(
|
||||
`problems/${encodeURIComponent(problemId)}/yearly-ac`,
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 流程图相关API ====================
|
||||
|
||||
export function submitFlowchart(data: {
|
||||
problem_id: number
|
||||
mermaid_code: string
|
||||
flowchart_data: any // 这个是压缩之后的,元数据太长了
|
||||
problemId: number
|
||||
mermaidCode: string
|
||||
flowchartData: Record<string, unknown> // 压缩之后的,元数据太长了
|
||||
}) {
|
||||
return legacyResponse(
|
||||
api2.post("flowcharts", {
|
||||
problemId: data.problem_id,
|
||||
mermaidCode: data.mermaid_code,
|
||||
flowchartData: data.flowchart_data,
|
||||
}),
|
||||
)
|
||||
return api2.post<CreateFlowchartResponse>("flowcharts", data)
|
||||
}
|
||||
|
||||
function legacyFlowchart(value: unknown) {
|
||||
const item = toLegacy<any>(value)
|
||||
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 getFlowchartSubmission(id: string) {
|
||||
return api2.get<FlowchartSubmission>(`flowcharts/${encodeURIComponent(id)}`)
|
||||
}
|
||||
|
||||
export function getFlowchartSubmissions(params: {
|
||||
username?: string
|
||||
problem_id?: string
|
||||
problemId?: string
|
||||
myself?: string
|
||||
offset?: number
|
||||
limit?: number
|
||||
today?: string
|
||||
grade?: string
|
||||
}) {
|
||||
return legacyResponse<any>(
|
||||
api2.get("flowcharts", {
|
||||
params: {
|
||||
...params,
|
||||
problemId: params.problem_id,
|
||||
problem_id: undefined,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return api2.get<FlowchartList>("flowcharts", { params })
|
||||
}
|
||||
|
||||
export function getFlowchartStatistics(
|
||||
@@ -518,32 +395,19 @@ export function getFlowchartStatistics(
|
||||
}
|
||||
|
||||
export function retryFlowchartSubmission(submissionId: string) {
|
||||
return legacyResponse(
|
||||
api2.post(`flowcharts/${encodeURIComponent(submissionId)}/retry`),
|
||||
return api2.post<{ status: string }>(
|
||||
`flowcharts/${encodeURIComponent(submissionId)}/retry`,
|
||||
)
|
||||
}
|
||||
|
||||
export function getCurrentProblemFlowchartSubmission(problemId: number) {
|
||||
return api2.get(`problems/${problemId}/flowchart/current`)
|
||||
return api2.get<FlowchartCurrent>(`problems/${problemId}/flowchart/current`)
|
||||
}
|
||||
|
||||
export async function getFlowchartSubmissionDetail(
|
||||
problemId: number,
|
||||
page = 0,
|
||||
) {
|
||||
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,
|
||||
},
|
||||
}
|
||||
export function getFlowchartSubmissionDetail(problemId: number, page = 0) {
|
||||
return api2.get<FlowchartDetail>(`problems/${problemId}/flowchart/history`, {
|
||||
params: { page },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 题单相关API ====================
|
||||
@@ -555,64 +419,17 @@ export function getProblemSetList(
|
||||
difficulty = "",
|
||||
status = "",
|
||||
) {
|
||||
return legacyResponse<any>(
|
||||
api2.get("problem-sets", {
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
difficulty,
|
||||
status,
|
||||
},
|
||||
}),
|
||||
).then(mapProblemSetResponse)
|
||||
return api2.get<ProblemSetList>("problem-sets", {
|
||||
params: { offset, limit, keyword, difficulty, status },
|
||||
})
|
||||
}
|
||||
|
||||
export function getProblemSetDetail(id: number) {
|
||||
return legacyResponse<any>(api2.get(`problem-sets/${id}`)).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: legacyProblemSet(response.data),
|
||||
}),
|
||||
)
|
||||
return api2.get<ProblemSet>(`problem-sets/${id}`)
|
||||
}
|
||||
|
||||
function legacyBadge(value: any) {
|
||||
return { ...value, problemset: value.problemset_id }
|
||||
}
|
||||
|
||||
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 getProblemSetProblems(problemSetId: number) {
|
||||
return api2.get<ProblemSetProblem[]>(`problem-sets/${problemSetId}/problems`)
|
||||
}
|
||||
|
||||
export function joinProblemSet(problemSetId: number) {
|
||||
@@ -624,57 +441,35 @@ export function updateProblemSetProgress(
|
||||
problemId: number,
|
||||
submissionId: string,
|
||||
) {
|
||||
return legacyResponse(
|
||||
api2.put("problem-set-progress", {
|
||||
problemSetId,
|
||||
problemId,
|
||||
submissionId,
|
||||
}),
|
||||
return api2.put("problem-set-progress", {
|
||||
problemSetId,
|
||||
problemId,
|
||||
submissionId,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserBadges(username?: string) {
|
||||
return api2.get<UserBadge[]>(
|
||||
`users/${encodeURIComponent(username ?? "me")}/badges`,
|
||||
)
|
||||
}
|
||||
|
||||
// 获取用户徽章列表
|
||||
export async function getUserBadges(username?: string) {
|
||||
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 function getProblemSetBadges(problemSetId: number) {
|
||||
return api2.get<ProblemSetBadge[]>(`problem-sets/${problemSetId}/badges`)
|
||||
}
|
||||
|
||||
// 获取题单徽章列表
|
||||
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(
|
||||
problemSetId: number,
|
||||
params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
class_name?: string
|
||||
completion_status?: "" | "completed" | "in_progress" | "not_started"
|
||||
className?: string
|
||||
completionStatus?: "" | "completed" | "in_progress" | "not_started"
|
||||
},
|
||||
) {
|
||||
return legacyResponse(
|
||||
api2.get(`problem-sets/${problemSetId}/user-progress`, {
|
||||
params: {
|
||||
limit: params?.limit,
|
||||
offset: params?.offset,
|
||||
className: params?.class_name,
|
||||
completionStatus: params?.completion_status,
|
||||
},
|
||||
}),
|
||||
return api2.get<ProblemSetProgressList>(
|
||||
`problem-sets/${problemSetId}/user-progress`,
|
||||
{ params },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { ClassComparison } from "utils/types"
|
||||
import { h } from "vue"
|
||||
import { formatISO, sub, type Duration } from "date-fns"
|
||||
import { getClassPK } from "oj/api"
|
||||
@@ -46,32 +47,6 @@ const { isTeacherOrAbove } = useUserStore()
|
||||
const message = useMessage()
|
||||
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 comparisons = ref<ClassComparison[]>([])
|
||||
const duration = ref<string>("")
|
||||
@@ -129,7 +104,7 @@ function getTimeRange(): {
|
||||
|
||||
const classOptions = computed(() => {
|
||||
return (
|
||||
configStore.config?.class_list.map((item) => ({
|
||||
configStore.config?.classList.map((item) => ({
|
||||
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
||||
value: item,
|
||||
})) ?? []
|
||||
@@ -148,7 +123,7 @@ async function compare() {
|
||||
|
||||
const res = await getClassPK(selectedClasses.value, startTime, endTime)
|
||||
comparisons.value = res.data.comparisons
|
||||
hasTimeRange.value = res.data.has_time_range || false
|
||||
hasTimeRange.value = res.data.hasTimeRange || false
|
||||
} catch (error) {
|
||||
message.error("获取数据失败")
|
||||
} finally {
|
||||
@@ -252,11 +227,11 @@ function getClassColor(index: number) {
|
||||
const compositeScoreChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
label: "综合分",
|
||||
data: comparisons.value.map((c) => c.composite_score),
|
||||
data: comparisons.value.map((c) => c.compositeScore),
|
||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -270,11 +245,11 @@ const compositeScoreChartData = computed(() => {
|
||||
const totalAcChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
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),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -288,11 +263,11 @@ const totalAcChartData = computed(() => {
|
||||
const avgAcChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
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),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -306,11 +281,11 @@ const avgAcChartData = computed(() => {
|
||||
const medianAcChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
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),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -324,11 +299,11 @@ const medianAcChartData = computed(() => {
|
||||
const excellentRateChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
label: "优秀率",
|
||||
data: comparisons.value.map((c) => c.excellent_rate),
|
||||
data: comparisons.value.map((c) => c.excellentRate),
|
||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -342,11 +317,11 @@ const excellentRateChartData = computed(() => {
|
||||
const passRateChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
label: "及格率",
|
||||
data: comparisons.value.map((c) => c.pass_rate),
|
||||
data: comparisons.value.map((c) => c.passRate),
|
||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -360,11 +335,11 @@ const passRateChartData = computed(() => {
|
||||
const activeRateChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
label: "参与度",
|
||||
data: comparisons.value.map((c) => c.active_rate),
|
||||
data: comparisons.value.map((c) => c.activeRate),
|
||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -378,11 +353,11 @@ const activeRateChartData = computed(() => {
|
||||
const top10AvgChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
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),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -396,11 +371,11 @@ const top10AvgChartData = computed(() => {
|
||||
const bottom10AvgChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
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),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -414,11 +389,11 @@ const bottom10AvgChartData = computed(() => {
|
||||
const middle80AvgChartData = computed(() => {
|
||||
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 = [
|
||||
{
|
||||
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),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -449,18 +424,18 @@ const radarChartData = computed(() => {
|
||||
|
||||
// 计算每个指标的最大最小值
|
||||
const maxValues = [
|
||||
Math.max(...comparisons.value.map((c) => c.total_ac)),
|
||||
Math.max(...comparisons.value.map((c) => c.avg_ac)),
|
||||
Math.max(...comparisons.value.map((c) => c.median_ac)),
|
||||
Math.max(...comparisons.value.map((c) => c.totalAc)),
|
||||
Math.max(...comparisons.value.map((c) => c.avgAc)),
|
||||
Math.max(...comparisons.value.map((c) => c.medianAc)),
|
||||
100, // 优秀率最大值
|
||||
100, // 及格率最大值
|
||||
100, // 参与度最大值
|
||||
]
|
||||
|
||||
const minValues = [
|
||||
Math.min(...comparisons.value.map((c) => c.total_ac)),
|
||||
Math.min(...comparisons.value.map((c) => c.avg_ac)),
|
||||
Math.min(...comparisons.value.map((c) => c.median_ac)),
|
||||
Math.min(...comparisons.value.map((c) => c.totalAc)),
|
||||
Math.min(...comparisons.value.map((c) => c.avgAc)),
|
||||
Math.min(...comparisons.value.map((c) => c.medianAc)),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -469,22 +444,22 @@ const radarChartData = computed(() => {
|
||||
const datasets = comparisons.value.map((c, index) => {
|
||||
const color = getClassColor(index)
|
||||
const rawData = [
|
||||
c.total_ac,
|
||||
c.avg_ac,
|
||||
c.median_ac,
|
||||
c.excellent_rate,
|
||||
c.pass_rate,
|
||||
c.active_rate,
|
||||
c.totalAc,
|
||||
c.avgAc,
|
||||
c.medianAc,
|
||||
c.excellentRate,
|
||||
c.passRate,
|
||||
c.activeRate,
|
||||
]
|
||||
return {
|
||||
label: c.class_name,
|
||||
label: c.className,
|
||||
data: [
|
||||
normalize(c.total_ac, maxValues[0], minValues[0]),
|
||||
normalize(c.avg_ac, maxValues[1], minValues[1]),
|
||||
normalize(c.median_ac, maxValues[2], minValues[2]),
|
||||
c.excellent_rate,
|
||||
c.pass_rate,
|
||||
c.active_rate,
|
||||
normalize(c.totalAc, maxValues[0], minValues[0]),
|
||||
normalize(c.avgAc, maxValues[1], minValues[1]),
|
||||
normalize(c.medianAc, maxValues[2], minValues[2]),
|
||||
c.excellentRate,
|
||||
c.passRate,
|
||||
c.activeRate,
|
||||
],
|
||||
rawData,
|
||||
backgroundColor: color.bg,
|
||||
@@ -584,14 +559,14 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
fontSize: "15px",
|
||||
},
|
||||
},
|
||||
row.composite_score.toFixed(1),
|
||||
row.compositeScore.toFixed(1),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "班级",
|
||||
key: "class_name",
|
||||
render: (row) =>
|
||||
`${row.class_name.slice(0, 2)}计算机${row.class_name.slice(2)}班`,
|
||||
`${row.className.slice(0, 2)}计算机${row.className.slice(2)}班`,
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
@@ -602,7 +577,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#1890ff", fontWeight: "600" } },
|
||||
row.user_count,
|
||||
row.userCount,
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -613,7 +588,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#ff4d4f", fontWeight: "600" } },
|
||||
row.total_ac,
|
||||
row.totalAc,
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -624,7 +599,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#52c41a", fontWeight: "600" } },
|
||||
row.avg_ac.toFixed(2),
|
||||
row.avgAc.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -635,7 +610,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#fa8c16", fontWeight: "600" } },
|
||||
row.median_ac.toFixed(2),
|
||||
row.medianAc.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -646,7 +621,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#cf1322", fontWeight: "600" } },
|
||||
row.top10_avg.toFixed(2),
|
||||
row.top10Avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -657,7 +632,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#389e0d", fontWeight: "600" } },
|
||||
row.middle80_avg.toFixed(2),
|
||||
row.middle80Avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -668,7 +643,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#096dd9", fontWeight: "500" } },
|
||||
row.bottom10_avg.toFixed(2),
|
||||
row.bottom10Avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -679,7 +654,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#faad14", fontWeight: "600" } },
|
||||
row.excellent_rate.toFixed(1) + "%",
|
||||
row.excellentRate.toFixed(1) + "%",
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -690,7 +665,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#52c41a", fontWeight: "600" } },
|
||||
row.pass_rate.toFixed(1) + "%",
|
||||
row.passRate.toFixed(1) + "%",
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -701,7 +676,7 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
h(
|
||||
"span",
|
||||
{ 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-gi
|
||||
v-for="(classData, index) in comparisons"
|
||||
:key="classData.class_name"
|
||||
:key="classData.className"
|
||||
:span="isDesktop ? 1 : 2"
|
||||
>
|
||||
<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"
|
||||
hoverable
|
||||
:style="{
|
||||
@@ -842,7 +817,7 @@ const radarChartOptions = {
|
||||
<n-tag :type="getRankColor(index).type" size="large">
|
||||
#{{ getRankColor(index).text }}
|
||||
<span style="margin-left: 6px; font-size: 12px; opacity: 0.85">
|
||||
{{ classData.composite_score }} 分
|
||||
{{ classData.compositeScore }} 分
|
||||
</span>
|
||||
</n-tag>
|
||||
</template>
|
||||
@@ -854,7 +829,7 @@ const radarChartOptions = {
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="总AC数"
|
||||
:value="classData.total_ac"
|
||||
:value="classData.totalAc"
|
||||
size="large"
|
||||
class="stat-total-ac"
|
||||
>
|
||||
@@ -866,7 +841,7 @@ const radarChartOptions = {
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="平均AC数"
|
||||
:value="classData.avg_ac.toFixed(2)"
|
||||
:value="classData.avgAc.toFixed(2)"
|
||||
size="large"
|
||||
class="stat-avg-ac"
|
||||
>
|
||||
@@ -881,7 +856,7 @@ const radarChartOptions = {
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="中位数AC数"
|
||||
:value="classData.median_ac.toFixed(2)"
|
||||
:value="classData.medianAc.toFixed(2)"
|
||||
size="large"
|
||||
class="stat-median-ac"
|
||||
>
|
||||
@@ -896,7 +871,7 @@ const radarChartOptions = {
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="总提交数"
|
||||
:value="classData.total_submission"
|
||||
:value="classData.totalSubmission"
|
||||
size="large"
|
||||
class="stat-total-submission"
|
||||
>
|
||||
@@ -911,7 +886,7 @@ const radarChartOptions = {
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="AC率"
|
||||
:value="classData.ac_rate.toFixed(1) + '%'"
|
||||
:value="classData.acRate.toFixed(1) + '%'"
|
||||
size="large"
|
||||
class="stat-ac-rate"
|
||||
>
|
||||
@@ -934,12 +909,12 @@ const radarChartOptions = {
|
||||
<!-- 分位数统计 -->
|
||||
<n-descriptions-item label="第一四分位数(Q1)">
|
||||
<span style="color: #9254de; font-weight: 500">{{
|
||||
classData.q1_ac.toFixed(2)
|
||||
classData.q1Ac.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="第三四分位数(Q3)">
|
||||
<span style="color: #f759ab; font-weight: 500">{{
|
||||
classData.q3_ac.toFixed(2)
|
||||
classData.q3Ac.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="四分位距(IQR)">
|
||||
@@ -949,31 +924,31 @@ const radarChartOptions = {
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="标准差">
|
||||
<span style="color: #fa8c16; font-weight: 500">{{
|
||||
classData.std_dev.toFixed(2)
|
||||
classData.stdDev.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
|
||||
<!-- 分层统计 -->
|
||||
<n-descriptions-item label="前10%均值">
|
||||
<span style="color: #cf1322; font-weight: 600">{{
|
||||
classData.top10_avg.toFixed(2)
|
||||
classData.top10Avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="中间80%均值">
|
||||
<span style="color: #389e0d; font-weight: 600">{{
|
||||
classData.middle80_avg.toFixed(2)
|
||||
classData.middle80Avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="后10%均值">
|
||||
<span style="color: #096dd9; font-weight: 500">{{
|
||||
classData.bottom10_avg.toFixed(2)
|
||||
classData.bottom10Avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
|
||||
<!-- 人数 -->
|
||||
<n-descriptions-item label="人数">
|
||||
<span style="color: #1890ff; font-weight: 600">{{
|
||||
classData.user_count
|
||||
classData.userCount
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
@@ -988,34 +963,34 @@ const radarChartOptions = {
|
||||
<n-space vertical :size="10">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="classData.excellent_rate"
|
||||
:percentage="classData.excellentRate"
|
||||
:show-indicator="true"
|
||||
:border-radius="4"
|
||||
>
|
||||
<template #default>
|
||||
优秀率: {{ classData.excellent_rate.toFixed(1) }}%
|
||||
优秀率: {{ classData.excellentRate.toFixed(1) }}%
|
||||
</template>
|
||||
</n-progress>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="classData.pass_rate"
|
||||
:percentage="classData.passRate"
|
||||
:show-indicator="true"
|
||||
:border-radius="4"
|
||||
status="success"
|
||||
>
|
||||
<template #default>
|
||||
及格率: {{ classData.pass_rate.toFixed(1) }}%
|
||||
及格率: {{ classData.passRate.toFixed(1) }}%
|
||||
</template>
|
||||
</n-progress>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="classData.active_rate"
|
||||
:percentage="classData.activeRate"
|
||||
:show-indicator="true"
|
||||
:border-radius="4"
|
||||
status="info"
|
||||
>
|
||||
<template #default>
|
||||
参与度: {{ classData.active_rate.toFixed(1) }}%
|
||||
参与度: {{ classData.activeRate.toFixed(1) }}%
|
||||
</template>
|
||||
</n-progress>
|
||||
</n-space>
|
||||
@@ -1023,7 +998,7 @@ const radarChartOptions = {
|
||||
|
||||
<!-- 时间段统计(如果有) -->
|
||||
<template
|
||||
v-if="hasTimeRange && classData.recent_total_ac !== undefined"
|
||||
v-if="hasTimeRange && classData.recentTotalAc !== undefined"
|
||||
>
|
||||
<n-descriptions
|
||||
bordered
|
||||
@@ -1034,27 +1009,27 @@ const radarChartOptions = {
|
||||
>
|
||||
<n-descriptions-item label="时间段总AC">
|
||||
<span style="color: #ff7875; font-weight: 600">{{
|
||||
classData.recent_total_ac
|
||||
classData.recentTotalAc
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间段平均AC">
|
||||
<span style="color: #73d13d; font-weight: 600">{{
|
||||
classData.recent_avg_ac?.toFixed(2)
|
||||
classData.recentAvgAc?.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间段中位数AC">
|
||||
<span style="color: #ffc53d; font-weight: 600">{{
|
||||
classData.recent_median_ac?.toFixed(2)
|
||||
classData.recentMedianAc?.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间段前10名平均">
|
||||
<span style="color: #ff4d4f; font-weight: 600">{{
|
||||
classData.recent_top10_avg?.toFixed(2)
|
||||
classData.recentTop10Avg?.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="活跃学生数" :span="2">
|
||||
<span style="color: #1890ff; font-weight: 600">{{
|
||||
classData.recent_active_count
|
||||
classData.recentActiveCount
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
@@ -16,9 +16,9 @@ function goto() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{ rank.accepted_number }} /
|
||||
{{ rank.acceptedNumber }} /
|
||||
<n-button text type="primary" @click="goto">
|
||||
{{ rank.submission_number }}
|
||||
{{ rank.submissionNumber }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -24,18 +24,16 @@ const contestStore = useContestStore()
|
||||
<div v-html="contestStore.contest.description"></div>
|
||||
<n-descriptions bordered label-placement="left" :column="1">
|
||||
<n-descriptions-item label="开始时间">
|
||||
{{
|
||||
parseTime(contestStore.contest.start_time, "YYYY年M月D日 HH:mm:ss")
|
||||
}}
|
||||
{{ parseTime(contestStore.contest.startTime, "YYYY年M月D日 HH:mm:ss") }}
|
||||
</n-descriptions-item>
|
||||
<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 label="比赛类型">
|
||||
<ContestType :contest="contestStore.contest" />
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="发起人">
|
||||
{{ contestStore.contest.created_by.username }}
|
||||
{{ contestStore.contest.createdBy.username }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-popover>
|
||||
|
||||
@@ -78,7 +78,7 @@ const chartData = computed(() => {
|
||||
// 收集所有AC事件并按时间排序
|
||||
const events: AcEvent[] = []
|
||||
topUsers.forEach((rank, userIndex) => {
|
||||
Object.entries(rank.submission_info).forEach(([problemId, info]) => {
|
||||
Object.entries(rank.submissionInfo).forEach(([problemId, info]) => {
|
||||
if (info.is_ac) {
|
||||
events.push({ time: info.ac_time, userIndex, problemId })
|
||||
}
|
||||
@@ -100,7 +100,7 @@ const chartData = computed(() => {
|
||||
// 用于记录每个用户每道题的错误次数
|
||||
const userErrors: Map<string, number>[] = topUsers.map(() => new Map())
|
||||
topUsers.forEach((rank, i) => {
|
||||
Object.entries(rank.submission_info).forEach(([problemId, info]) => {
|
||||
Object.entries(rank.submissionInfo).forEach(([problemId, info]) => {
|
||||
if (info.error_number > 0) {
|
||||
userErrors[i].set(problemId, info.error_number)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouteQuery } from "@vueuse/router"
|
||||
import { NTag } from "naive-ui"
|
||||
import { getContestList } from "oj/api"
|
||||
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 Pagination from "shared/components/Pagination.vue"
|
||||
import { useAuthModalStore } from "shared/store/authModal"
|
||||
@@ -29,7 +29,7 @@ const { query, clearQuery } = usePagination<ContestQuery>({
|
||||
tag: useRouteQuery("tag", "").value,
|
||||
})
|
||||
|
||||
const data = ref<Contest[]>([])
|
||||
const data = ref<OjContest[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const options: SelectOption[] = [
|
||||
@@ -46,7 +46,7 @@ const tags: SelectOption[] = [
|
||||
{ label: "期末", value: "期末" },
|
||||
]
|
||||
|
||||
const columns: DataTableColumn<Contest>[] = [
|
||||
const columns: DataTableColumn<OjContest>[] = [
|
||||
{
|
||||
title: renderTableTitle("状态", "streamline-emojis:collision"),
|
||||
key: "status",
|
||||
@@ -74,13 +74,13 @@ const columns: DataTableColumn<Contest>[] = [
|
||||
title: renderTableTitle("开始时间", "fluent-emoji-flat:eleven-thirty"),
|
||||
key: "start_time",
|
||||
width: 180,
|
||||
render: (row) => parseTime(row.start_time),
|
||||
render: (row) => parseTime(row.startTime),
|
||||
},
|
||||
{
|
||||
title: renderTableTitle("比赛时长", "streamline-emojis:fishing-pole"),
|
||||
key: "duration",
|
||||
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)
|
||||
|
||||
function rowProps(row: Contest) {
|
||||
function rowProps(row: OjContest) {
|
||||
return {
|
||||
style: "cursor: pointer",
|
||||
onClick() {
|
||||
if (!userStore.isAuthed && row.contest_type === ContestType.private) {
|
||||
if (!userStore.isAuthed && row.contestType === ContestType.private) {
|
||||
authStore.openLoginModal()
|
||||
} else {
|
||||
router.push("/contest/" + row.id)
|
||||
|
||||
@@ -87,7 +87,7 @@ const columns = ref<DataTableColumn<ContestRank>[]>([
|
||||
key: "total_time",
|
||||
width: 120,
|
||||
align: "center",
|
||||
render: (row) => secondsToDuration(row.total_time),
|
||||
render: (row) => secondsToDuration(row.totalTime),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -129,8 +129,8 @@ async function addColumns() {
|
||||
() => problem.title,
|
||||
),
|
||||
render: (row) => {
|
||||
if (row.submission_info[problem.id]) {
|
||||
const status = row.submission_info[problem.id]
|
||||
if (row.submissionInfo[problem.id]) {
|
||||
const status = row.submissionInfo[problem.id]
|
||||
let acTime
|
||||
let errorNumber
|
||||
if (status.is_ac) {
|
||||
@@ -162,8 +162,8 @@ async function addColumns() {
|
||||
cellProps: (row) => {
|
||||
let backgroundColor = ""
|
||||
let color = theme.value.textColorBase
|
||||
if (row.submission_info[problem.id]) {
|
||||
const status = row.submission_info[problem.id]
|
||||
if (row.submissionInfo[problem.id]) {
|
||||
const status = row.submissionInfo[problem.id]
|
||||
if (status.is_first_ac) {
|
||||
backgroundColor = theme.value.primaryColor
|
||||
color = theme.value.baseColor
|
||||
|
||||
@@ -185,8 +185,8 @@ const goSubmissions = () => {
|
||||
}
|
||||
|
||||
const goEdit = () => {
|
||||
const url = problem.value!.contest
|
||||
? `/admin/contest/${problem.value!.contest}/problem/edit/${problem.value!.id}`
|
||||
const url = problem.value!.contestId
|
||||
? `/admin/contest/${problem.value!.contestId}/problem/edit/${problem.value!.id}`
|
||||
: `/admin/problem/edit/${problem.value!.id}`
|
||||
window.open(router.resolve(url).href, "_blank")
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ const { problem } = storeToRefs(problemStore)
|
||||
const problemSetId = computed(() => route.params.problemSetId)
|
||||
|
||||
// SQL 题:隐藏输入/输出/例子,改为渲染数据表与期望结果
|
||||
const isSQL = computed(() => !!problem.value?.sql_config)
|
||||
const sqlDisplay = computed(() => problem.value?.sql_display ?? null)
|
||||
const isSQL = computed(() => !!problem.value?.sqlConfig)
|
||||
const sqlDisplay = computed(() => problem.value?.sqlDisplay ?? null)
|
||||
const sqlExpectedQuery = computed(() => {
|
||||
const exp = sqlDisplay.value?.expected
|
||||
return exp && "columns" in exp ? exp : null
|
||||
@@ -71,7 +71,7 @@ watch(
|
||||
|
||||
// AC 或失败次数 >= 3 时加载推荐
|
||||
watch(
|
||||
() => [problem.value?._id, problem.value?.my_status, problemStore.failCount],
|
||||
() => [problem.value?._id, problem.value?.myStatus, problemStore.failCount],
|
||||
([, status, failCount]) => {
|
||||
if (status === 0 || (failCount as number) >= 3) {
|
||||
loadSimilarProblems()
|
||||
@@ -82,9 +82,9 @@ watch(
|
||||
|
||||
const hasTriedButNotPassed = computed(() => {
|
||||
return (
|
||||
problem.value?.my_status !== undefined &&
|
||||
problem.value?.my_status !== null &&
|
||||
problem.value?.my_status !== 0
|
||||
problem.value?.myStatus !== undefined &&
|
||||
problem.value?.myStatus !== null &&
|
||||
problem.value?.myStatus !== 0
|
||||
)
|
||||
})
|
||||
|
||||
@@ -177,8 +177,8 @@ function ruleTagType(engine: string): "error" | "success" | "info" {
|
||||
}
|
||||
|
||||
const astRulesForDisplay = computed(() => {
|
||||
if (!problem.value?.ast_rules) return []
|
||||
return Object.entries(problem.value.ast_rules).filter(
|
||||
if (!problem.value?.astRules) return []
|
||||
return Object.entries(problem.value.astRules).filter(
|
||||
([, rules]) => rules.length > 0,
|
||||
)
|
||||
})
|
||||
@@ -249,7 +249,7 @@ function type(status: ProblemStatus) {
|
||||
<!-- 已通过 -->
|
||||
<n-alert
|
||||
class="status-alert"
|
||||
v-if="problem.my_status === 0"
|
||||
v-if="problem.myStatus === 0"
|
||||
type="success"
|
||||
title="🎉 本 题 已 经 被 你 解 决 啦"
|
||||
>
|
||||
@@ -291,7 +291,7 @@ function type(status: ProblemStatus) {
|
||||
</p>
|
||||
<MdPreview
|
||||
preview-theme="vuepress"
|
||||
:model-value="problem.input_description"
|
||||
:model-value="problem.inputDescription"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
/>
|
||||
|
||||
@@ -303,7 +303,7 @@ function type(status: ProblemStatus) {
|
||||
</p>
|
||||
<MdPreview
|
||||
preview-theme="vuepress"
|
||||
:model-value="problem.output_description"
|
||||
:model-value="problem.outputDescription"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
/>
|
||||
</template>
|
||||
@@ -338,7 +338,7 @@ function type(status: ProblemStatus) {
|
||||
:total-rows="sqlExpectedQuery.total_rows"
|
||||
:truncated="sqlExpectedQuery.truncated"
|
||||
/>
|
||||
<p v-if="!problem.sql_config?.order_sensitive" class="sqlNote">
|
||||
<p v-if="!problem.sqlConfig?.order_sensitive" class="sqlNote">
|
||||
结果顺序不限
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -11,13 +11,13 @@ const { renderError, renderFlowchart } = useMermaid()
|
||||
const renderProblemFlowchart = async () => {
|
||||
await renderFlowchart(
|
||||
mermaidContainer.value,
|
||||
problem.value?.mermaid_code ?? "",
|
||||
problem.value?.mermaidCode ?? "",
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(renderProblemFlowchart)
|
||||
|
||||
watch(() => problem.value?.mermaid_code, renderProblemFlowchart)
|
||||
watch(() => problem.value?.mermaidCode, renderProblemFlowchart)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -30,7 +30,7 @@ const beatRate = ref("0")
|
||||
const yearlyACData = ref<YearlyACData[]>([])
|
||||
|
||||
const data = computed(() => {
|
||||
const status = problem.value!.statistic_info
|
||||
const status = problem.value!.statisticInfo
|
||||
const labels = []
|
||||
for (let i in status) {
|
||||
if (status[i] !== 0) {
|
||||
@@ -50,14 +50,14 @@ const numbers = computed(() => {
|
||||
return [
|
||||
{
|
||||
icon: "streamline-ultimate-color:checklist",
|
||||
title: problem.value?.submission_number ?? 0,
|
||||
title: problem.value?.submissionNumber ?? 0,
|
||||
content: "总提交",
|
||||
int: true,
|
||||
suffix: "",
|
||||
},
|
||||
{
|
||||
icon: "streamline-emojis:woman-raising-hand-2",
|
||||
title: problem.value?.accepted_number ?? 0,
|
||||
title: problem.value?.acceptedNumber ?? 0,
|
||||
content: "通过数",
|
||||
int: true,
|
||||
suffix: "",
|
||||
@@ -65,8 +65,8 @@ const numbers = computed(() => {
|
||||
{
|
||||
icon: "fluent-emoji:chart-increasing",
|
||||
title: getACRateNumber(
|
||||
problem.value?.accepted_number ?? 0,
|
||||
problem.value?.submission_number ?? 0,
|
||||
problem.value?.acceptedNumber ?? 0,
|
||||
problem.value?.submissionNumber ?? 0,
|
||||
),
|
||||
content: "通过率",
|
||||
int: false,
|
||||
@@ -115,10 +115,10 @@ onMounted(() => {
|
||||
{{ problem._id }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="出题人">
|
||||
{{ problem.created_by.username }}
|
||||
{{ problem.createdBy.username }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">
|
||||
{{ parseTime(problem.create_time) }}
|
||||
{{ parseTime(problem.createTime) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="难度">
|
||||
<n-tag :type="getTagColor(problem.difficulty)">
|
||||
@@ -150,7 +150,7 @@ onMounted(() => {
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</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" />
|
||||
</div>
|
||||
<ProblemYearlyChart :data="yearlyACData" />
|
||||
|
||||
@@ -10,17 +10,17 @@ defineProps<{
|
||||
<n-flex align="center">
|
||||
<span>{{ problem.title }}</span>
|
||||
<Icon
|
||||
v-if="problem.allow_flowchart"
|
||||
v-if="problem.allowFlowchart"
|
||||
width="18"
|
||||
icon="vscode-icons:file-type-drawio"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="problem.show_flowchart"
|
||||
v-else-if="problem.showFlowchart"
|
||||
width="18"
|
||||
icon="vscode-icons:file-type-graphql"
|
||||
/>
|
||||
<Icon
|
||||
v-if="problem.has_ast_rules"
|
||||
v-if="problem.hasAstRules"
|
||||
width="18"
|
||||
icon="vscode-icons:file-type-light-todo"
|
||||
/>
|
||||
|
||||
@@ -80,7 +80,7 @@ const wheelItems = REACTIONS.map((item, 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 canInteract = computed(
|
||||
() =>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useUserStore } from "shared/store/user"
|
||||
import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
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 { useBreakpoints } from "shared/composables/breakpoints"
|
||||
|
||||
@@ -29,19 +29,19 @@ function showCodePanel(id: string, problem: string) {
|
||||
toggleCodePanel(true)
|
||||
}
|
||||
|
||||
const columns: DataTableColumn<Submission>[] = [
|
||||
const columns: DataTableColumn<SubmissionListItem>[] = [
|
||||
{
|
||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
||||
key: "create_time",
|
||||
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"),
|
||||
key: "id",
|
||||
minWidth: 160,
|
||||
render: (row) => {
|
||||
if (!row.show_link)
|
||||
if (!row.showLink)
|
||||
return h(NFlex, { align: "center" }, () => [
|
||||
h("span", row.id.slice(0, 12)),
|
||||
h(
|
||||
@@ -90,7 +90,7 @@ const class_ac_count = ref(0)
|
||||
const all_ac_count = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const submissions = ref<Submission[]>([])
|
||||
const submissions = ref<SubmissionListItem[]>([])
|
||||
const total = ref(0)
|
||||
const query = reactive({
|
||||
limit: 10,
|
||||
@@ -126,8 +126,8 @@ async function listSubmissions() {
|
||||
...query,
|
||||
myself: "1",
|
||||
offset,
|
||||
problem_id: (route.params.problemID as string) ?? "",
|
||||
contest_id: (route.params.contestID as string) ?? "",
|
||||
problemId: (route.params.problemID as string) ?? "",
|
||||
contestId: (route.params.contestID as string) ?? "",
|
||||
})
|
||||
submissions.value = res.data.results
|
||||
total.value = res.data.total
|
||||
@@ -138,10 +138,10 @@ async function getRankOfThisProblem() {
|
||||
const res = await getRankOfProblem((route.params.problemID as string) ?? "")
|
||||
loading.value = false
|
||||
|
||||
class_name.value = res.data.class_name
|
||||
class_name.value = res.data.className
|
||||
rank.value = res.data.rank
|
||||
class_ac_count.value = res.data.class_ac_count
|
||||
all_ac_count.value = res.data.all_ac_count
|
||||
class_ac_count.value = res.data.classAcCount
|
||||
all_ac_count.value = res.data.allAcCount
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -35,7 +35,7 @@ const chartData = computed(() => ({
|
||||
datasets: [
|
||||
{
|
||||
label: "AC 率",
|
||||
data: props.data.map((d) => d.ac_rate),
|
||||
data: props.data.map((d) => d.acRate),
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
||||
@@ -58,7 +58,7 @@ const chartOptions = computed(() => ({
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
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 (
|
||||
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
|
||||
@@ -161,15 +161,13 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
||||
<n-flex
|
||||
vertical
|
||||
v-if="
|
||||
msg ||
|
||||
infoTable.length ||
|
||||
submission.statistic_info?.ast_results?.length
|
||||
msg || infoTable.length || submission.statisticInfo?.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
|
||||
v-for="(rule, i) in submission.statistic_info.ast_results"
|
||||
v-for="(rule, i) in submission.statisticInfo.ast_results"
|
||||
:key="i"
|
||||
align="center"
|
||||
:size="6"
|
||||
|
||||
@@ -130,22 +130,22 @@ async function submit() {
|
||||
|
||||
// 1. 构建提交数据
|
||||
const data: SubmitCodePayload = {
|
||||
problem_id: problem.value!.id,
|
||||
problemId: problem.value!.id,
|
||||
language: codeStore.code.language,
|
||||
code: codeStore.code.value,
|
||||
}
|
||||
if (contestID) {
|
||||
data.contest_id = parseInt(contestID)
|
||||
data.contestId = parseInt(contestID)
|
||||
}
|
||||
// 2. 提交代码到后端
|
||||
isSubmittingRequest.value = true
|
||||
try {
|
||||
const res = await submitCode(data)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.data.submissionId}`)
|
||||
|
||||
// 3. 启动冷却 + 监控
|
||||
startCooldown()
|
||||
startMonitoring(res.data.submission_id)
|
||||
startMonitoring(res.data.submissionId)
|
||||
showResult.value = true
|
||||
} finally {
|
||||
isSubmittingRequest.value = false
|
||||
@@ -183,7 +183,7 @@ watch(
|
||||
return
|
||||
|
||||
// 1. 刷新题目状态
|
||||
problem.value!.my_status = 0
|
||||
problem.value!.myStatus = 0
|
||||
|
||||
// 2. 创建ProblemSetSubmission记录,更新题单进度
|
||||
if (problemSetId) {
|
||||
|
||||
@@ -135,16 +135,16 @@ async function submitFlowchartData() {
|
||||
|
||||
try {
|
||||
const response = await submitFlowchart({
|
||||
problem_id: problem.value!.id,
|
||||
mermaid_code: mermaidCode,
|
||||
flowchart_data: {
|
||||
problemId: problem.value!.id,
|
||||
mermaidCode,
|
||||
flowchartData: {
|
||||
compressed: true,
|
||||
data: compressed,
|
||||
},
|
||||
})
|
||||
|
||||
// 获取提交ID并订阅更新
|
||||
const submissionId = response.data.submission_id
|
||||
const submissionId = response.data.submissionId
|
||||
|
||||
if (submissionId) {
|
||||
subscribeToSubmission(submissionId)
|
||||
@@ -183,18 +183,34 @@ async function getSubmission(submissionPage = 0) {
|
||||
)
|
||||
submissionCount.value = data.count
|
||||
const submission = data.submission
|
||||
myFlowchartZippedStr.value = submission.flowchart_data.data
|
||||
myMermaidCode.value = submission.mermaid_code || ""
|
||||
// 翻到没有提交的页时后端返回 null(契约里 submission 是 nullable)——
|
||||
// 原来的 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 = {
|
||||
score: submission.ai_score,
|
||||
grade: submission.ai_grade,
|
||||
score: submission.aiScore ?? 0,
|
||||
grade: (submission.aiGrade ?? "") as Rating["grade"],
|
||||
}
|
||||
evaluation.value = {
|
||||
score: submission.ai_score,
|
||||
grade: submission.ai_grade,
|
||||
feedback: submission.ai_feedback,
|
||||
suggestions: submission.ai_suggestions,
|
||||
criteria_details: submission.ai_criteria_details,
|
||||
score: submission.aiScore ?? 0,
|
||||
grade: (submission.aiGrade ?? "") as Rating["grade"],
|
||||
feedback: submission.aiFeedback ?? "",
|
||||
suggestions: submission.aiSuggestions ?? "",
|
||||
criteria_details:
|
||||
submission.aiCriteriaDetails as Evaluation["criteria_details"],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const { isMobile, isDesktop } = useBreakpoints()
|
||||
|
||||
const tabOptions = computed(() => {
|
||||
const options: string[] = ["content"]
|
||||
if (problem.value?.show_flowchart) {
|
||||
if (problem.value?.showFlowchart) {
|
||||
options.push("flowchart")
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ async function init() {
|
||||
problem.value = res.data
|
||||
} catch (err: any) {
|
||||
problem.value = null
|
||||
if (err.data === "Contest has not started yet.") {
|
||||
if (err.error === "contest-not-started") {
|
||||
errMsg.value = "比赛还没有开始"
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ watch(
|
||||
<ProblemContent />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane
|
||||
v-if="problem.show_flowchart && problem.mermaid_code"
|
||||
v-if="problem.showFlowchart && problem.mermaidCode"
|
||||
name="flowchart"
|
||||
tab="流程图表"
|
||||
>
|
||||
@@ -211,7 +211,7 @@ watch(
|
||||
<ProblemContent />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane
|
||||
v-if="problem.show_flowchart && problem.mermaid_code"
|
||||
v-if="problem.showFlowchart && problem.mermaidCode"
|
||||
name="flowchart"
|
||||
tab="流程图表"
|
||||
>
|
||||
@@ -251,7 +251,7 @@ watch(
|
||||
<n-tab-pane name="content" tab="描述">
|
||||
<ProblemContent />
|
||||
</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 />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="editor" tab="代码">
|
||||
|
||||
@@ -35,7 +35,8 @@ function getDifficultyTag(difficulty: string) {
|
||||
function getProgressPercentage() {
|
||||
if (!props.problemSet) return 0
|
||||
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-text strong>完成进度</n-text>
|
||||
<n-text>
|
||||
{{ problemSet.completed_count }} / {{ problemSet.problems_count }}
|
||||
{{ problemSet.completedCount }} / {{ problemSet.problemsCount }}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
<n-progress
|
||||
|
||||
@@ -41,7 +41,7 @@ function handleProblemClick(problemId: string) {
|
||||
style="margin-right: 10px"
|
||||
width="48"
|
||||
icon="fluent-emoji:check-mark-button"
|
||||
v-if="problemSetProblem.is_completed"
|
||||
v-if="problemSetProblem.isCompleted"
|
||||
/>
|
||||
|
||||
<n-flex vertical style="flex: 1">
|
||||
@@ -60,7 +60,7 @@ function handleProblemClick(problemId: string) {
|
||||
{{ DIFFICULTY[problemSetProblem.problem.difficulty] }}
|
||||
</n-tag>
|
||||
<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>
|
||||
|
||||
@@ -16,7 +16,7 @@ const total = ref(0)
|
||||
const statistics = ref<{
|
||||
total: number
|
||||
completed: number
|
||||
avg_progress: number
|
||||
avgProgress: number
|
||||
} | null>(null)
|
||||
const classFilter = ref<string>("")
|
||||
const completionFilter = ref<"" | "completed" | "in_progress" | "not_started">(
|
||||
@@ -42,17 +42,17 @@ async function loadUserProgress() {
|
||||
const params: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
class_name?: string
|
||||
completion_status?: "" | "completed" | "in_progress" | "not_started"
|
||||
className?: string
|
||||
completionStatus?: "" | "completed" | "in_progress" | "not_started"
|
||||
} = {
|
||||
limit: query.limit,
|
||||
offset,
|
||||
}
|
||||
if (classFilter.value.trim()) {
|
||||
params.class_name = classFilter.value.trim()
|
||||
params.className = classFilter.value.trim()
|
||||
}
|
||||
if (completionFilter.value) {
|
||||
params.completion_status = completionFilter.value
|
||||
params.completionStatus = completionFilter.value
|
||||
}
|
||||
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
||||
|
||||
@@ -92,7 +92,7 @@ const stats = computed(() => {
|
||||
return {
|
||||
total: statistics.value.total,
|
||||
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",
|
||||
width: 180,
|
||||
render: (row: ProblemSetProgress) =>
|
||||
parseTime(row.join_time, "YYYY-MM-DD HH:mm:ss"),
|
||||
parseTime(row.joinTime, "YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
title: "已完成数量",
|
||||
@@ -140,12 +140,12 @@ const progressColumns = [
|
||||
key: "completed_problems",
|
||||
width: 300,
|
||||
render: (row: ProblemSetProgress) => {
|
||||
if (row.progress_percentage === 100) {
|
||||
if (row.progressPercentage === 100) {
|
||||
return "全部题目已完成"
|
||||
}
|
||||
if (row.progress_percentage > 50 && row.progress_percentage < 100) {
|
||||
if (row.progressPercentage > 50 && row.progressPercentage < 100) {
|
||||
const completedProblemIds = new Set(
|
||||
row.completed_problems.map((p: any) => p.id),
|
||||
row.completedProblems.map((p: any) => p.id),
|
||||
)
|
||||
const incompleteProblems = allProblems.value.filter(
|
||||
(p) => !completedProblemIds.has(p.id),
|
||||
@@ -164,7 +164,7 @@ const progressColumns = [
|
||||
}
|
||||
return h("div", { style: "max-height: 120px; overflow-y: auto" }, [
|
||||
h(NFlex, {}, () =>
|
||||
row.completed_problems.map((problem: any) =>
|
||||
row.completedProblems.map((problem: any) =>
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
@@ -184,7 +184,7 @@ const progressColumns = [
|
||||
key: "progress_percentage",
|
||||
width: 120,
|
||||
render: (row: ProblemSetProgress) => {
|
||||
return `${row.progress_percentage.toFixed(0)}%`
|
||||
return `${row.progressPercentage.toFixed(0)}%`
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -192,7 +192,7 @@ const progressColumns = [
|
||||
key: "is_completed",
|
||||
width: 100,
|
||||
render: (row: ProblemSetProgress) => {
|
||||
if (row.is_completed) {
|
||||
if (row.isCompleted) {
|
||||
return h(NTag, { type: "success" }, () => "已完成")
|
||||
} else {
|
||||
return h(NTag, { type: "warning" }, () => "进行中")
|
||||
|
||||
@@ -35,7 +35,7 @@ const activeTab = ref("problems")
|
||||
async function loadProblemSetDetail() {
|
||||
const res = await getProblemSetDetail(problemSetId.value)
|
||||
problemSet.value = res.data
|
||||
isJoined.value = res.data.user_progress?.is_joined || false
|
||||
isJoined.value = res.data.userProgress?.isJoined || false
|
||||
}
|
||||
|
||||
async function loadProblems() {
|
||||
@@ -48,14 +48,14 @@ async function loadUserBadges() {
|
||||
|
||||
const res = await getUserBadges()
|
||||
userBadges.value = res.data.filter(
|
||||
(badge: UserBadgeType) => badge.badge.problemset === problemSetId.value,
|
||||
(badge: UserBadgeType) => badge.badge.problemsetId === problemSetId.value,
|
||||
)
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await Promise.all([loadProblemSetDetail(), loadProblems()])
|
||||
if (isJoined.value) {
|
||||
if (problemSet.value?.user_progress?.is_completed) {
|
||||
if (problemSet.value?.userProgress?.isCompleted) {
|
||||
celebrate()
|
||||
}
|
||||
loadUserBadges()
|
||||
@@ -100,7 +100,7 @@ async function handleJoinProblemSet() {
|
||||
const showTabs = computed(
|
||||
() =>
|
||||
userStore.isSuperAdmin ||
|
||||
(isJoined.value && problemSet.value?.user_progress?.is_completed),
|
||||
(isJoined.value && problemSet.value?.userProgress?.isCompleted),
|
||||
)
|
||||
|
||||
onMounted(init)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Icon } from "@iconify/vue"
|
||||
import { useRouteQuery } from "@vueuse/router"
|
||||
import { getProblemSetList } from "../api"
|
||||
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 { usePagination } from "shared/composables/pagination"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
@@ -12,7 +12,7 @@ const router = useRouter()
|
||||
const { isDesktop } = useBreakpoints()
|
||||
|
||||
const total = ref(0)
|
||||
const problemSets = ref<ProblemSetList[]>([])
|
||||
const problemSets = ref<ProblemSet[]>([])
|
||||
|
||||
interface ProblemSetQuery {
|
||||
keyword: string
|
||||
@@ -164,27 +164,25 @@ watch(
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-flex>
|
||||
<Icon width="20" icon="streamline-emojis:blossom" />
|
||||
<n-text>{{ problemSet.problems_count }} 道题目</n-text>
|
||||
<n-text>{{ problemSet.problemsCount }} 道题目</n-text>
|
||||
</n-flex>
|
||||
|
||||
<n-flex align="center" style="height: 28px">
|
||||
<!-- 用户进度显示 -->
|
||||
<n-progress
|
||||
v-if="
|
||||
problemSet.user_progress?.is_joined &&
|
||||
!problemSet.user_progress?.is_completed
|
||||
problemSet.userProgress?.isJoined &&
|
||||
!problemSet.userProgress?.isCompleted
|
||||
"
|
||||
type="line"
|
||||
:percentage="
|
||||
Math.round(problemSet.user_progress.progress_percentage)
|
||||
Math.round(problemSet.userProgress.progressPercentage)
|
||||
"
|
||||
:height="4"
|
||||
:border-radius="2"
|
||||
style="width: 100px"
|
||||
:color="
|
||||
getProgressColor(
|
||||
problemSet.user_progress.progress_percentage,
|
||||
)
|
||||
getProgressColor(problemSet.userProgress.progressPercentage)
|
||||
"
|
||||
/>
|
||||
<n-tag type="warning" v-if="problemSet.status === 'archived'">
|
||||
@@ -192,17 +190,14 @@ watch(
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-if="
|
||||
problemSet.user_progress?.is_joined &&
|
||||
!problemSet.user_progress?.is_completed
|
||||
problemSet.userProgress?.isJoined &&
|
||||
!problemSet.userProgress?.isCompleted
|
||||
"
|
||||
type="warning"
|
||||
>
|
||||
已加入
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-if="problemSet.user_progress?.is_completed"
|
||||
type="error"
|
||||
>
|
||||
<n-tag v-if="problemSet.userProgress?.isCompleted" type="error">
|
||||
已完成
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
@@ -212,7 +207,7 @@ watch(
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-text depth="3">
|
||||
创建于
|
||||
{{ parseTime(problemSet.create_time, "YYYY-MM-DD") }}
|
||||
{{ parseTime(problemSet.createTime, "YYYY-MM-DD") }}
|
||||
</n-text>
|
||||
<n-flex>
|
||||
<n-tooltip
|
||||
@@ -227,7 +222,7 @@ watch(
|
||||
width="24"
|
||||
height="24"
|
||||
object-fit="cover"
|
||||
:class="{ 'earned-badge': badge.is_earned }"
|
||||
:class="{ 'earned-badge': badge.isEarned }"
|
||||
/>
|
||||
</template>
|
||||
<n-flex vertical size="small">
|
||||
@@ -238,12 +233,12 @@ watch(
|
||||
获取条件:
|
||||
{{
|
||||
getConditionText(
|
||||
badge.condition_type,
|
||||
badge.condition_value,
|
||||
badge.conditionType,
|
||||
badge.conditionValue,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<n-text type="primary" v-if="badge.is_earned">
|
||||
<n-text type="primary" v-if="badge.isEarned">
|
||||
✓ 已获得
|
||||
</n-text>
|
||||
</n-flex>
|
||||
|
||||
@@ -31,7 +31,7 @@ const data = computed(() => {
|
||||
const datasets: any[] = [
|
||||
{
|
||||
label: props.type === ChartType.Rank ? "已解决" : "做题数",
|
||||
data: props.rankData.map((rank) => rank.accepted_number),
|
||||
data: props.rankData.map((rank) => rank.acceptedNumber),
|
||||
backgroundColor: [
|
||||
"rgba(255, 99, 132, 0.2)",
|
||||
"rgba(255, 159, 64, 0.2)",
|
||||
@@ -87,7 +87,7 @@ const data = computed(() => {
|
||||
if (props.type === ChartType.Rank) {
|
||||
datasets.push({
|
||||
label: "总提交数",
|
||||
data: props.rankData.map((rank) => rank.submission_number),
|
||||
data: props.rankData.map((rank) => rank.submissionNumber),
|
||||
hidden: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
ClassComparison,
|
||||
ClassRankItem as ClassRank,
|
||||
ClassUserRank,
|
||||
Rank,
|
||||
} from "utils/types"
|
||||
import { formatISO, sub, type Duration } from "date-fns"
|
||||
import { NButton, NFlex } from "naive-ui"
|
||||
import {
|
||||
@@ -10,7 +16,6 @@ import {
|
||||
} from "oj/api"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { getACRate, getCSRFToken } from "utils/functions"
|
||||
import type { Rank } from "utils/types"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { ChartType } from "utils/constants"
|
||||
import { renderTableTitle } from "utils/renders"
|
||||
@@ -39,6 +44,7 @@ const query = reactive({
|
||||
limit: 10,
|
||||
page: 1,
|
||||
})
|
||||
const message = useMessage()
|
||||
const rankChart = ref<Rank[]>([])
|
||||
const activityChart = ref<Rank[]>([])
|
||||
const duration = ref("months:1")
|
||||
@@ -46,7 +52,7 @@ const classData = ref<ClassRank[]>([])
|
||||
const classQuery = reactive({
|
||||
grade: gradeOptions[0].value,
|
||||
})
|
||||
const myClassData = ref<UserRank[]>([])
|
||||
const myClassData = ref<ClassUserRank["ranks"]>([])
|
||||
const myRank = ref(-1)
|
||||
const myClassName = ref("")
|
||||
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() {
|
||||
const offset = (query.page - 1) * query.limit
|
||||
const res = await getRank(offset, query.limit, 100)
|
||||
@@ -251,7 +219,7 @@ const columns: DataTableColumn<Rank>[] = [
|
||||
key: "rate",
|
||||
width: 120,
|
||||
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 start = formatISO(sub(current, subOptions.value))
|
||||
const res = await getActivityRank(start)
|
||||
activityChart.value = res.data.map(
|
||||
(d: { username: string; count: number }) => ({
|
||||
user: {
|
||||
username: d.username,
|
||||
},
|
||||
accepted_number: d.count,
|
||||
submission_number: 0,
|
||||
}),
|
||||
)
|
||||
// 活动榜只有「用户名 + 做题数」,塞进榜单图表复用的 Rank 形状里
|
||||
activityChart.value = res.data.map((d, index) => ({
|
||||
id: index,
|
||||
user: { id: index, username: d.username, realName: null },
|
||||
acceptedNumber: d.count,
|
||||
submissionNumber: 0,
|
||||
mood: null,
|
||||
}))
|
||||
}
|
||||
|
||||
async function listRank() {
|
||||
@@ -321,7 +288,7 @@ const classColumns: DataTableColumn<ClassRank>[] = [
|
||||
title: "班级",
|
||||
key: "class_name",
|
||||
render: (row) =>
|
||||
`${row.class_name.slice(0, 2)}计算机${row.class_name.slice(2)}班`,
|
||||
`${row.className.slice(0, 2)}计算机${row.className.slice(2)}班`,
|
||||
minWidth: 120,
|
||||
titleAlign: "center",
|
||||
align: "center",
|
||||
@@ -360,7 +327,7 @@ const classColumns: DataTableColumn<ClassRank>[] = [
|
||||
width: 90,
|
||||
titleAlign: "center",
|
||||
align: "center",
|
||||
render: (row) => `${row.ac_rate}%`,
|
||||
render: (row) => `${row.acRate}%`,
|
||||
},
|
||||
{
|
||||
title: "详情",
|
||||
@@ -374,14 +341,14 @@ const classColumns: DataTableColumn<ClassRank>[] = [
|
||||
{
|
||||
text: true,
|
||||
type: "info",
|
||||
onClick: () => loadClassDetail(row.class_name),
|
||||
onClick: () => loadClassDetail(row.className),
|
||||
},
|
||||
() => "查看",
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const myClassColumns: DataTableColumn<UserRank>[] = [
|
||||
const myClassColumns: DataTableColumn<ClassUserRank["ranks"][number]>[] = [
|
||||
{
|
||||
title: "排名",
|
||||
key: "rank",
|
||||
@@ -448,7 +415,7 @@ async function listClassRank() {
|
||||
if (!userStore.user) {
|
||||
await userStore.getMyProfile()
|
||||
}
|
||||
const className = userStore.user?.class_name
|
||||
const className = userStore.user?.className
|
||||
if (className) {
|
||||
classQuery.grade = parseInt(className.slice(0, 2))
|
||||
}
|
||||
@@ -464,8 +431,8 @@ async function listMyClassRank() {
|
||||
: 0
|
||||
const limit = myClassScope.value === "all" ? myClassQuery.limit : undefined
|
||||
const res = await getUserClassRank(myClassScope.value, offset, limit)
|
||||
myRank.value = res.data.my_rank
|
||||
myClassName.value = res.data.class_name
|
||||
myRank.value = res.data.myRank
|
||||
myClassName.value = res.data.className
|
||||
myClassData.value = res.data.ranks
|
||||
myClassTotal.value = res.data.total ?? res.data.ranks.length
|
||||
if (myClassScope.value === "window") {
|
||||
@@ -610,7 +577,7 @@ watch(
|
||||
preset="card"
|
||||
:title="
|
||||
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' }"
|
||||
@@ -621,7 +588,7 @@ watch(
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="总AC数"
|
||||
:value="classDetailData.total_ac"
|
||||
:value="classDetailData.totalAc"
|
||||
size="large"
|
||||
class="stat-total-ac"
|
||||
>
|
||||
@@ -633,7 +600,7 @@ watch(
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="平均AC数"
|
||||
:value="classDetailData.avg_ac.toFixed(2)"
|
||||
:value="classDetailData.avgAc.toFixed(2)"
|
||||
size="large"
|
||||
class="stat-avg-ac"
|
||||
>
|
||||
@@ -648,7 +615,7 @@ watch(
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="中位数AC数"
|
||||
:value="classDetailData.median_ac.toFixed(2)"
|
||||
:value="classDetailData.medianAc.toFixed(2)"
|
||||
size="large"
|
||||
class="stat-median-ac"
|
||||
>
|
||||
@@ -663,7 +630,7 @@ watch(
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="总提交数"
|
||||
:value="classDetailData.total_submission"
|
||||
:value="classDetailData.totalSubmission"
|
||||
size="large"
|
||||
class="stat-total-submission"
|
||||
>
|
||||
@@ -678,7 +645,7 @@ watch(
|
||||
<n-gi>
|
||||
<n-statistic
|
||||
label="AC率"
|
||||
:value="classDetailData.ac_rate.toFixed(1) + '%'"
|
||||
:value="classDetailData.acRate.toFixed(1) + '%'"
|
||||
size="large"
|
||||
class="stat-ac-rate"
|
||||
>
|
||||
@@ -699,12 +666,12 @@ watch(
|
||||
>
|
||||
<n-descriptions-item label="第一四分位数(Q1)">
|
||||
<span style="color: #9254de; font-weight: 500">{{
|
||||
classDetailData.q1_ac.toFixed(2)
|
||||
classDetailData.q1Ac.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="第三四分位数(Q3)">
|
||||
<span style="color: #f759ab; font-weight: 500">{{
|
||||
classDetailData.q3_ac.toFixed(2)
|
||||
classDetailData.q3Ac.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="四分位距(IQR)">
|
||||
@@ -714,27 +681,27 @@ watch(
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="标准差">
|
||||
<span style="color: #fa8c16; font-weight: 500">{{
|
||||
classDetailData.std_dev.toFixed(2)
|
||||
classDetailData.stdDev.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="前10%均值">
|
||||
<span style="color: #cf1322; font-weight: 600">{{
|
||||
classDetailData.top10_avg.toFixed(2)
|
||||
classDetailData.top10Avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="中间80%均值">
|
||||
<span style="color: #389e0d; font-weight: 600">{{
|
||||
classDetailData.middle80_avg.toFixed(2)
|
||||
classDetailData.middle80Avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="后10%均值">
|
||||
<span style="color: #096dd9; font-weight: 500">{{
|
||||
classDetailData.bottom10_avg.toFixed(2)
|
||||
classDetailData.bottom10Avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="人数">
|
||||
<span style="color: #1890ff; font-weight: 600">{{
|
||||
classDetailData.user_count
|
||||
classDetailData.userCount
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
@@ -743,35 +710,35 @@ watch(
|
||||
<n-space vertical :size="10">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="classDetailData.excellent_rate"
|
||||
:percentage="classDetailData.excellentRate"
|
||||
:show-indicator="true"
|
||||
:border-radius="4"
|
||||
>
|
||||
<template #default
|
||||
>优秀率:
|
||||
{{ classDetailData.excellent_rate.toFixed(1) }}%</template
|
||||
{{ classDetailData.excellentRate.toFixed(1) }}%</template
|
||||
>
|
||||
</n-progress>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="classDetailData.pass_rate"
|
||||
:percentage="classDetailData.passRate"
|
||||
:show-indicator="true"
|
||||
:border-radius="4"
|
||||
status="success"
|
||||
>
|
||||
<template #default
|
||||
>及格率: {{ classDetailData.pass_rate.toFixed(1) }}%</template
|
||||
>及格率: {{ classDetailData.passRate.toFixed(1) }}%</template
|
||||
>
|
||||
</n-progress>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="classDetailData.active_rate"
|
||||
:percentage="classDetailData.activeRate"
|
||||
:show-indicator="true"
|
||||
:border-radius="4"
|
||||
status="info"
|
||||
>
|
||||
<template #default
|
||||
>参与度: {{ classDetailData.active_rate.toFixed(1) }}%</template
|
||||
>参与度: {{ classDetailData.activeRate.toFixed(1) }}%</template
|
||||
>
|
||||
</n-progress>
|
||||
</n-space>
|
||||
@@ -784,7 +751,7 @@ watch(
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<n-tag type="success" size="large">
|
||||
综合分: {{ classDetailData.composite_score.toFixed(1) }}
|
||||
综合分: {{ classDetailData.compositeScore.toFixed(1) }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
type="info"
|
||||
|
||||
@@ -13,13 +13,14 @@ export const useAIStore = defineStore("ai", () => {
|
||||
const targetUsername = ref("")
|
||||
const durationData = ref<DurationData[]>([])
|
||||
const detailsData = reactive<DetailsData>({
|
||||
user: "",
|
||||
start: "",
|
||||
end: "",
|
||||
grade: "B",
|
||||
class_name: "",
|
||||
grade: "",
|
||||
className: null,
|
||||
tags: {},
|
||||
difficulty: {},
|
||||
contest_count: 0,
|
||||
contestCount: 0,
|
||||
solved: [],
|
||||
flowcharts: [],
|
||||
})
|
||||
@@ -44,10 +45,10 @@ export const useAIStore = defineStore("ai", () => {
|
||||
detailsData.end = res.data.end
|
||||
detailsData.solved = res.data.solved
|
||||
detailsData.grade = res.data.grade
|
||||
detailsData.class_name = res.data.class_name
|
||||
detailsData.className = res.data.className
|
||||
detailsData.tags = res.data.tags
|
||||
detailsData.difficulty = res.data.difficulty
|
||||
detailsData.contest_count = res.data.contest_count
|
||||
detailsData.contestCount = res.data.contestCount
|
||||
detailsData.flowcharts = res.data.flowcharts
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ export const useContestStore = defineStore("contest", () => {
|
||||
|
||||
const contestStatus = computed<ContestStatus>(() => {
|
||||
if (!contest.value) return ContestStatus.initial
|
||||
const start = getTime(parseISO(contest.value.start_time.toString()))
|
||||
const end = getTime(parseISO(contest.value.end_time.toString()))
|
||||
const start = getTime(parseISO(contest.value.startTime.toString()))
|
||||
const end = getTime(parseISO(contest.value.endTime.toString()))
|
||||
if (start > now.value) {
|
||||
return ContestStatus.not_started
|
||||
} else if (end < now.value) {
|
||||
@@ -36,10 +36,10 @@ export const useContestStore = defineStore("contest", () => {
|
||||
if (contestStatus.value === ContestStatus.finished) {
|
||||
return "已结束"
|
||||
} 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
|
||||
} else {
|
||||
const d = duration(formatISO(now.value), contest.value!.end_time, true)
|
||||
const d = duration(formatISO(now.value), contest.value!.endTime, true)
|
||||
return "距离比赛结束 " + d
|
||||
}
|
||||
})
|
||||
@@ -48,11 +48,11 @@ export const useContestStore = defineStore("contest", () => {
|
||||
() =>
|
||||
userStore.isSuperAdmin ||
|
||||
(userStore.isAuthed &&
|
||||
contest.value?.created_by.id === userStore.user!.id),
|
||||
contest.value?.createdBy.id === userStore.user!.id),
|
||||
)
|
||||
|
||||
const isPrivate = computed(
|
||||
() => contest.value!.contest_type === ContestType.private,
|
||||
() => contest.value!.contestType === ContestType.private,
|
||||
)
|
||||
|
||||
async function init(contestID: string) {
|
||||
@@ -65,7 +65,7 @@ export const useContestStore = defineStore("contest", () => {
|
||||
now.value = now.value + 1000
|
||||
}, 1000)
|
||||
}
|
||||
if (contest.value?.contest_type === ContestType.private) {
|
||||
if (contest.value?.contestType === ContestType.private) {
|
||||
const res = await getContestAccess(contestID)
|
||||
toggleAccess(res.data.access)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const useProblemStore = defineStore("problem", () => {
|
||||
const failCount = ref(0)
|
||||
|
||||
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 problem.value?.languages ?? []
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<n-card title="流程图预览">
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
v-if="!renderError && submission?.mermaid_code"
|
||||
v-if="!renderError && submission?.mermaidCode"
|
||||
quaternary
|
||||
size="small"
|
||||
@click="showLargeImage = true"
|
||||
@@ -42,12 +42,12 @@
|
||||
<n-gi :span="2">
|
||||
<!-- AI反馈 -->
|
||||
<n-card
|
||||
v-if="submission.ai_feedback"
|
||||
v-if="submission.aiFeedback"
|
||||
size="small"
|
||||
title="AI反馈"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<n-text>{{ submission.ai_feedback }}</n-text>
|
||||
<n-text>{{ submission.aiFeedback }}</n-text>
|
||||
</n-card>
|
||||
|
||||
<!-- 改进建议 -->
|
||||
@@ -69,15 +69,12 @@
|
||||
|
||||
<!-- 详细评分 -->
|
||||
<n-card
|
||||
v-if="
|
||||
submission.ai_criteria_details &&
|
||||
Object.keys(submission.ai_criteria_details).length > 0
|
||||
"
|
||||
v-if="Object.keys(criteriaDetails).length > 0"
|
||||
size="small"
|
||||
title="详细评分"
|
||||
>
|
||||
<div
|
||||
v-for="(detail, key) in submission.ai_criteria_details"
|
||||
v-for="(detail, key) in criteriaDetails"
|
||||
:key="key"
|
||||
style="margin-bottom: 12px"
|
||||
>
|
||||
@@ -121,11 +118,38 @@ const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
|
||||
const { renderError, renderFlowchart } = useMermaid()
|
||||
|
||||
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 rendering = ref(false)
|
||||
const showLargeImage = ref(false)
|
||||
const suggestionLines = computed(() =>
|
||||
splitSuggestionLines(submission.value?.ai_suggestions),
|
||||
splitSuggestionLines(submission.value?.aiSuggestions),
|
||||
)
|
||||
|
||||
function splitSuggestionLines(suggestions?: string | null) {
|
||||
@@ -154,12 +178,12 @@ async function loadSubmission() {
|
||||
submission.value = res.data
|
||||
|
||||
// 渲染流程图
|
||||
if (submission.value?.mermaid_code) {
|
||||
if (submission.value?.mermaidCode) {
|
||||
rendering.value = true
|
||||
await nextTick()
|
||||
await renderFlowchart(
|
||||
mermaidContainer.value,
|
||||
submission.value.mermaid_code,
|
||||
submission.value.mermaidCode,
|
||||
)
|
||||
rendering.value = false
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ defineProps<{
|
||||
function gradeType(grade: Grade) {
|
||||
return (
|
||||
{
|
||||
"": "default",
|
||||
S: "success",
|
||||
A: "info",
|
||||
B: "warning",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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')">
|
||||
{{ props.submission.id.slice(0, 12) }}
|
||||
</n-button>
|
||||
@@ -43,7 +43,7 @@ defineEmits(["showCode"])
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isOwnSubmission = computed(
|
||||
() => userStore.profile?.user?.id === props.submission.user_id,
|
||||
() => userStore.profile?.user?.id === props.submission.userId,
|
||||
)
|
||||
|
||||
function goto() {
|
||||
|
||||
@@ -77,10 +77,10 @@ function copyToCat() {
|
||||
}
|
||||
|
||||
function copyToProblem() {
|
||||
const { code, language, contest } = submission.value!
|
||||
const { code, language, contestId } = submission.value!
|
||||
// 编辑器的 storageKey 用 display id(problem._id),等于 props.problemID,
|
||||
// 而非 submission.problem(内部数字 id)
|
||||
const contestIDForKey = contest || null
|
||||
const contestIDForKey = contestId || null
|
||||
const storageKey = `problem_${props.problemID}_contest_${contestIDForKey}_lang_${language}`
|
||||
storage.set(storageKey, code)
|
||||
// 设置语言 + 代码:localStorage 覆盖全新挂载的编辑器,
|
||||
@@ -89,10 +89,10 @@ function copyToProblem() {
|
||||
codeStore.setCode(code)
|
||||
|
||||
const problemSetId = (route.params.problemSetId as string) ?? ""
|
||||
if (contest) {
|
||||
if (contestId) {
|
||||
router.push({
|
||||
name: "contest problem",
|
||||
params: { contestID: String(contest), problemID: props.problemID },
|
||||
params: { contestID: String(contestId), problemID: props.problemID },
|
||||
})
|
||||
} else if (problemSetId) {
|
||||
router.push({
|
||||
@@ -121,7 +121,7 @@ onMounted(init)
|
||||
:title="JUDGE_STATUS[submission.result]['title']"
|
||||
>
|
||||
<n-flex>
|
||||
<span>提交时间:{{ parseTime(submission.create_time) }}</span>
|
||||
<span>提交时间:{{ parseTime(submission.createTime) }}</span>
|
||||
<span>编程语言:{{ LANGUAGE_SHOW_VALUE[submission.language] }}</span>
|
||||
<span>用户:{{ submission.username }}</span>
|
||||
</n-flex>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
retryFlowchartSubmission,
|
||||
} from "oj/api"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { Grade as GradeValue } from "utils/types"
|
||||
import type {
|
||||
FlowchartSubmissionListItem,
|
||||
LANGUAGE,
|
||||
@@ -101,7 +102,7 @@ async function listSubmissions() {
|
||||
if (query.language === "Flowchart") {
|
||||
const res = await getFlowchartSubmissions({
|
||||
username: query.username,
|
||||
problem_id: query.problem,
|
||||
problemId: query.problem,
|
||||
myself: query.myself,
|
||||
offset,
|
||||
limit: query.limit,
|
||||
@@ -114,8 +115,8 @@ async function listSubmissions() {
|
||||
const res = await getSubmissions({
|
||||
...query,
|
||||
offset,
|
||||
problem_id: query.problem,
|
||||
contest_id: (route.params.contestID as string) ?? "",
|
||||
problemId: query.problem,
|
||||
contestId: (route.params.contestID as string) ?? "",
|
||||
language: query.language,
|
||||
today: query.today,
|
||||
})
|
||||
@@ -233,7 +234,7 @@ const columns = computed(() => {
|
||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
||||
key: "create_time",
|
||||
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"),
|
||||
@@ -263,7 +264,7 @@ const columns = computed(() => {
|
||||
onClick: () => problemClicked(row),
|
||||
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"),
|
||||
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"),
|
||||
@@ -340,7 +341,7 @@ const flowchartColumns = computed(() => {
|
||||
onClick: () => problemClicked(row),
|
||||
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",
|
||||
),
|
||||
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(
|
||||
@@ -545,9 +550,9 @@ const flowchartColumns = computed(() => {
|
||||
<n-text>流程图评分详情</n-text>
|
||||
<n-text
|
||||
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-flex>
|
||||
</template>
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import { DIFFICULTY } from "utils/constants"
|
||||
import { getACRate } from "utils/functions"
|
||||
import type { Problem } from "utils/types"
|
||||
import type { ProblemFiltered, ProblemListItem } from "utils/types"
|
||||
|
||||
// 把后端的 Problem 塑形成列表项需要的形状,与请求逻辑解耦。
|
||||
export function filterResult(result: Problem) {
|
||||
const newResult = {
|
||||
// 把后端的列表项塑形成列表页需要的形状,与请求逻辑解耦。
|
||||
export function filterResult(result: ProblemListItem): ProblemFiltered {
|
||||
return {
|
||||
id: result.id,
|
||||
_id: result._id,
|
||||
title: result.title,
|
||||
difficulty: DIFFICULTY[result.difficulty],
|
||||
tags: result.tags,
|
||||
submission: result.submission_number,
|
||||
rate: getACRate(result.accepted_number, result.submission_number),
|
||||
status: "",
|
||||
author: result.created_by.username,
|
||||
allow_flowchart: result.allow_flowchart,
|
||||
show_flowchart: result.show_flowchart,
|
||||
has_ast_rules: result.has_ast_rules,
|
||||
submission: result.submissionNumber,
|
||||
rate: getACRate(result.acceptedNumber, result.submissionNumber),
|
||||
// null / undefined 都表示「没做过」
|
||||
status:
|
||||
result.myStatus === null || result.myStatus === undefined
|
||||
? "not_test"
|
||||
: 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 {
|
||||
const res = await getProfile(route.query.name as string)
|
||||
profile.value = res.data
|
||||
const acm = res.data.acm_problems_status.problems || {}
|
||||
const acm = res.data!.acmProblemsStatus.problems || {}
|
||||
const ac: string[] = []
|
||||
Object.keys(acm).forEach((id) => {
|
||||
if (acm[id]["status"] === 0) {
|
||||
@@ -74,7 +74,7 @@ async function init() {
|
||||
ac.sort()
|
||||
problems.value = ac
|
||||
|
||||
if (profile.value.submission_number > 0) {
|
||||
if (profile.value.submissionNumber > 0) {
|
||||
const metricsRes = await getMetrics(profile.value.user.id)
|
||||
firstSubmissionAt.value = parseTime(metricsRes.data.first)
|
||||
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
|
||||
@@ -130,13 +130,13 @@ const metrics = computed(() => {
|
||||
},
|
||||
{
|
||||
icon: "fluent-emoji:candy",
|
||||
title: profile.value?.accepted_number ?? 0,
|
||||
title: profile.value?.acceptedNumber ?? 0,
|
||||
content: "已解决的题目数量",
|
||||
animate: true,
|
||||
},
|
||||
{
|
||||
icon: "fluent-emoji:thinking-face",
|
||||
title: profile.value?.submission_number ?? 0,
|
||||
title: profile.value?.submissionNumber ?? 0,
|
||||
content: "总提交数量",
|
||||
animate: true,
|
||||
},
|
||||
@@ -186,7 +186,7 @@ onMounted(() => {
|
||||
</n-flex>
|
||||
|
||||
<n-grid
|
||||
v-if="profile && profile.submission_number > 0"
|
||||
v-if="profile && profile.submissionNumber > 0"
|
||||
class="wrapper"
|
||||
:cols="isDesktop ? 2 : 1"
|
||||
:x-gap="10"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<n-flex size="large" vertical>
|
||||
<n-flex align="center">
|
||||
<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>{{ item.sender.username }}</div>
|
||||
</n-flex>
|
||||
|
||||
@@ -30,7 +30,7 @@ async function upload({ file }: UploadCustomRequestOptions) {
|
||||
async function saveProfile() {
|
||||
try {
|
||||
await updateProfile({
|
||||
real_name: userStore.profile?.real_name ?? "",
|
||||
realName: userStore.profile?.realName ?? "",
|
||||
mood: userStore.profile?.mood ?? "",
|
||||
})
|
||||
message.success("更改成功")
|
||||
@@ -55,7 +55,7 @@ async function saveProfile() {
|
||||
</n-upload>
|
||||
</n-form-item>
|
||||
<!-- <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 label="个性签名">
|
||||
<n-input v-model:value="userStore.profile.mood" />
|
||||
|
||||
Reference in New Issue
Block a user