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:
2026-08-25 14:04:10 -06:00
parent 3d501e8b69
commit 1198895b54
33 changed files with 119 additions and 99 deletions

View File

@@ -28,7 +28,9 @@ const hljsInstance = ref<any>(null)
const loadHighlightJS = async () => {
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/languages/c"),
import("highlight.js/lib/languages/cpp"),
@@ -37,15 +39,16 @@ const loadHighlightJS = async () => {
import("highlight.js/lib/languages/javascript"),
import("highlight.js/lib/languages/go"),
import("highlight.js/lib/languages/sql"),
]).then((modules) => modules.map((m) => m.default))
])
const hljs = core.default
hljs.registerLanguage("c", c)
hljs.registerLanguage("python", python)
hljs.registerLanguage("cpp", cpp)
hljs.registerLanguage("java", java)
hljs.registerLanguage("javascript", javascript)
hljs.registerLanguage("go", go)
hljs.registerLanguage("sql", sql)
hljs.registerLanguage("c", c.default)
hljs.registerLanguage("python", python.default)
hljs.registerLanguage("cpp", cpp.default)
hljs.registerLanguage("java", java.default)
hljs.registerLanguage("javascript", javascript.default)
hljs.registerLanguage("go", go.default)
hljs.registerLanguage("sql", sql.default)
hljsInstance.value = hljs
return hljs

View File

@@ -2,11 +2,13 @@ import api2 from "utils/api2"
import { toProblemListItem } from "admin/transforms"
import type {
AcTrend,
AdminProblemSetProgress,
BatchProblemTagResponse,
GenerateSqlTestCaseResponse,
RenameTagResponse,
SqlTestCaseScript,
AcmHelperItem,
SubmissionInfo,
AdminAiReport,
AdminAiReportList,
StuckProblem,
@@ -36,7 +38,6 @@ import type {
ProblemSetBadge,
ProblemSetList,
ProblemSetProblem,
ProblemSetProgressList,
TutorialListItem,
} from "utils/types"
@@ -476,7 +477,11 @@ export function makeProblemPublic(id: number, displayId: string) {
// 比赛辅助检查
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(
@@ -652,8 +657,9 @@ export function deleteProblemSetBadge(problemSetId: number, badgeId: number) {
}
// 题单进度管理 API
// 注意:返回的是裸数组,不是分页信封 —— 和 oj 侧的 /user-progress 不同
export function getProblemSetProgress(problemSetId: number) {
return api2.get<ProblemSetProgressList>(
return api2.get<AdminProblemSetProgress[]>(
`admin/problem-sets/${problemSetId}/progress`,
)
}

View File

@@ -4,7 +4,7 @@ import type {
ProblemSet,
ProblemSetProblem,
ProblemSetBadge,
ProblemSetProgress,
AdminProblemSetProgress,
} from "utils/types"
import {
getProblemSetDetail,
@@ -37,7 +37,7 @@ const problemSetId = computed(() => Number(route.params.problemSetId))
const problemSet = ref<ProblemSet | null>(null)
const problems = ref<ProblemSetProblem[]>([])
const badges = ref<ProblemSetBadge[]>([])
const progress = ref<ProblemSetProgress[]>([])
const progress = ref<AdminProblemSetProgress[]>([])
// 模态框状态
const showAddProblemModal = ref(false)

View File

@@ -6,7 +6,7 @@ import {
CLASS_NAME_RE,
} from "utils/constants"
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 { useConfigWebSocket } from "shared/composables/websocket"
import {
@@ -98,8 +98,8 @@ const serverColumns: DataTableColumn<Server>[] = [
width: 100,
},
{ title: "IP", key: "ip", width: 140 },
{ title: "判题机版本", key: "judger_version", width: 100 },
{ title: "服务器 URL", key: "service_url", width: 200 },
{ title: "判题机版本", key: "judgerVersion", width: 100 },
{ title: "服务器 URL", key: "serviceUrl", width: 200 },
{
title: "上一次心跳",
key: "last_heartbeat",
@@ -114,14 +114,14 @@ const serverColumns: DataTableColumn<Server>[] = [
},
]
const testcases = ref<Testcase[]>([])
const testcases = ref<OrphanTestCase[]>([])
const token = ref("")
const servers = ref<Server[]>([])
const abnormalServers = computed(() =>
servers.value.filter((item) => item.status === "abnormal"),
)
const websiteConfig = reactive({
const websiteConfig = reactive<WebsiteConfig>({
websiteBaseUrl: import.meta.env.PUBLIC_OJ_URL,
websiteName: "判题狗",
websiteNameShortcut: "判题狗",

View File

@@ -427,7 +427,7 @@ function typeTagType(type: ExerciseType) {
<n-form-item label="选项(勾选所有正确答案)">
<n-space vertical style="width: 100%">
<n-flex
v-for="(opt, i) in mcqOptions"
v-for="(_opt, i) in mcqOptions"
:key="i"
align="center"
:size="8"

View File

@@ -10,7 +10,6 @@ interface Props {
}
const route = useRoute()
const router = useRouter()
const message = useMessage()
const props = defineProps<Props>()

View File

@@ -26,7 +26,7 @@ import { useUserStore } from "./shared/store/user"
const authStore = useAuthModalStore()
router.beforeEach(async (to, from, next) => {
router.beforeEach(async (to, _from, next) => {
// 检查是否需要认证
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!storage.get(STORAGE_KEY.AUTHED)) {

View File

@@ -60,10 +60,6 @@ const title = computed(() => {
const data = computed<ChartData<"bar" | "line">>(() => {
return {
labels: aiStore.durationData.map((duration) => {
let prefix = "周"
if (duration.unit === "months") {
prefix = "月"
}
return [
parseTime(duration.start, "M月D日"),
parseTime(duration.end, "M月D日"),

View File

@@ -9,7 +9,7 @@
</n-card>
</template>
<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 as ChartJS,

View File

@@ -99,17 +99,6 @@ const streakData = computed(() => {
let weekCount = 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) {
if (item.value > 0) {

View File

@@ -1,12 +1,15 @@
import {
type AiAnalysisRecord,
type Contest as OjContest,
type ContestAccess,
type ContestList,
type ActivityRankItem,
type FormatCodeResponse,
type Metrics,
type TutorialSummary,
type ClassComparisonResponse,
type ClassRankItem,
type ClassUserRank,
type ContestRank,
type UserRank,
type ProblemRank,
type CreateSubmissionResponse,
@@ -38,6 +41,7 @@ import api2 from "utils/api2"
import { filterResult } from "oj/transforms"
import type {
Announcement,
ContestRank,
Profile,
Message,
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) {
const endpoint = contestID
? `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) }
}
// 未登录返回 "0",登录后返回百分比字符串
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) {
@@ -121,7 +122,7 @@ export function formatCode(data: { code: string; language: string }) {
"C++": "cpp",
SQL: "sql",
}
return api2.post("code/format", {
return api2.post<FormatCodeResponse>("code/format", {
code: data.code,
language: languages[data.language] ?? data.language.toLowerCase(),
})
@@ -148,7 +149,9 @@ export function getTodaySubmissionCount(language?: 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(
@@ -219,13 +222,17 @@ export function getContest(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) {
return api2.post(`contests/${encodeURIComponent(contestID)}/access`, {
password,
})
return api2.post<boolean>(
`contests/${encodeURIComponent(contestID)}/access`,
{
password,
},
)
}
export async function getContestProblems(contestID: string) {
@@ -239,7 +246,9 @@ export function getContestRank(
contestID: string,
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`,
{ params: query },
)
@@ -300,7 +309,7 @@ export function refreshUserProblemDisplayIds() {
}
export function getMetrics(userid: number) {
return api2.get(`users/${userid}/metrics`)
return api2.get<Metrics>(`users/${userid}/metrics`)
}
export function getTutorial(id: number) {
@@ -308,7 +317,7 @@ export function getTutorial(id: number) {
}
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) {

View File

@@ -88,7 +88,7 @@ function reset() {
<n-space vertical :size="6">
<div
v-for="(line, idx) in data.lines"
v-for="(_line, idx) in data.lines"
:key="idx"
:style="lineStyle(idx)"
@click="toggle(idx)"

View File

@@ -3,6 +3,7 @@ import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import { useProblemStore } from "oj/store/problem"
import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
import type { SUBMISSION_RESULT } from "utils/types"
import { getACRateNumber, getTagColor, parseTime } from "utils/functions"
import { Pie } from "vue-chartjs"
import {
@@ -30,19 +31,19 @@ const beatRate = ref("0")
const yearlyACData = ref<YearlyACData[]>([])
const data = computed(() => {
const status = problem.value!.statisticInfo
const labels = []
for (let i in status) {
if (status[i] !== 0) {
// @ts-ignore
labels.push(JUDGE_STATUS[i]["name"])
// statisticInfo 是 { 判题状态码: 次数 } 的 JSONB契约里是 Record<string, unknown>
const status = problem.value!.statisticInfo as Record<string, number>
const labels: string[] = []
const values: number[] = []
for (const code in status) {
if (status[code] !== 0) {
labels.push(JUDGE_STATUS[Number(code) as SUBMISSION_RESULT].name)
values.push(status[code]!)
}
}
return {
labels,
datasets: [
{ data: Object.values(status), hoverOffset: 5, borderRadius: 10 },
],
datasets: [{ data: values, hoverOffset: 5, borderRadius: 10 }],
}
})

View File

@@ -2,7 +2,7 @@
import { Icon } from "@iconify/vue"
import { NFlex, NTag } from "naive-ui"
import { useRouteQuery } from "@vueuse/router"
import { getProblemList, getRandomProblemID } from "oj/api"
import { getProblemList } from "oj/api"
import { getTagColor } from "utils/functions"
import type { ProblemFiltered } from "utils/types"
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, {
debounce: 500,
@@ -263,9 +258,6 @@ function rowProps(row: ProblemFiltered) {
<n-form-item>
<n-button @click="clearQuery" quaternary>重置</n-button>
</n-form-item>
<!-- <n-form-item>
<n-button @click="getRandom" quaternary>随机</n-button>
</n-form-item> -->
<n-form-item>
<n-button
@click="toggleShowTag()"

View File

@@ -2,7 +2,7 @@ import { formatISO, getTime, parseISO } from "date-fns"
import { useUserStore } from "shared/store/user"
import { ContestStatus, ContestType } from "utils/constants"
import { duration } from "utils/functions"
import type { Contest, Problem } from "utils/types"
import type { OjContest, ProblemFiltered } from "utils/types"
import {
checkContestPassword,
getContest,
@@ -13,8 +13,8 @@ import {
export const useContestStore = defineStore("contest", () => {
const userStore = useUserStore()
const [access, toggleAccess] = useToggle(false)
const contest = ref<Contest | null>(null)
const problems = ref<Problem[]>([])
const contest = ref<OjContest | null>(null)
const problems = ref<ProblemFiltered[]>([])
const now = ref(0)
let timer = 0
@@ -59,7 +59,8 @@ export const useContestStore = defineStore("contest", () => {
problems.value = []
const res = await getContest(contestID)
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) {
timer = setInterval(() => {
now.value = now.value + 1000

View File

@@ -64,7 +64,9 @@ async function init() {
try {
const res = await getProfile(route.query.name as string)
profile.value = res.data
const acm = res.data!.acmProblemsStatus.problems || {}
// 用户不存在时后端返回 null后面的统计全都无从算起
if (!res.data) return
const acm = res.data.acmProblemsStatus.problems || {}
const ac: string[] = []
Object.keys(acm).forEach((id) => {
if (acm[id]["status"] === 0) {
@@ -74,8 +76,8 @@ async function init() {
ac.sort()
problems.value = ac
if (profile.value.submissionNumber > 0) {
const metricsRes = await getMetrics(profile.value.user.id)
if (res.data.submissionNumber > 0) {
const metricsRes = await getMetrics(res.data.user.id)
firstSubmissionAt.value = parseTime(metricsRes.data.first)
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
toLatestAt.value = durationToDays(

View File

@@ -1,4 +1,4 @@
import { userProfileSchema } from "@oj2/contract"
import { userProfileSchema, type Quote } from "@oj2/contract"
import api2 from "utils/api2"
import type { ApiResponse } from "utils/api2"
import type { Profile, Tag } from "utils/types"
@@ -38,9 +38,11 @@ export function getProblemTagList() {
}
export function getHitokoto() {
return api2.get("quotes/random")
return api2.get<Quote>("quotes/random")
}
export function getClassUsernames(classroom: string) {
return api2.get(`classes/${encodeURIComponent(classroom)}/usernames`)
return api2.get<string[]>(
`classes/${encodeURIComponent(classroom)}/usernames`,
)
}

View File

@@ -47,7 +47,7 @@
</template>
<script lang="ts" setup>
import { ref, onUnmounted, nextTick, computed, watch } from "vue"
import { ref, onUnmounted, nextTick, computed } from "vue"
import { getNodeTypeConfig } from "./useNodeStyles"
import NodeHandles from "./NodeHandles.vue"
import NodeActions from "./NodeActions.vue"

View File

@@ -32,8 +32,10 @@ const { height = "calc(100vh - 133px)" } = defineProps<Props>()
const { addEdges, removeNodes, removeEdges } = useVueFlow()
// 节点和边的响应式数据
const nodes = ref<Node[]>([])
const edges = ref<Edge[]>([])
// 显式标注成 Ref<Node[]>,不走 ref<T>() 的 UnwrapRef 推导 —— vue-flow 的
// Node/Edge 嵌套很深,让 TS 去调和两种形态会直接撞上「实例化层级过深」
const nodes = ref([]) as Ref<Node[]>
const edges = ref([]) as Ref<Edge[]>
// 历史记录管理
const { canUndo, canRedo, saveState, undo, redo } = useHistory()

View File

@@ -9,8 +9,15 @@ const hitokoto = reactive({
async function receive() {
try {
const res = await getHitokoto()
hitokoto.sentence = res.data.hitokoto
hitokoto.from = res.data.from
// 契约是 string | Record —— 一言数据集不在仓库里,形状留了余地
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) {
hitokoto.sentence = "获取一言失败,请点击重试"
hitokoto.from = "DEV"

View File

@@ -39,7 +39,7 @@ const rules: FormRules = {
}
async function submit() {
loginRef.value!.validate(async (errors: FormRules | undefined) => {
loginRef.value!.validate(async (errors?: unknown) => {
if (!errors) {
try {
authStore.clearLoginError()

View File

@@ -37,7 +37,7 @@ function goLogin() {
}
function submit() {
signupRef.value!.validate(async (errors: FormRules | undefined) => {
signupRef.value!.validate(async (errors?: unknown) => {
if (!errors) {
try {
authStore.clearSignupError()

View File

@@ -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) {
// 子类实现
}

View File

@@ -1,4 +1,4 @@
import axios, { type AxiosRequestConfig } from "axios"
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"
import { createDiscreteApi } from "naive-ui"
import { useAuthModalStore } from "shared/store/authModal"
import { STORAGE_KEY } from "./constants"
@@ -52,7 +52,14 @@ instance.interceptors.request.use((config) => {
})
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) => {
const payload = error.response?.data as Api2Error | undefined
const code = payload?.error?.code ?? "network-error"

View File

@@ -13,7 +13,9 @@ async function download(url: string) {
const headers = res.headers
const link = document.createElement("a")
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]
document.body.appendChild(link)

View File

@@ -400,11 +400,6 @@ export function trickOrTreat() {
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 distance = 1000 + Math.random() * 500

View File

@@ -436,6 +436,7 @@ export type {
AdminUserList,
AcmHelperItem,
AdminContestList,
AdminProblemSetProgress,
AdminAiReport,
AdminAiReportListItem,
AdminAiReportList,

View File

@@ -51,3 +51,4 @@ export type ProblemRank = z.infer<typeof problemRankSchema>
export type RankProfile = z.infer<typeof rankProfileSchema>
export type UserRank = z.infer<typeof userRankSchema>
export type ActivityRankItem = z.infer<typeof activityRankItemSchema>
export type Metrics = z.infer<typeof metricsSchema>

View File

@@ -656,3 +656,4 @@ export type RenameTagResponse = z.infer<typeof renameTagResponseSchema>
export type BatchProblemTagResponse = z.infer<typeof batchProblemTagResponseSchema>
export type SqlTestCaseScript = z.infer<typeof sqlTestCaseScriptSchema>
export type GenerateSqlTestCaseResponse = z.infer<typeof generateSqlTestCaseResponseSchema>
export type AdminProblemSetProgress = z.infer<typeof adminProblemSetProgressSchema>

View File

@@ -76,3 +76,4 @@ export const exerciseSchema = z.object({
export type Message = z.infer<typeof messageSchema>
export type MessageList = z.infer<typeof messageListSchema>
export type Announcement = z.infer<typeof announcementSchema>
export type TutorialSummary = z.infer<typeof tutorialSummarySchema>

View File

@@ -45,3 +45,4 @@ export type Contest = z.infer<typeof contestSchema>
export type ContestList = z.infer<typeof contestListSchema>
export type ContestRankItem = z.infer<typeof contestRankItemSchema>
export type ContestRank = z.infer<typeof contestRankSchema>
export type ContestAccess = z.infer<typeof contestAccessSchema>

View File

@@ -17,3 +17,4 @@ export const quoteSchema = z.union([
])
export type WebsiteConfig = z.infer<typeof websiteConfigSchema>
export type Quote = z.infer<typeof quoteSchema>

View File

@@ -145,3 +145,4 @@ export type SubmissionListItem = z.infer<typeof submissionListItemSchema>
export type SubmissionList = z.infer<typeof submissionListSchema>
export type EmbeddedSubmission = z.infer<typeof embeddedSubmissionSchema>
export type CreateSubmissionResponse = z.infer<typeof createSubmissionResponseSchema>
export type FormatCodeResponse = z.infer<typeof formatCodeResponseSchema>