refactor(前端): vue-tsc 清零,type-check 可以当 CI 门禁了
接上 vue-tsc 时有 143 条既有错误,上一批迁移带走 89 条,这里把剩下的清完。
大部分是噪音(未使用的回调参数、死变量、少数 unknown),但里面躺着两个真问题:
- 个人主页查不存在的用户会抛:getProfile 返回 null 时后面照样取
.acmProblemsStatus,靠 `!` 压着。加了早返回。
- getProblemSetProgress 我上一批标错了类型:后台这个接口返回的是裸数组,
不是分页信封(和 oj 侧的 /user-progress 不一样)。已改正并加注释区分。
其余处理:
- 未使用的回调参数按 TS 约定加 `_` 前缀(v-for 的项、供子类重写的空钩子、
路由守卫的 from);确认无用的局部变量直接删(clickX/clickY 算了点击位置
但飞出方向用的是随机角度,hasToday 下面已经用 lastDateOnly 判过,
prefix 算了周/月但标签里没用上)。
- App.vue 的 highlight.js 注册:Promise.all([...]).then(m => m.map(x => x.default))
会把元组塌成 (HLJSApi | LanguageFn)[],hljs 上就找不到 registerLanguage。
改成逐个取 .default。
- 给一批 api 补上契约类型(getMetrics / getHitokoto / getTutorials /
formatCode / getProblemBeatRate / getClassUsernames 等),契约相应补了
8 个 z.infer 导出。
- ContestRank.submissionInfo 和 AcmHelperItem.acInfo 在 api 边界窄化成
SubmissionInfo —— 契约里是 Record<string, unknown>(JSONB 原文)。
- FlowchartEditor 的 TS2589:vue-flow 的 Node 嵌套太深,ref<Node[]>([]) 的
UnwrapRef 推导撞上实例化层级上限,改成 ref([]) as Ref<Node[]>。
- api2.ts 的响应拦截器**故意**不返回 AxiosResponse(要把 { data } 信封剥掉),
类型上确实说不通,没硬掰,写注释说明为什么用断言。
- 题目列表的「随机」按钮在模板里一直是注释状态,对应的 getRandom 和
getRandomProblemID 一并删除(后端 /problems/random 保留不动)。
验证:vue-tsc 0 条,apps/api tsc、check:routes、web build 全通过;
修过 key 的列都打接口确认过字段真实存在。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+12
-9
@@ -28,7 +28,9 @@ const hljsInstance = ref<any>(null)
|
|||||||
const loadHighlightJS = async () => {
|
const loadHighlightJS = async () => {
|
||||||
if (hljsInstance.value) return hljsInstance.value
|
if (hljsInstance.value) return hljsInstance.value
|
||||||
|
|
||||||
const [hljs, c, cpp, python, java, javascript, go, sql] = await Promise.all([
|
// 逐个取 .default,不要在 Promise.all 后面 map —— 那样元组会塌成
|
||||||
|
// (HLJSApi | LanguageFn)[],每个元素都变成联合类型,hljs 上就找不到方法了
|
||||||
|
const [core, c, cpp, python, java, javascript, go, sql] = await Promise.all([
|
||||||
import("highlight.js/lib/core"),
|
import("highlight.js/lib/core"),
|
||||||
import("highlight.js/lib/languages/c"),
|
import("highlight.js/lib/languages/c"),
|
||||||
import("highlight.js/lib/languages/cpp"),
|
import("highlight.js/lib/languages/cpp"),
|
||||||
@@ -37,15 +39,16 @@ const loadHighlightJS = async () => {
|
|||||||
import("highlight.js/lib/languages/javascript"),
|
import("highlight.js/lib/languages/javascript"),
|
||||||
import("highlight.js/lib/languages/go"),
|
import("highlight.js/lib/languages/go"),
|
||||||
import("highlight.js/lib/languages/sql"),
|
import("highlight.js/lib/languages/sql"),
|
||||||
]).then((modules) => modules.map((m) => m.default))
|
])
|
||||||
|
const hljs = core.default
|
||||||
|
|
||||||
hljs.registerLanguage("c", c)
|
hljs.registerLanguage("c", c.default)
|
||||||
hljs.registerLanguage("python", python)
|
hljs.registerLanguage("python", python.default)
|
||||||
hljs.registerLanguage("cpp", cpp)
|
hljs.registerLanguage("cpp", cpp.default)
|
||||||
hljs.registerLanguage("java", java)
|
hljs.registerLanguage("java", java.default)
|
||||||
hljs.registerLanguage("javascript", javascript)
|
hljs.registerLanguage("javascript", javascript.default)
|
||||||
hljs.registerLanguage("go", go)
|
hljs.registerLanguage("go", go.default)
|
||||||
hljs.registerLanguage("sql", sql)
|
hljs.registerLanguage("sql", sql.default)
|
||||||
|
|
||||||
hljsInstance.value = hljs
|
hljsInstance.value = hljs
|
||||||
return hljs
|
return hljs
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import api2 from "utils/api2"
|
|||||||
import { toProblemListItem } from "admin/transforms"
|
import { toProblemListItem } from "admin/transforms"
|
||||||
import type {
|
import type {
|
||||||
AcTrend,
|
AcTrend,
|
||||||
|
AdminProblemSetProgress,
|
||||||
BatchProblemTagResponse,
|
BatchProblemTagResponse,
|
||||||
GenerateSqlTestCaseResponse,
|
GenerateSqlTestCaseResponse,
|
||||||
RenameTagResponse,
|
RenameTagResponse,
|
||||||
SqlTestCaseScript,
|
SqlTestCaseScript,
|
||||||
AcmHelperItem,
|
AcmHelperItem,
|
||||||
|
SubmissionInfo,
|
||||||
AdminAiReport,
|
AdminAiReport,
|
||||||
AdminAiReportList,
|
AdminAiReportList,
|
||||||
StuckProblem,
|
StuckProblem,
|
||||||
@@ -36,7 +38,6 @@ import type {
|
|||||||
ProblemSetBadge,
|
ProblemSetBadge,
|
||||||
ProblemSetList,
|
ProblemSetList,
|
||||||
ProblemSetProblem,
|
ProblemSetProblem,
|
||||||
ProblemSetProgressList,
|
|
||||||
TutorialListItem,
|
TutorialListItem,
|
||||||
} from "utils/types"
|
} from "utils/types"
|
||||||
|
|
||||||
@@ -476,7 +477,11 @@ export function makeProblemPublic(id: number, displayId: string) {
|
|||||||
|
|
||||||
// 比赛辅助检查
|
// 比赛辅助检查
|
||||||
export function getACMHelperList(contestId: number) {
|
export function getACMHelperList(contestId: number) {
|
||||||
return api2.get<AcmHelperItem[]>(`admin/contests/${contestId}/acm-helper`)
|
// acInfo 在契约里是 Record<string, unknown>(acm_contest_rank 的 JSONB 原文),
|
||||||
|
// 组件侧按 SubmissionInfo 读,收窄放在这里
|
||||||
|
return api2.get<
|
||||||
|
Array<Omit<AcmHelperItem, "acInfo"> & { acInfo: SubmissionInfo }>
|
||||||
|
>(`admin/contests/${contestId}/acm-helper`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateACMHelperChecked(
|
export function updateACMHelperChecked(
|
||||||
@@ -652,8 +657,9 @@ export function deleteProblemSetBadge(problemSetId: number, badgeId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 题单进度管理 API
|
// 题单进度管理 API
|
||||||
|
// 注意:返回的是裸数组,不是分页信封 —— 和 oj 侧的 /user-progress 不同
|
||||||
export function getProblemSetProgress(problemSetId: number) {
|
export function getProblemSetProgress(problemSetId: number) {
|
||||||
return api2.get<ProblemSetProgressList>(
|
return api2.get<AdminProblemSetProgress[]>(
|
||||||
`admin/problem-sets/${problemSetId}/progress`,
|
`admin/problem-sets/${problemSetId}/progress`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
ProblemSet,
|
ProblemSet,
|
||||||
ProblemSetProblem,
|
ProblemSetProblem,
|
||||||
ProblemSetBadge,
|
ProblemSetBadge,
|
||||||
ProblemSetProgress,
|
AdminProblemSetProgress,
|
||||||
} from "utils/types"
|
} from "utils/types"
|
||||||
import {
|
import {
|
||||||
getProblemSetDetail,
|
getProblemSetDetail,
|
||||||
@@ -37,7 +37,7 @@ const problemSetId = computed(() => Number(route.params.problemSetId))
|
|||||||
const problemSet = ref<ProblemSet | null>(null)
|
const problemSet = ref<ProblemSet | null>(null)
|
||||||
const problems = ref<ProblemSetProblem[]>([])
|
const problems = ref<ProblemSetProblem[]>([])
|
||||||
const badges = ref<ProblemSetBadge[]>([])
|
const badges = ref<ProblemSetBadge[]>([])
|
||||||
const progress = ref<ProblemSetProgress[]>([])
|
const progress = ref<AdminProblemSetProgress[]>([])
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
const showAddProblemModal = ref(false)
|
const showAddProblemModal = ref(false)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
CLASS_NAME_RE,
|
CLASS_NAME_RE,
|
||||||
} from "utils/constants"
|
} from "utils/constants"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import type { Server } from "utils/types"
|
import type { OrphanTestCase, Server, WebsiteConfig } from "utils/types"
|
||||||
import { useConfigStore } from "shared/store/config"
|
import { useConfigStore } from "shared/store/config"
|
||||||
import { useConfigWebSocket } from "shared/composables/websocket"
|
import { useConfigWebSocket } from "shared/composables/websocket"
|
||||||
import {
|
import {
|
||||||
@@ -98,8 +98,8 @@ const serverColumns: DataTableColumn<Server>[] = [
|
|||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{ title: "IP", key: "ip", width: 140 },
|
{ title: "IP", key: "ip", width: 140 },
|
||||||
{ title: "判题机版本", key: "judger_version", width: 100 },
|
{ title: "判题机版本", key: "judgerVersion", width: 100 },
|
||||||
{ title: "服务器 URL", key: "service_url", width: 200 },
|
{ title: "服务器 URL", key: "serviceUrl", width: 200 },
|
||||||
{
|
{
|
||||||
title: "上一次心跳",
|
title: "上一次心跳",
|
||||||
key: "last_heartbeat",
|
key: "last_heartbeat",
|
||||||
@@ -114,14 +114,14 @@ const serverColumns: DataTableColumn<Server>[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const testcases = ref<Testcase[]>([])
|
const testcases = ref<OrphanTestCase[]>([])
|
||||||
const token = ref("")
|
const token = ref("")
|
||||||
const servers = ref<Server[]>([])
|
const servers = ref<Server[]>([])
|
||||||
const abnormalServers = computed(() =>
|
const abnormalServers = computed(() =>
|
||||||
servers.value.filter((item) => item.status === "abnormal"),
|
servers.value.filter((item) => item.status === "abnormal"),
|
||||||
)
|
)
|
||||||
|
|
||||||
const websiteConfig = reactive({
|
const websiteConfig = reactive<WebsiteConfig>({
|
||||||
websiteBaseUrl: import.meta.env.PUBLIC_OJ_URL,
|
websiteBaseUrl: import.meta.env.PUBLIC_OJ_URL,
|
||||||
websiteName: "判题狗",
|
websiteName: "判题狗",
|
||||||
websiteNameShortcut: "判题狗",
|
websiteNameShortcut: "判题狗",
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ function typeTagType(type: ExerciseType) {
|
|||||||
<n-form-item label="选项(勾选所有正确答案)">
|
<n-form-item label="选项(勾选所有正确答案)">
|
||||||
<n-space vertical style="width: 100%">
|
<n-space vertical style="width: 100%">
|
||||||
<n-flex
|
<n-flex
|
||||||
v-for="(opt, i) in mcqOptions"
|
v-for="(_opt, i) in mcqOptions"
|
||||||
:key="i"
|
:key="i"
|
||||||
align="center"
|
align="center"
|
||||||
:size="8"
|
:size="8"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { useUserStore } from "./shared/store/user"
|
|||||||
|
|
||||||
const authStore = useAuthModalStore()
|
const authStore = useAuthModalStore()
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
// 检查是否需要认证
|
// 检查是否需要认证
|
||||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||||
if (!storage.get(STORAGE_KEY.AUTHED)) {
|
if (!storage.get(STORAGE_KEY.AUTHED)) {
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ const title = computed(() => {
|
|||||||
const data = computed<ChartData<"bar" | "line">>(() => {
|
const data = computed<ChartData<"bar" | "line">>(() => {
|
||||||
return {
|
return {
|
||||||
labels: aiStore.durationData.map((duration) => {
|
labels: aiStore.durationData.map((duration) => {
|
||||||
let prefix = "周"
|
|
||||||
if (duration.unit === "months") {
|
|
||||||
prefix = "月"
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
parseTime(duration.start, "M月D日"),
|
parseTime(duration.start, "M月D日"),
|
||||||
parseTime(duration.end, "M月D日"),
|
parseTime(duration.end, "M月D日"),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChartData, ChartOptions, TooltipItem } from "chart.js"
|
import type { ChartData, TooltipItem } from "chart.js"
|
||||||
import { Chart } from "vue-chartjs"
|
import { Chart } from "vue-chartjs"
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
|
|||||||
@@ -99,17 +99,6 @@ const streakData = computed(() => {
|
|||||||
let weekCount = 0
|
let weekCount = 0
|
||||||
let monthCount = 0
|
let monthCount = 0
|
||||||
|
|
||||||
// 检查今天是否有做题
|
|
||||||
const todayData = sortedData.find((item) => {
|
|
||||||
const itemDate = new Date(item.timestamp)
|
|
||||||
return (
|
|
||||||
itemDate.getFullYear() === today.getFullYear() &&
|
|
||||||
itemDate.getMonth() === today.getMonth() &&
|
|
||||||
itemDate.getDate() === today.getDate()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
const hasToday = todayData && todayData.value > 0
|
|
||||||
|
|
||||||
// 遍历数据计算连续天数
|
// 遍历数据计算连续天数
|
||||||
for (const item of sortedData) {
|
for (const item of sortedData) {
|
||||||
if (item.value > 0) {
|
if (item.value > 0) {
|
||||||
|
|||||||
+23
-14
@@ -1,12 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
type AiAnalysisRecord,
|
type AiAnalysisRecord,
|
||||||
type Contest as OjContest,
|
type Contest as OjContest,
|
||||||
|
type ContestAccess,
|
||||||
type ContestList,
|
type ContestList,
|
||||||
type ActivityRankItem,
|
type ActivityRankItem,
|
||||||
|
type FormatCodeResponse,
|
||||||
|
type Metrics,
|
||||||
|
type TutorialSummary,
|
||||||
type ClassComparisonResponse,
|
type ClassComparisonResponse,
|
||||||
type ClassRankItem,
|
type ClassRankItem,
|
||||||
type ClassUserRank,
|
type ClassUserRank,
|
||||||
type ContestRank,
|
|
||||||
type UserRank,
|
type UserRank,
|
||||||
type ProblemRank,
|
type ProblemRank,
|
||||||
type CreateSubmissionResponse,
|
type CreateSubmissionResponse,
|
||||||
@@ -38,6 +41,7 @@ import api2 from "utils/api2"
|
|||||||
import { filterResult } from "oj/transforms"
|
import { filterResult } from "oj/transforms"
|
||||||
import type {
|
import type {
|
||||||
Announcement,
|
Announcement,
|
||||||
|
ContestRank,
|
||||||
Profile,
|
Profile,
|
||||||
Message,
|
Message,
|
||||||
SubmissionListItem,
|
SubmissionListItem,
|
||||||
@@ -84,10 +88,6 @@ export function getAuthors(all = false) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRandomProblemID() {
|
|
||||||
return api2.get("problems/random")
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProblem(problemID: string, contestID: string) {
|
export async function getProblem(problemID: string, contestID: string) {
|
||||||
const endpoint = contestID
|
const endpoint = contestID
|
||||||
? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}`
|
? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}`
|
||||||
@@ -96,8 +96,9 @@ export async function getProblem(problemID: string, contestID: string) {
|
|||||||
return { error: null, data: detailProblem(response.data) }
|
return { error: null, data: detailProblem(response.data) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 未登录返回 "0",登录后返回百分比字符串
|
||||||
export function getProblemBeatRate(problemID: number) {
|
export function getProblemBeatRate(problemID: number) {
|
||||||
return api2.get(`problems/${problemID}/beat-count`)
|
return api2.get<string>(`problems/${problemID}/beat-count`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSubmission(id: string) {
|
export async function getSubmission(id: string) {
|
||||||
@@ -121,7 +122,7 @@ export function formatCode(data: { code: string; language: string }) {
|
|||||||
"C++": "cpp",
|
"C++": "cpp",
|
||||||
SQL: "sql",
|
SQL: "sql",
|
||||||
}
|
}
|
||||||
return api2.post("code/format", {
|
return api2.post<FormatCodeResponse>("code/format", {
|
||||||
code: data.code,
|
code: data.code,
|
||||||
language: languages[data.language] ?? data.language.toLowerCase(),
|
language: languages[data.language] ?? data.language.toLowerCase(),
|
||||||
})
|
})
|
||||||
@@ -148,7 +149,9 @@ export function getTodaySubmissionCount(language?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function adminRejudge(id: string) {
|
export function adminRejudge(id: string) {
|
||||||
return api2.post(`submissions/${encodeURIComponent(id)}/rejudge`)
|
return api2.post<{ ok: boolean }>(
|
||||||
|
`submissions/${encodeURIComponent(id)}/rejudge`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSubmissionStatistics(
|
export function getSubmissionStatistics(
|
||||||
@@ -219,13 +222,17 @@ export function getContest(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getContestAccess(id: string) {
|
export function getContestAccess(id: string) {
|
||||||
return api2.get(`contests/${encodeURIComponent(id)}/access`)
|
return api2.get<ContestAccess>(`contests/${encodeURIComponent(id)}/access`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 注意和 GET /access 不一样:这个返回裸 true,密码错是 403 走 catch
|
||||||
export function checkContestPassword(contestID: string, password: string) {
|
export function checkContestPassword(contestID: string, password: string) {
|
||||||
return api2.post(`contests/${encodeURIComponent(contestID)}/access`, {
|
return api2.post<boolean>(
|
||||||
|
`contests/${encodeURIComponent(contestID)}/access`,
|
||||||
|
{
|
||||||
password,
|
password,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getContestProblems(contestID: string) {
|
export async function getContestProblems(contestID: string) {
|
||||||
@@ -239,7 +246,9 @@ export function getContestRank(
|
|||||||
contestID: string,
|
contestID: string,
|
||||||
query: { limit: number; offset: number },
|
query: { limit: number; offset: number },
|
||||||
) {
|
) {
|
||||||
return api2.get<ContestRank>(
|
// submissionInfo 在契约里是 Record<string, unknown>(JSONB 原文),
|
||||||
|
// 前端在这里收窄成 SubmissionInfo,见 utils/types 的 ContestRank
|
||||||
|
return api2.get<{ results: ContestRank[]; total: number }>(
|
||||||
`contests/${encodeURIComponent(contestID)}/rank`,
|
`contests/${encodeURIComponent(contestID)}/rank`,
|
||||||
{ params: query },
|
{ params: query },
|
||||||
)
|
)
|
||||||
@@ -300,7 +309,7 @@ export function refreshUserProblemDisplayIds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getMetrics(userid: number) {
|
export function getMetrics(userid: number) {
|
||||||
return api2.get(`users/${userid}/metrics`)
|
return api2.get<Metrics>(`users/${userid}/metrics`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTutorial(id: number) {
|
export function getTutorial(id: number) {
|
||||||
@@ -308,7 +317,7 @@ export function getTutorial(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getTutorials(type: "python" | "c") {
|
export function getTutorials(type: "python" | "c") {
|
||||||
return api2.get("tutorials", { params: { type } })
|
return api2.get<TutorialSummary[]>("tutorials", { params: { type } })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAIDetailData(start: string, end: string, username?: string) {
|
export function getAIDetailData(start: string, end: string, username?: string) {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ function reset() {
|
|||||||
|
|
||||||
<n-space vertical :size="6">
|
<n-space vertical :size="6">
|
||||||
<div
|
<div
|
||||||
v-for="(line, idx) in data.lines"
|
v-for="(_line, idx) in data.lines"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
:style="lineStyle(idx)"
|
:style="lineStyle(idx)"
|
||||||
@click="toggle(idx)"
|
@click="toggle(idx)"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Icon } from "@iconify/vue"
|
|||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
|
import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
|
||||||
|
import type { SUBMISSION_RESULT } from "utils/types"
|
||||||
import { getACRateNumber, getTagColor, parseTime } from "utils/functions"
|
import { getACRateNumber, getTagColor, parseTime } from "utils/functions"
|
||||||
import { Pie } from "vue-chartjs"
|
import { Pie } from "vue-chartjs"
|
||||||
import {
|
import {
|
||||||
@@ -30,19 +31,19 @@ const beatRate = ref("0")
|
|||||||
const yearlyACData = ref<YearlyACData[]>([])
|
const yearlyACData = ref<YearlyACData[]>([])
|
||||||
|
|
||||||
const data = computed(() => {
|
const data = computed(() => {
|
||||||
const status = problem.value!.statisticInfo
|
// statisticInfo 是 { 判题状态码: 次数 } 的 JSONB,契约里是 Record<string, unknown>
|
||||||
const labels = []
|
const status = problem.value!.statisticInfo as Record<string, number>
|
||||||
for (let i in status) {
|
const labels: string[] = []
|
||||||
if (status[i] !== 0) {
|
const values: number[] = []
|
||||||
// @ts-ignore
|
for (const code in status) {
|
||||||
labels.push(JUDGE_STATUS[i]["name"])
|
if (status[code] !== 0) {
|
||||||
|
labels.push(JUDGE_STATUS[Number(code) as SUBMISSION_RESULT].name)
|
||||||
|
values.push(status[code]!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets: [{ data: values, hoverOffset: 5, borderRadius: 10 }],
|
||||||
{ data: Object.values(status), hoverOffset: 5, borderRadius: 10 },
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import { NFlex, NTag } from "naive-ui"
|
import { NFlex, NTag } from "naive-ui"
|
||||||
import { useRouteQuery } from "@vueuse/router"
|
import { useRouteQuery } from "@vueuse/router"
|
||||||
import { getProblemList, getRandomProblemID } from "oj/api"
|
import { getProblemList } from "oj/api"
|
||||||
import { getTagColor } from "utils/functions"
|
import { getTagColor } from "utils/functions"
|
||||||
import type { ProblemFiltered } from "utils/types"
|
import type { ProblemFiltered } from "utils/types"
|
||||||
import { getProblemTagList } from "shared/api"
|
import { getProblemTagList } from "shared/api"
|
||||||
@@ -102,11 +102,6 @@ function chooseTag(tag: Tag) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRandom() {
|
|
||||||
const res = await getRandomProblemID()
|
|
||||||
router.push("/problem/" + res.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听搜索关键词变化(防抖)
|
// 监听搜索关键词变化(防抖)
|
||||||
watchDebounced(() => query.keyword, listProblems, {
|
watchDebounced(() => query.keyword, listProblems, {
|
||||||
debounce: 500,
|
debounce: 500,
|
||||||
@@ -263,9 +258,6 @@ function rowProps(row: ProblemFiltered) {
|
|||||||
<n-form-item>
|
<n-form-item>
|
||||||
<n-button @click="clearQuery" quaternary>重置</n-button>
|
<n-button @click="clearQuery" quaternary>重置</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<!-- <n-form-item>
|
|
||||||
<n-button @click="getRandom" quaternary>随机</n-button>
|
|
||||||
</n-form-item> -->
|
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<n-button
|
<n-button
|
||||||
@click="toggleShowTag()"
|
@click="toggleShowTag()"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { formatISO, getTime, parseISO } from "date-fns"
|
|||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { ContestStatus, ContestType } from "utils/constants"
|
import { ContestStatus, ContestType } from "utils/constants"
|
||||||
import { duration } from "utils/functions"
|
import { duration } from "utils/functions"
|
||||||
import type { Contest, Problem } from "utils/types"
|
import type { OjContest, ProblemFiltered } from "utils/types"
|
||||||
import {
|
import {
|
||||||
checkContestPassword,
|
checkContestPassword,
|
||||||
getContest,
|
getContest,
|
||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
export const useContestStore = defineStore("contest", () => {
|
export const useContestStore = defineStore("contest", () => {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const [access, toggleAccess] = useToggle(false)
|
const [access, toggleAccess] = useToggle(false)
|
||||||
const contest = ref<Contest | null>(null)
|
const contest = ref<OjContest | null>(null)
|
||||||
const problems = ref<Problem[]>([])
|
const problems = ref<ProblemFiltered[]>([])
|
||||||
const now = ref(0)
|
const now = ref(0)
|
||||||
|
|
||||||
let timer = 0
|
let timer = 0
|
||||||
@@ -59,7 +59,8 @@ export const useContestStore = defineStore("contest", () => {
|
|||||||
problems.value = []
|
problems.value = []
|
||||||
const res = await getContest(contestID)
|
const res = await getContest(contestID)
|
||||||
contest.value = res.data
|
contest.value = res.data
|
||||||
now.value = getTime(parseISO(res.data.now))
|
// now 是学生侧比赛专有的服务器时间,用来对齐倒计时
|
||||||
|
now.value = getTime(parseISO(res.data.now ?? res.data.createTime))
|
||||||
if (contestStatus.value !== ContestStatus.finished) {
|
if (contestStatus.value !== ContestStatus.finished) {
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
now.value = now.value + 1000
|
now.value = now.value + 1000
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ async function init() {
|
|||||||
try {
|
try {
|
||||||
const res = await getProfile(route.query.name as string)
|
const res = await getProfile(route.query.name as string)
|
||||||
profile.value = res.data
|
profile.value = res.data
|
||||||
const acm = res.data!.acmProblemsStatus.problems || {}
|
// 用户不存在时后端返回 null,后面的统计全都无从算起
|
||||||
|
if (!res.data) return
|
||||||
|
const acm = res.data.acmProblemsStatus.problems || {}
|
||||||
const ac: string[] = []
|
const ac: string[] = []
|
||||||
Object.keys(acm).forEach((id) => {
|
Object.keys(acm).forEach((id) => {
|
||||||
if (acm[id]["status"] === 0) {
|
if (acm[id]["status"] === 0) {
|
||||||
@@ -74,8 +76,8 @@ async function init() {
|
|||||||
ac.sort()
|
ac.sort()
|
||||||
problems.value = ac
|
problems.value = ac
|
||||||
|
|
||||||
if (profile.value.submissionNumber > 0) {
|
if (res.data.submissionNumber > 0) {
|
||||||
const metricsRes = await getMetrics(profile.value.user.id)
|
const metricsRes = await getMetrics(res.data.user.id)
|
||||||
firstSubmissionAt.value = parseTime(metricsRes.data.first)
|
firstSubmissionAt.value = parseTime(metricsRes.data.first)
|
||||||
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
|
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
|
||||||
toLatestAt.value = durationToDays(
|
toLatestAt.value = durationToDays(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { userProfileSchema } from "@oj2/contract"
|
import { userProfileSchema, type Quote } from "@oj2/contract"
|
||||||
import api2 from "utils/api2"
|
import api2 from "utils/api2"
|
||||||
import type { ApiResponse } from "utils/api2"
|
import type { ApiResponse } from "utils/api2"
|
||||||
import type { Profile, Tag } from "utils/types"
|
import type { Profile, Tag } from "utils/types"
|
||||||
@@ -38,9 +38,11 @@ export function getProblemTagList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getHitokoto() {
|
export function getHitokoto() {
|
||||||
return api2.get("quotes/random")
|
return api2.get<Quote>("quotes/random")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClassUsernames(classroom: string) {
|
export function getClassUsernames(classroom: string) {
|
||||||
return api2.get(`classes/${encodeURIComponent(classroom)}/usernames`)
|
return api2.get<string[]>(
|
||||||
|
`classes/${encodeURIComponent(classroom)}/usernames`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onUnmounted, nextTick, computed, watch } from "vue"
|
import { ref, onUnmounted, nextTick, computed } from "vue"
|
||||||
import { getNodeTypeConfig } from "./useNodeStyles"
|
import { getNodeTypeConfig } from "./useNodeStyles"
|
||||||
import NodeHandles from "./NodeHandles.vue"
|
import NodeHandles from "./NodeHandles.vue"
|
||||||
import NodeActions from "./NodeActions.vue"
|
import NodeActions from "./NodeActions.vue"
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ const { height = "calc(100vh - 133px)" } = defineProps<Props>()
|
|||||||
const { addEdges, removeNodes, removeEdges } = useVueFlow()
|
const { addEdges, removeNodes, removeEdges } = useVueFlow()
|
||||||
|
|
||||||
// 节点和边的响应式数据
|
// 节点和边的响应式数据
|
||||||
const nodes = ref<Node[]>([])
|
// 显式标注成 Ref<Node[]>,不走 ref<T>() 的 UnwrapRef 推导 —— vue-flow 的
|
||||||
const edges = ref<Edge[]>([])
|
// Node/Edge 嵌套很深,让 TS 去调和两种形态会直接撞上「实例化层级过深」
|
||||||
|
const nodes = ref([]) as Ref<Node[]>
|
||||||
|
const edges = ref([]) as Ref<Edge[]>
|
||||||
|
|
||||||
// 历史记录管理
|
// 历史记录管理
|
||||||
const { canUndo, canRedo, saveState, undo, redo } = useHistory()
|
const { canUndo, canRedo, saveState, undo, redo } = useHistory()
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ const hitokoto = reactive({
|
|||||||
async function receive() {
|
async function receive() {
|
||||||
try {
|
try {
|
||||||
const res = await getHitokoto()
|
const res = await getHitokoto()
|
||||||
hitokoto.sentence = res.data.hitokoto
|
// 契约是 string | Record —— 一言数据集不在仓库里,形状留了余地
|
||||||
hitokoto.from = res.data.from
|
const quote = res.data
|
||||||
|
if (typeof quote === "string") {
|
||||||
|
hitokoto.sentence = quote
|
||||||
|
hitokoto.from = ""
|
||||||
|
} else {
|
||||||
|
hitokoto.sentence = String(quote.hitokoto ?? "")
|
||||||
|
hitokoto.from = String(quote.from ?? "")
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hitokoto.sentence = "获取一言失败,请点击重试"
|
hitokoto.sentence = "获取一言失败,请点击重试"
|
||||||
hitokoto.from = "DEV"
|
hitokoto.from = "DEV"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const rules: FormRules = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
loginRef.value!.validate(async (errors: FormRules | undefined) => {
|
loginRef.value!.validate(async (errors?: unknown) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
try {
|
try {
|
||||||
authStore.clearLoginError()
|
authStore.clearLoginError()
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function goLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
signupRef.value!.validate(async (errors: FormRules | undefined) => {
|
signupRef.value!.validate(async (errors?: unknown) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
try {
|
try {
|
||||||
authStore.clearSignupError()
|
authStore.clearSignupError()
|
||||||
|
|||||||
@@ -258,14 +258,14 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
|
|||||||
/**
|
/**
|
||||||
* 断开连接钩子(子类可重写)
|
* 断开连接钩子(子类可重写)
|
||||||
*/
|
*/
|
||||||
protected onDisconnected(event: CloseEvent) {
|
protected onDisconnected(_event: CloseEvent) {
|
||||||
// 子类实现
|
// 子类实现
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 错误钩子(子类可重写)
|
* 错误钩子(子类可重写)
|
||||||
*/
|
*/
|
||||||
protected onError(error: Event) {
|
protected onError(_error: Event) {
|
||||||
// 子类实现
|
// 子类实现
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios, { type AxiosRequestConfig } from "axios"
|
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"
|
||||||
import { createDiscreteApi } from "naive-ui"
|
import { createDiscreteApi } from "naive-ui"
|
||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
import { STORAGE_KEY } from "./constants"
|
import { STORAGE_KEY } from "./constants"
|
||||||
@@ -52,7 +52,14 @@ instance.interceptors.request.use((config) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(response) => Promise.resolve({ error: null, data: response.data.data }),
|
// 这里**故意**不返回 AxiosResponse:把 { data } 信封剥掉,让调用方直接拿到
|
||||||
|
// ApiResponse。类型上和 axios 的拦截器签名对不上(它期望原样返回响应),
|
||||||
|
// 文件末尾的 `as unknown as Api2Client` 就是为了把这个真实形状交出去。
|
||||||
|
((response: AxiosResponse) =>
|
||||||
|
Promise.resolve({
|
||||||
|
error: null,
|
||||||
|
data: response.data.data,
|
||||||
|
})) as unknown as (response: AxiosResponse) => AxiosResponse,
|
||||||
(error) => {
|
(error) => {
|
||||||
const payload = error.response?.data as Api2Error | undefined
|
const payload = error.response?.data as Api2Error | undefined
|
||||||
const code = payload?.error?.code ?? "network-error"
|
const code = payload?.error?.code ?? "network-error"
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ async function download(url: string) {
|
|||||||
const headers = res.headers
|
const headers = res.headers
|
||||||
const link = document.createElement("a")
|
const link = document.createElement("a")
|
||||||
link.href = window.URL.createObjectURL(
|
link.href = window.URL.createObjectURL(
|
||||||
new window.Blob([res.data], { type: headers["content-type"] }),
|
new window.Blob([res.data], {
|
||||||
|
type: String(headers["content-type"] ?? ""),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
link.download = (headers["content-disposition"] || "").split("filename=")[1]
|
link.download = (headers["content-disposition"] || "").split("filename=")[1]
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
|
|||||||
@@ -400,11 +400,6 @@ export function trickOrTreat() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取点击位置相对于视口的位置
|
|
||||||
const rect = target.getBoundingClientRect()
|
|
||||||
const clickX = e.clientX - rect.left - rect.width / 2
|
|
||||||
const clickY = e.clientY - rect.top - rect.height / 2
|
|
||||||
|
|
||||||
// 计算飞出方向(随机方向)
|
// 计算飞出方向(随机方向)
|
||||||
const angle = Math.random() * Math.PI * 2
|
const angle = Math.random() * Math.PI * 2
|
||||||
const distance = 1000 + Math.random() * 500
|
const distance = 1000 + Math.random() * 500
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ export type {
|
|||||||
AdminUserList,
|
AdminUserList,
|
||||||
AcmHelperItem,
|
AcmHelperItem,
|
||||||
AdminContestList,
|
AdminContestList,
|
||||||
|
AdminProblemSetProgress,
|
||||||
AdminAiReport,
|
AdminAiReport,
|
||||||
AdminAiReportListItem,
|
AdminAiReportListItem,
|
||||||
AdminAiReportList,
|
AdminAiReportList,
|
||||||
|
|||||||
@@ -51,3 +51,4 @@ export type ProblemRank = z.infer<typeof problemRankSchema>
|
|||||||
export type RankProfile = z.infer<typeof rankProfileSchema>
|
export type RankProfile = z.infer<typeof rankProfileSchema>
|
||||||
export type UserRank = z.infer<typeof userRankSchema>
|
export type UserRank = z.infer<typeof userRankSchema>
|
||||||
export type ActivityRankItem = z.infer<typeof activityRankItemSchema>
|
export type ActivityRankItem = z.infer<typeof activityRankItemSchema>
|
||||||
|
export type Metrics = z.infer<typeof metricsSchema>
|
||||||
|
|||||||
@@ -656,3 +656,4 @@ export type RenameTagResponse = z.infer<typeof renameTagResponseSchema>
|
|||||||
export type BatchProblemTagResponse = z.infer<typeof batchProblemTagResponseSchema>
|
export type BatchProblemTagResponse = z.infer<typeof batchProblemTagResponseSchema>
|
||||||
export type SqlTestCaseScript = z.infer<typeof sqlTestCaseScriptSchema>
|
export type SqlTestCaseScript = z.infer<typeof sqlTestCaseScriptSchema>
|
||||||
export type GenerateSqlTestCaseResponse = z.infer<typeof generateSqlTestCaseResponseSchema>
|
export type GenerateSqlTestCaseResponse = z.infer<typeof generateSqlTestCaseResponseSchema>
|
||||||
|
export type AdminProblemSetProgress = z.infer<typeof adminProblemSetProgressSchema>
|
||||||
|
|||||||
@@ -76,3 +76,4 @@ export const exerciseSchema = z.object({
|
|||||||
export type Message = z.infer<typeof messageSchema>
|
export type Message = z.infer<typeof messageSchema>
|
||||||
export type MessageList = z.infer<typeof messageListSchema>
|
export type MessageList = z.infer<typeof messageListSchema>
|
||||||
export type Announcement = z.infer<typeof announcementSchema>
|
export type Announcement = z.infer<typeof announcementSchema>
|
||||||
|
export type TutorialSummary = z.infer<typeof tutorialSummarySchema>
|
||||||
|
|||||||
@@ -45,3 +45,4 @@ export type Contest = z.infer<typeof contestSchema>
|
|||||||
export type ContestList = z.infer<typeof contestListSchema>
|
export type ContestList = z.infer<typeof contestListSchema>
|
||||||
export type ContestRankItem = z.infer<typeof contestRankItemSchema>
|
export type ContestRankItem = z.infer<typeof contestRankItemSchema>
|
||||||
export type ContestRank = z.infer<typeof contestRankSchema>
|
export type ContestRank = z.infer<typeof contestRankSchema>
|
||||||
|
export type ContestAccess = z.infer<typeof contestAccessSchema>
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ export const quoteSchema = z.union([
|
|||||||
])
|
])
|
||||||
|
|
||||||
export type WebsiteConfig = z.infer<typeof websiteConfigSchema>
|
export type WebsiteConfig = z.infer<typeof websiteConfigSchema>
|
||||||
|
export type Quote = z.infer<typeof quoteSchema>
|
||||||
|
|||||||
@@ -145,3 +145,4 @@ export type SubmissionListItem = z.infer<typeof submissionListItemSchema>
|
|||||||
export type SubmissionList = z.infer<typeof submissionListSchema>
|
export type SubmissionList = z.infer<typeof submissionListSchema>
|
||||||
export type EmbeddedSubmission = z.infer<typeof embeddedSubmissionSchema>
|
export type EmbeddedSubmission = z.infer<typeof embeddedSubmissionSchema>
|
||||||
export type CreateSubmissionResponse = z.infer<typeof createSubmissionResponseSchema>
|
export type CreateSubmissionResponse = z.infer<typeof createSubmissionResponseSchema>
|
||||||
|
export type FormatCodeResponse = z.infer<typeof formatCodeResponseSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user