@@ -1,98 +1,86 @@
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 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 SolvedList ,
type ProblemSet ,
type ProblemSetBadge ,
type ProblemSetList ,
type ProblemSetProblem ,
type ProblemSetProgressList ,
type UserBadge ,
problemDetailSchema ,
problemListSchema ,
problemListItemSchema ,
submissionDetailSchema ,
submissionListSchema ,
s ubmissionStatisticsSchema ,
s ubmissionStatisticsItemsSchema ,
onlineCountSchema ,
websiteConfigSchema ,
contestListSchema ,
contestSchema ,
contestAccessSchema ,
contestRankSchema ,
announcementListSchema ,
announcementSchema ,
problemSetListSchema ,
problemSetSchema ,
problemSetBadgeSchema ,
userBadgeSchema ,
tutorialSchema ,
metricsSchema ,
activityRankItemSchema ,
problemRankSchema ,
userRankSchema ,
flowchartListSchema ,
flowchartDetailSchema ,
flowchartCurrentSchema ,
flowchartStatisticsSchema ,
flowchartSubmissionSchema ,
exerciseSchema ,
exerciseDataByType ,
problemSetProgressListSchema ,
problemSetProblemSchema ,
tutorialSummarySchema ,
tutorialProgressSchema ,
messageListSchema ,
yearlyAcSchema ,
aiDetailSchema ,
solvedListSchema ,
durationDataSchema ,
heatmapItemSchema ,
loginSummarySchema ,
aiAnalysisRecordSchema ,
type FlowchartStatistics ,
type S ubmissionStatistics,
type S ubmissionStatisticsItems,
} from "@oj2/contract"
import api from "utils/api"
import { contract } from "utils/contract"
import { filterResult } from "oj/transforms"
import type {
Announcement ,
AnnouncementListItem ,
ContestRank ,
Profile ,
Message ,
SubmissionListItem ,
Exercise ,
Problem ,
ReactionKey ,
ReactionState ,
Submission ,
SubmissionListPayload ,
SubmitCodePayload ,
OnlineCount ,
WebsiteConfig ,
Tutorial ,
TutorialProgress ,
} from "utils/types"
/**
* 题目详情。走契约的 zod 解析,形状即契约 —— 之前这里手抄了一份 camel→snake 的
* 键名映射,抄漏一个字段就是静默 undefined。
*
* 走 `contract()` 而不是裸 `parse()`: 这里 原来是
* `problemDetailSchema.parse(value) as Problem` —— `as` 把校验结果又断言回本地
* 类型,等于校验白做。契约现在把 `languages` / `template` 都收进了联合,
* `Problem` 不再需要额外窄化,`as` 也就没有存在的理由了。
* 走 `contract()` 而不是裸 `parse()`:原来是 `problemDetailSchema.parse(v) as Problem`,
* `as` 把校验结果又断言回去、等于没校验,而 `parse` 抛错会让整个题目页白屏。
* 现在形状不符时记一条控制台分歧再放行原始数据。
*/
function detailProblem ( value : unknown ) : Problem {
return contract ( "GET /problems/:id" , problemDetailSchema , value )
}
export async function getWebsiteConfig() {
const endpoint = "site"
return contract (
"GET /site" ,
websiteConfigSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getWebsiteConfig() {
return api . get < WebsiteConfig > ( "site" )
}
/** 当前在线人数。只有聚合数字,「谁在线」在榜单接口里、且只对老师下发 */
export async function getOnlineCount() {
const endpoint = "site/online"
return contract (
"GET /site/online" ,
onlineCountSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getOnlineCount() {
return api . get < OnlineCount > ( "site/online" )
}
export async function getProblemList (
@@ -100,14 +88,9 @@ export async function getProblemList(
limit = 10 ,
searchParams : Record < string , unknown > = { } ,
) {
const endpoint = "problems"
const res = contract (
"GET /problems" ,
problemListSchema ,
await api . get < unknown > ( endpoint , {
params : { paging : true , offset , limit , . . . searchParams } ,
} ) ,
)
const res = await api . get < ProblemList > ( "problems" , {
params : { paging : true , offset , limit , . . . searchParams } ,
} )
return {
results : res.results.map ( filterResult ) ,
total : res.total ,
@@ -132,13 +115,11 @@ export function getProblemBeatRate(problemID: number) {
return api . get < string > ( ` problems/ ${ problemID } /beat-count ` )
}
export async function getSubmission ( id : string ) {
const endpoint = ` submissions/ ${ encodeURIComponent ( id ) } `
return contract (
"GET /submissions/:id" ,
submissionDetailSchema ,
await api . get < unknown > ( endpoint ) ,
export async function getSubmission ( id : string ) : Promise < Submission > {
const response = await api . get < unknown > (
` submissions/ ${ encodeURIComponent ( id ) } ` ,
)
return contract ( "GET /submissions/:id" , submissionDetailSchema , response )
}
export function submitCode ( data : SubmitCodePayload ) {
@@ -162,36 +143,16 @@ export function getSubmissions(params: Partial<SubmissionListPayload>) {
const endpoint = params . contestId
? ` contests/ ${ encodeURIComponent ( params . contestId ) } /submissions `
: "submissions"
return getSubmissionPage ( endpoint , params )
// 契约里 language 是 z.string()(语言是配置项,随时可能加,收紧成枚举会让
// 新加的语言在后端 parse 时直接抛),前端在这一处收窄成 LANGUAGE
return api . get < { results : SubmissionListItem [ ] ; total : number } > ( endpoint , {
// contestId 走的是路径, page 只有前端分页器用
params : { . . . params , contestId : undefined , page : undefined } ,
} )
}
/**
* 提交列表。后端在 `submissionListItemSchema.parse` 上真的会抛 —— 它逐个列表项
* 过 schema, 所以这条链路上的分歧**后端自己就拦住了**,前端这层校验是第二道保险:
* 主要防「后端加了字段但契约没跟上、前端类型声称有实际是 undefined」这类
* 只在展示端出问题的偏差。
*/
async function getSubmissionPage (
endpoint : string ,
params : Partial < SubmissionListPayload > ,
) {
return contract (
` GET / ${ endpoint } ` ,
submissionListSchema ,
await api . get < unknown > ( endpoint , {
// contestId 走的是路径, page 只有前端分页器用
params : { . . . params , contestId : undefined , page : undefined } ,
} ) ,
)
}
export async function getRankOfProblem ( problemId : string ) {
const endpoint = ` problems/ ${ encodeURIComponent ( problemId ) } /rank `
return contract (
"GET /problems/:id/rank" ,
problemRankSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getRankOfProblem ( problemId : string ) {
return api . get < ProblemRank > ( ` problems/ ${ encodeURIComponent ( problemId ) } /rank ` )
}
export function getTodaySubmissionCount ( language? : string ) {
@@ -208,56 +169,38 @@ export function adminRejudge(id: string) {
* 统计面板展开一行时拉这个人的明细。username 这里要**精确**到人,
* 和上面那个按班级模糊匹配的不是一回事。
*/
export async function getSubmissionStatisticsItems (
export function getSubmissionStatisticsItems (
duration : { start? : string ; end : string } ,
username : string ,
problemID? : string ,
) {
const endpoint = "submissions/statistics/items"
return contract (
"GET /submissions/statistics/items" ,
submissionStatisticsItemsSchema ,
await api . get < unknown > ( endpoint , {
params : { . . . duration , problemId : problemID , username } ,
} ) ,
)
return api . get < SubmissionStatisticsItems > ( "submissions/statistics/items" , {
params : { . . . duration , problemId : problemID , username } ,
} )
}
export async function getSubmissionStatistics (
export function getSubmissionStatistics (
duration : { start? : string ; end : string } ,
problemID? : string ,
username? : string ,
) {
const endpoint = "submissions/statistics"
return contract (
"GET /submissions/statistics" ,
submissionStatisticsSchema ,
await api . get < unknown > ( endpoint , {
params : { . . . duration , problemId : problemID , username } ,
} ) ,
)
return api . get < SubmissionStatistics > ( "submissions/statistics" , {
params : { . . . duration , problemId : problemID , username } ,
} )
}
/**
* 全服榜单。上限( 100 名)由服务端定,调用方只管翻页 ——
* 「全服 Top10」就是这个榜的第一页, 取 limit=10 即可,不需要另一个上限参数。
*/
export async function getRank ( offset : number , limit : number ) {
const endpoint = "rankings/users"
return contract (
"GET /rankings/users" ,
userRankSchema ,
await api . get < unknown > ( endpoint , { params : { offset , limit } } ) ,
)
export function getRank ( offset : number , limit : number ) {
return api . get < UserRank > ( "rankings/users" , { params : { offset , limit } } )
}
export async function getActivityRank ( start : string ) {
const endpoint = "rankings/activity"
return contract (
"GET /rankings/activity" ,
activityRankItemSchema . array ( ) ,
await api . get < unknown > ( endpoint , { params : { start } } ) ,
)
export function getActivityRank ( start : string ) {
return api . get < ActivityRankItem [ ] > ( "rankings/activity" , {
params : { start } ,
} )
}
export function getClassRank ( grade? : number | null ) {
@@ -286,37 +229,22 @@ export function getClassPK(
} )
}
export async function getContestList ( query : {
export function getContestList ( query : {
offset : number
limit : number
keyword : string
status : string
tag : string
} ) {
const endpoint = "contests"
return contract (
"GET /contests" ,
contestListSchema ,
await api . get < unknown > ( endpoint , { params : query } ) ,
)
return api . get < ContestList > ( "contests" , { params : query } )
}
export async function getContest ( id : string ) {
const endpoint = ` contests/ ${ encodeURIComponent ( id ) } `
return contract (
"GET /contests/:id" ,
contestSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getContest ( id : string ) {
return api . get < OjContest > ( ` contests/ ${ encodeURIComponent ( id ) } ` )
}
export async function getContestAccess ( id : string ) {
const endpoint = ` contests/ ${ encodeURIComponent ( id ) } /access `
return contract (
"GET /contests/:id/access" ,
contestAccessSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getContestAccess ( id : string ) {
return api . get < ContestAccess > ( ` contests/ ${ encodeURIComponent ( id ) } /access ` )
}
// 注意和 GET /access 不一样:这个返回裸 true, 密码错是 403 走 catch
@@ -330,29 +258,21 @@ export function checkContestPassword(contestID: string, password: string) {
}
export async function getContestProblems ( contestID : string ) {
const endpoint = ` contests/ ${ encodeURIComponent ( contestID ) } /problems `
// 用 problemListItemSchema.array(),不是契约的 contestProblemsSchema ——
// 后者是 `array(union([列表项, 详情]))`,联合类型会让 filterResult 的类型收窄
// 落到详情分支上,而且学生侧这条接口只下发列表项。
const res = contract (
"GET /contests/:id/problems" ,
problemListItemSchema . array ( ) ,
await api . get < unknown > ( endpoint ) ,
const res = await api . get < ProblemListItem [ ] > (
` contests/ ${ encodeURIComponent ( contestID ) } /problems ` ,
)
return res . map ( filterResult )
}
export async function getContestRank (
export function getContestRank (
contestID : string ,
query : { limit : number ; offset : number } ,
) {
// submissionInfo 在契约里是 Record<string, unknown>( JSONB 原文),
// 前端在这里收窄成 SubmissionInfo, 见 utils/types 的 ContestRank
const endpoint = ` contests/ ${ encodeURIComponent ( contestID ) } /rank `
return contract (
"GET /contests/:id/rank" ,
contestRankSchema ,
await api . get < unknown > ( endpoint , { params : query } ) ,
return api . get < { results : ContestRank [ ] ; total : number } > (
` contests/ ${ encodeURIComponent ( contestID ) } /rank ` ,
{ params : query } ,
)
}
@@ -368,31 +288,21 @@ export function updateProfile(data: { realName: string; mood: string }) {
return api . put < Profile > ( "me/profile" , data )
}
export async function getAnnouncementList ( offset = 0 , limit = 10 ) {
const endpoint = "announcements"
return contract (
"GET /announcements" ,
announcementListSchema ,
await api . get < unknown > ( endpoint , { params : { limit , offset } } ) ,
)
export function getAnnouncementList ( offset = 0 , limit = 10 ) {
return api . get < { results : AnnouncementListItem [ ] ; total : number } > ( "announcements" , {
params : { limit , offset } ,
} )
}
export async function getAnnouncement ( id : number ) {
const endpoint = ` announcements/ ${ id } `
return contract (
"GET /announcements/:id" ,
announcementSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getAnnouncement ( id : number ) {
return api . get < Announcement > ( ` announcements/ ${ id } ` )
}
export async function getMessageList ( offset = 0 , limit = 10 ) {
const endpoint = "messages"
return contract (
"GET /messages" ,
messageListSchema ,
await api . get < unknown > ( endpoint , { params : { limit , offset } } ) ,
)
export function getMessageList ( offset = 0 , limit = 10 ) {
// language 的收窄同 getSubmissions, 见那里的说明
return api . get < { results : Message [ ] ; total : number } > ( "messages" , {
params : { limit , offset } ,
} )
}
export function getReaction ( problemID : number ) {
@@ -403,125 +313,71 @@ export function setReaction(problemID: number, type: ReactionKey) {
return api . post < ReactionState > ( ` problems/ ${ problemID } /reaction ` , { type } )
}
export async function getMetrics ( userid : number ) {
const endpoint = ` users/ ${ userid } /metrics `
return contract (
"GET /users/:id/metrics" ,
metricsSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getMetrics ( userid : number ) {
return api . get < Metrics > ( ` users/ ${ userid } /metrics ` )
}
export async function getTutorial ( id : number ) {
const endpoint = ` tutorials/ ${ id } `
return contract (
"GET /tutorials/:id" ,
tutorialSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getTutorial ( id : number ) {
return api . get < Tutorial > ( ` tutorials/ ${ id } ` )
}
export async function getTutorials ( type : "python" | "c" ) {
const endpoint = "tutorials"
return contract (
"GET /tutorials" ,
tutorialSummarySchema . array ( ) ,
await api . get < unknown > ( endpoint , { params : { type } } ) ,
)
export function getTutorials ( type : "python" | "c" ) {
return api . get < TutorialSummary [ ] > ( "tutorials" , { params : { type } } )
}
export async function getAIDetailData (
start : string ,
end : string ,
username? : string ,
) {
const endpoint = "ai/detail"
return contract (
"GET /ai/detail" ,
aiDetailSchema ,
await api . get < unknown > ( endpoint , { params : { start , end , username } } ) ,
)
export function getAIDetailData ( start : string , end : string , username? : string ) {
return api . get < AiDetail > ( "ai/detail" , { params : { start , end , username } } )
}
export async function getAISolved (
export function getAISolved (
start : string ,
end : string ,
offset : number ,
limit : number ,
username? : string ,
) {
const endpoint = "ai/solved"
return contract (
"GET /ai/solved" ,
solvedListSchema ,
await api . get < unknown > ( endpoint , {
params : { start , end , offset , limit , username } ,
} ) ,
)
return api . get < SolvedList > ( "ai/solved" , {
params : { start , end , offset , limit , username } ,
} )
}
export async function getAIDurationData (
export function getAIDurationData (
end : string ,
duration : string ,
username? : string ,
) {
const endpoint = "ai/duration"
return contract (
"GET /ai/duration" ,
durationDataSchema . array ( ) ,
await api . get < unknown > ( endpoint , { params : { end , duration , username } } ) ,
)
return api . get < DurationData [ ] > ( "ai/duration" , {
params : { end , duration , username } ,
} )
}
export async function getAIHeatmapData ( username? : string ) {
const endpoint = "ai/heatmap"
return contract (
"GET /ai/heatmap" ,
heatmapItemSchema . array ( ) ,
await api . get < unknown > ( endpoint , {
params : username ? { username } : { } ,
} ) ,
)
export function getAIHeatmapData ( username? : string ) {
return api . get < HeatmapItem [ ] > ( "ai/heatmap" , {
params : username ? { username } : { } ,
} )
}
export async function getAILoginSummary() {
const endpoint = "ai/login-summary"
return contract (
"GET /ai/login-summary" ,
loginSummarySchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getAILoginSummary() {
return api . get < LoginSummary > ( "ai/login-summary" )
}
export async function getAIPinnedReport() {
const endpoint = "ai/pinned"
return contract (
"GET /ai/pinned" ,
aiAnalysisRecordSchema . nullable ( ) ,
await api . get < unknown > ( endpoint ) ,
)
export function getAIPinnedReport() {
return api . get < AiAnalysisRecord | null > ( "ai/pinned" )
}
// ==================== 相似题目推荐 ====================
export async function getSimilarProblems ( problemId : string ) {
const endpoint = ` problems/ ${ encodeURIComponent ( problemId ) } /similar `
const res = contract (
"GET /problems/:id/similar" ,
problemListItemSchema . array ( ) ,
await api . get < unknown > ( endpoint ) ,
)
return res . map ( filterResult )
export function getSimilarProblems ( problemId : string ) {
return api
. get < ProblemListItem [ ] > ( ` problems/ ${ encodeURIComponent ( problemId ) } /similar ` )
. then ( ( response ) = > response . map ( filterResult ) )
}
export type { YearlyAc as YearlyACData } from "@oj2/contract"
export async function getProblemYearlyAC ( problemId : string ) {
const endpoint = ` problems/ ${ encodeURIComponent ( problemId ) } /yearly-ac `
return contract (
"GET /problems/:id/yearly-ac" ,
yearlyAcSchema . array ( ) ,
await api . get < unknown > ( endpoint ) ,
export function getProblemYearlyAC ( problemId : string ) {
return api . get < YearlyAc [ ] > (
` problems/ ${ encodeURIComponent ( problemId ) } /yearly-ac ` ,
)
}
@@ -535,16 +391,11 @@ export function submitFlowchart(data: {
return api . post < CreateFlowchartResponse > ( "flowcharts" , data )
}
export async function getFlowchartSubmission ( id : string ) {
const endpoint = ` flowcharts/ ${ encodeURIComponent ( id ) } `
return contract (
"GET /flowcharts/:id" ,
flowchartSubmissionSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getFlowchartSubmission ( id : string ) {
return api . get < FlowchartSubmission > ( ` flowcharts/ ${ encodeURIComponent ( id ) } ` )
}
export async function getFlowchartSubmissions ( params : {
export function getFlowchartSubmissions ( params : {
username? : string
problemId? : string
myself? : string
@@ -553,27 +404,17 @@ export async function getFlowchartSubmissions(params: {
today? : string
grade? : string
} ) {
const endpoint = "flowcharts"
return contract (
"GET /flowcharts" ,
flowchartListSchema ,
await api . get < unknown > ( endpoint , { params } ) ,
)
return api . get < FlowchartList > ( "flowcharts" , { params } )
}
export async function getFlowchartStatistics (
export function getFlowchartStatistics (
duration : { start? : string ; end : string } ,
problemID? : string ,
username? : string ,
) {
const endpoint = "flowcharts/statistics"
return contract (
"GET /flowcharts/statistics" ,
flowchartStatisticsSchema ,
await api . get < unknown > ( endpoint , {
params : { . . . duration , problemId : problemID , username } ,
} ) ,
)
return api . get < FlowchartStatistics > ( "flowcharts/statistics" , {
params : { . . . duration , problemId : problemID , username } ,
} )
}
export function retryFlowchartSubmission ( submissionId : string ) {
@@ -582,59 +423,36 @@ export function retryFlowchartSubmission(submissionId: string) {
)
}
export async function getCurrentProblemFlowchartSubmission ( problemId : number ) {
const endpoint = ` problems/ ${ problemId } /flowchart/current `
return contract (
"GET /problems/:id/flowchart/current" ,
flowchartCurrentSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getCurrentProblemFlowchartSubmission ( problemId : number ) {
return api . get < FlowchartCurrent > ( ` problems/ ${ problemId } /flowchart/current ` )
}
export async function getFlowchartSubmissionDetail ( problemId : number , page = 0 ) {
const endpoint = ` problems/ ${ problemId } /flowchart/history `
return contract (
"GET /problems/:id/flowchart/history" ,
flowchartDetailSchema ,
await api . get < unknown > ( endpoint , { params : { page } } ) ,
)
export function getFlowchartSubmissionDetail ( problemId : number , page = 0 ) {
return api . get < FlowchartDetail > ( ` problems/ ${ problemId } /flowchart/history ` , {
params : { page } ,
} )
}
// ==================== 题单相关API ====================
export async function getProblemSetList (
export function getProblemSetList (
offset = 0 ,
limit = 10 ,
keyword = "" ,
difficulty = "" ,
status = "" ,
) {
const endpoint = "problem-sets"
return contract (
"GET /problem-sets" ,
problemSetListSchema ,
await api . get < unknown > ( endpoint , {
params : { offset , limit , keyword , difficulty , status } ,
} ) ,
)
return api . get < ProblemSetList > ( "problem-sets" , {
params : { offset , limit , keyword , difficulty , status } ,
} )
}
export async function getProblemSetDetail ( id : number ) {
const endpoint = ` problem-sets/ ${ id } `
return contract (
"GET /problem-sets/:id" ,
problemSetSchema ,
await api . get < unknown > ( endpoint ) ,
)
export function getProblemSetDetail ( id : number ) {
return api . get < ProblemSet > ( ` problem-sets/ ${ id } ` )
}
export async function getProblemSetProblems ( problemSetId : number ) {
const endpoint = ` problem-sets/ ${ problemSetId } /problems `
return contract (
"GET /problem-sets/:id/problems" ,
problemSetProblemSchema . array ( ) ,
await api . get < unknown > ( endpoint ) ,
)
export function getProblemSetProblems ( problemSetId : number ) {
return api . get < ProblemSetProblem [ ] > ( ` problem-sets/ ${ problemSetId } /problems ` )
}
export function joinProblemSet ( problemSetId : number ) {
@@ -653,25 +471,17 @@ export function updateProblemSetProgress(
} )
}
export async function getUserBadges ( username? : string ) {
const endpoint = ` users/ ${ encodeURIComponent ( username ? ? "me" ) } /badges `
return contract (
"GET /users/:username/badges" ,
userBadgeSchema . array ( ) ,
await api . get < unknown > ( endpoint ) ,
export function getUserBadges ( username? : string ) {
return api . get < UserBadge [ ] > (
` users/ ${ encodeURIComponent ( username ? ? "me" ) } /badges ` ,
)
}
export async function getProblemSetBadges ( problemSetId : number ) {
const endpoint = ` problem-sets/ ${ problemSetId } /badges `
return contract (
"GET /problem-sets/:id/badges" ,
problemSetBadgeSchema . array ( ) ,
await api . get < unknown > ( endpoint ) ,
)
export function getProblemSetBadges ( problemSetId : number ) {
return api . get < ProblemSetBadge [ ] > ( ` problem-sets/ ${ problemSetId } /badges ` )
}
export async function getProblemSetUserProgress (
export function getProblemSetUserProgress (
problemSetId : number ,
params ? : {
limit? : number
@@ -680,42 +490,14 @@ export async function getProblemSetUserProgress(
completionStatus ? : "" | "completed" | "in_progress" | "not_started"
} ,
) {
const endpoint = ` problem-sets/ ${ problemSetId } /user-progress `
return contract (
"GET /problem-sets/:id/user-progress" ,
problemSetProgressListSchema ,
await api . get < unknown > ( endpoint , { params } ) ,
return api . get < ProblemSetProgressList > (
` problem-sets/ ${ problemSetId } /user-progress ` ,
{ params } ,
)
}
export async function getExercises (
tutorialId : number ,
) : Promise < Exercise [ ] > {
const endpoint = ` tutorials/ ${ tutorialId } /exercises `
// 外层走 exerciseSchema, 内层 data 在这里按题型逐支校验:
// `z.infer` 只能把 data 还原成 Record<string, unknown>( superRefine 无法把
// 校验结果反映到推断类型上),所以那 7 个 Exercise*.vue 直接读
// data.question / data.options 时本没有任何运行时保护。
// 生产库 151 道练习题已确认七种题型的键集全部吻合。
const rows = contract (
"GET /tutorials/:id/exercises" ,
exerciseSchema . array ( ) ,
await api . get < unknown > ( endpoint ) ,
)
for ( const row of rows ) {
const shape = exerciseDataByType [ row . type ]
if ( ! shape ) continue
// 故意用同一个 contract():形状不符时它负责记日志并放行,不抛错
contract (
` GET /tutorials/:id/exercises( type= ${ row . type } 的 data) ` ,
shape ,
row . data ,
)
}
// 类型上仍要收窄一次:契约推断出的 data 是宽松 record, 组件要的是判别联合,
// 两者不重叠,所以只能经过 unknown。**这个断言是有意的,不是假校验** ——
// 上面那个循环已经在运行时按题型逐支验过;它只是把「运行时已确认」告诉 TS。
return rows as unknown as Exercise [ ]
export function getExercises ( tutorialId : number ) : Promise < Exercise [ ] > {
return api . get < Exercise [ ] > ( ` tutorials/ ${ tutorialId } /exercises ` )
}
/**
@@ -741,13 +523,8 @@ export function reportExerciseAttempt(
} ) . catch ( ( ) = > undefined )
}
export async function getLearnProgress ( type : "python" | "c" ) {
const endpoint = "learn/progress"
return contract (
"GET /learn/progress" ,
tutorialProgressSchema . array ( ) ,
await api . get < unknown > ( endpoint , { params : { type } } ) ,
)
export function getLearnProgress ( type : "python" | "c" ) {
return api . get < TutorialProgress [ ] > ( "learn/progress" , { params : { type } } )
}
/**