622 lines
18 KiB
TypeScript
622 lines
18 KiB
TypeScript
/**
|
|
* 学生相关API接口
|
|
*/
|
|
|
|
import { post, get } from '../utils/request'
|
|
import type { ApiResponse } from '../utils/request'
|
|
|
|
// ==================== 类型定义 ====================
|
|
|
|
// 学生信息类型
|
|
export type StudentInfo = {
|
|
id: string
|
|
name: string
|
|
account: string
|
|
avatar: string
|
|
grade: string
|
|
className: string
|
|
schoolName: string
|
|
}
|
|
|
|
// 首页当前学生基础信息
|
|
export type StudentHomeProfile = {
|
|
id: string
|
|
name: string
|
|
account: string
|
|
grade: string
|
|
school: string
|
|
className: string
|
|
avatar: string
|
|
comprehensiveLevel: 'excellent' | 'good' | 'qualified' | 'needs_improvement'
|
|
}
|
|
|
|
// 家长已绑定学生的选择列表项
|
|
export type BoundStudentSummary = {
|
|
id: string
|
|
name: string
|
|
account: string
|
|
grade: string
|
|
school: string
|
|
className: string
|
|
avatar: string
|
|
}
|
|
|
|
// 首页今日学习概览,所有数值均归属于 studentId 对应的学生。
|
|
export type StudentHomeMetrics = {
|
|
studentId: string
|
|
onlineToday: boolean
|
|
checkedInToday: boolean
|
|
learningMinutesToday: number
|
|
accuracyToday: number | null
|
|
learningDaysThisWeek: number
|
|
shouldStudyDaysThisWeek: number
|
|
pendingHomeworkCount: number
|
|
updatedAt: string
|
|
}
|
|
|
|
export type TodayLearningCourse = {
|
|
id: string
|
|
time: string
|
|
subject: string
|
|
content: string
|
|
checkedIn: boolean
|
|
}
|
|
|
|
export type TodayLearningScore = {
|
|
accuracy: number
|
|
yesterdayAccuracy: number
|
|
change: number
|
|
}
|
|
|
|
export type TodayLearningPoint = {
|
|
id: string
|
|
label: string
|
|
points: number
|
|
}
|
|
|
|
export type StudyReportSubject = {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
export type StudyReportTrendPoint = {
|
|
label: string
|
|
value: number
|
|
average: number
|
|
valueHeight: number
|
|
averageHeight: number
|
|
}
|
|
|
|
export type StudyReportTrend = {
|
|
subjectId: string
|
|
points: StudyReportTrendPoint[]
|
|
}
|
|
|
|
export type StudyReportActivityPoint = {
|
|
label: string
|
|
studyMinutes: number
|
|
plannedMinutes: number
|
|
newMasteredCount: number
|
|
studyHeight: number
|
|
plannedHeight: number
|
|
newMasteredHeight: number
|
|
}
|
|
|
|
export type StudyReportActivity = {
|
|
summary: string
|
|
activeDays: number
|
|
totalDurationText: string
|
|
points: StudyReportActivityPoint[]
|
|
}
|
|
|
|
export type StudyReportSubjectMastery = {
|
|
subjectName: string
|
|
masteredCount: number
|
|
totalCount: number
|
|
masteredPercent: number
|
|
}
|
|
|
|
export type StudyReportPeriod = {
|
|
rangeText: string
|
|
statusTitle: string
|
|
statusDescription: string
|
|
newMasteredPoints: number
|
|
consecutiveDays: number
|
|
averageAccuracy: number
|
|
scaleLabels: string[]
|
|
trends: StudyReportTrend[]
|
|
activity: StudyReportActivity
|
|
subjectMasteries: StudyReportSubjectMastery[]
|
|
}
|
|
|
|
export type StudentStudyReport = {
|
|
studentId: string
|
|
studentName: string
|
|
studentGrade: string
|
|
studentClass: string
|
|
updatedAtText: string
|
|
subjects: StudyReportSubject[]
|
|
week: StudyReportPeriod
|
|
month: StudyReportPeriod
|
|
}
|
|
|
|
export type TodayLearningDetail = {
|
|
studentId: string
|
|
studentName: string
|
|
dateText: string
|
|
checkedIn: boolean
|
|
learningMinutes: number
|
|
answerMinutes: number
|
|
answerCount: number
|
|
completedPackages: number
|
|
totalPackages: number
|
|
masteredPoints: number
|
|
courses: TodayLearningCourse[]
|
|
score: TodayLearningScore
|
|
pointsTotal: number
|
|
pointDetails: TodayLearningPoint[]
|
|
}
|
|
|
|
export type VocabularyBook = {
|
|
id: string
|
|
name: string
|
|
cover: string
|
|
subtitle: string
|
|
totalWords: number
|
|
learnedWords: number
|
|
status: 'learning' | 'unfinished' | 'completed' | 'not_started'
|
|
statusText: string
|
|
levelText: string
|
|
color: 'green' | 'yellow' | 'blue' | 'gray'
|
|
}
|
|
|
|
export type StudentVocabulary = {
|
|
studentId: string
|
|
studentName: string
|
|
studentGrade: string
|
|
studentClass: string
|
|
bookCount: number
|
|
books: VocabularyBook[]
|
|
}
|
|
|
|
export type GrowthPointRecord = {
|
|
id: string
|
|
title: string
|
|
timeText: string
|
|
points: number
|
|
icon: 'calendar' | 'assignment' | 'streak' | 'accuracy' | 'mastery' | 'note'
|
|
}
|
|
|
|
export type StudentGrowth = {
|
|
studentId: string
|
|
studentName: string
|
|
studentGrade: string
|
|
studentClass: string
|
|
pointsBalance: number
|
|
level: number
|
|
rank: number
|
|
streakDays: number
|
|
totalPoints: number
|
|
records: GrowthPointRecord[]
|
|
}
|
|
|
|
export type StudentPointRecordsPage = {
|
|
records: GrowthPointRecord[]
|
|
total: number
|
|
pageIndex: number
|
|
pageSize: number
|
|
lastId: number | null
|
|
}
|
|
|
|
export type LearningTaskItem = {
|
|
id: string
|
|
title: string
|
|
status: 'not_started' | 'in_progress' | 'success' | 'failed'
|
|
statusText: string
|
|
accuracyText: string
|
|
answerText: string
|
|
deadlineText: string
|
|
}
|
|
|
|
export type LearningTaskGroup = {
|
|
id: string
|
|
subject: string
|
|
unit: string
|
|
lesson: string
|
|
startText: string
|
|
endText: string
|
|
knowledgeCount: number
|
|
note: string
|
|
tasks: LearningTaskItem[]
|
|
}
|
|
|
|
export type StudentLearningTasks = {
|
|
studentId: string
|
|
studentName: string
|
|
studentGrade: string
|
|
studentClass: string
|
|
pendingCount: number
|
|
completedCount: number
|
|
failedCount: number
|
|
groups: LearningTaskGroup[]
|
|
}
|
|
|
|
export type ParentStudyPackStatus = 0 | 1 | 2 | 3
|
|
|
|
export type StudentAlertLevel = 'critical' | 'high' | 'medium' | 'low'
|
|
|
|
export type StudentAlert = {
|
|
id: string
|
|
level: StudentAlertLevel
|
|
title: string
|
|
content: string
|
|
occurredAt: string
|
|
isRead: boolean
|
|
statusName?: string
|
|
}
|
|
|
|
export type StudentAlertList = {
|
|
list: StudentAlert[]
|
|
pagination: {
|
|
page: number
|
|
pageSize: number
|
|
total: number
|
|
hasMore: boolean
|
|
}
|
|
}
|
|
|
|
export type StudentLearningWarningItem = {
|
|
type: number
|
|
title: string
|
|
description: string
|
|
isTriggered: boolean
|
|
statusName: string
|
|
}
|
|
|
|
export type StudentLearningWarningResult = {
|
|
userId: string
|
|
date: string
|
|
totalCount: number
|
|
triggeredCount: number
|
|
items: StudentLearningWarningItem[]
|
|
}
|
|
|
|
const learningWarningTypeMap: { [key: number]: { title: string, level: StudentAlertLevel } } = {
|
|
1: { title: '今日未打卡', level: 'high' },
|
|
2: { title: '仅学无交', level: 'medium' },
|
|
3: { title: '作业未完成', level: 'high' },
|
|
4: { title: '攻克失败', level: 'critical' },
|
|
5: { title: '连续 3 天未学习', level: 'high' },
|
|
6: { title: '正确率明显下滑', level: 'medium' }
|
|
}
|
|
|
|
// 验证学生二维码请求参数
|
|
export type VerifyQRCodeParams = {
|
|
qrCode: string
|
|
account: string
|
|
name: string
|
|
}
|
|
|
|
// 验证学生账号请求参数
|
|
export type VerifyAccountParams = {
|
|
account: string
|
|
name: string
|
|
}
|
|
|
|
// ==================== API方法 ====================
|
|
|
|
/**
|
|
* 验证学生二维码
|
|
*/
|
|
export function apiVerifyStudentQRCode(params: VerifyQRCodeParams): Promise<ApiResponse<StudentInfo> | null> {
|
|
return apiVerifyStudentAccount({ account: params.account, name: params.name })
|
|
}
|
|
|
|
/**
|
|
* 验证学生账号
|
|
*/
|
|
export function apiVerifyStudentAccount(params: VerifyAccountParams): Promise<ApiResponse<StudentInfo> | null> {
|
|
return get<any>('/api/v1/Parents/VerifyStudent', { RealName: params.name, Account: params.account }).then((res) => {
|
|
if (!res || !res.data) return null
|
|
return { code: res.code, message: res.message, data: { id: String(res.data.userId), name: res.data.name, account: res.data.account, avatar: res.data.avatar || '', grade: res.data.gradeLevel || '', className: res.data.className || '', schoolName: res.data.schoolName || '' } }
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 检查手机号是否已注册
|
|
*/
|
|
export function apiCheckPhone(phone: string): Promise<ApiResponse<{ exists: boolean }> | null> {
|
|
return get<{ exists: boolean }>(`/api/parent/check-phone?phone=${phone}`)
|
|
}
|
|
|
|
/**
|
|
* 获取当前家长默认绑定学生的首页基础信息。
|
|
* 服务端根据 Authorization Token 识别家长并校验学生绑定关系。
|
|
*/
|
|
export function apiGetStudentHomeProfile(studentId?: string, showLoading: boolean = true): Promise<ApiResponse<StudentHomeProfile> | null> {
|
|
const url = studentId ? `/api/parent/home-profile?studentId=${studentId}` : '/api/parent/home-profile'
|
|
return get<StudentHomeProfile>(url, undefined, { showLoading })
|
|
}
|
|
|
|
/**
|
|
* 获取当前家长绑定的全部学生,用于首页切换学生。
|
|
*/
|
|
export function apiGetBoundStudents(): Promise<ApiResponse<{ students: BoundStudentSummary[], currentStudentId: string | null }> | null> {
|
|
return get<any>('/api/v1/Parents/GetBoundStudents').then((res) => {
|
|
if (!res || !res.data) return null
|
|
const items = Array.isArray(res.data) ? res.data : res.data.students
|
|
if (!Array.isArray(items)) return null
|
|
return {
|
|
code: res.code,
|
|
message: res.message,
|
|
data: {
|
|
students: items.map((item: any) => ({
|
|
id: String(item.userId),
|
|
name: item.name || '',
|
|
account: item.account || '',
|
|
grade: item.gradeLevel || '',
|
|
school: item.schoolName || '',
|
|
className: item.className || '',
|
|
avatar: item.avatar || ''
|
|
})),
|
|
currentStudentId: null
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取指定学生的首页今日学习概览。
|
|
*/
|
|
export function apiGetStudentHomeMetrics(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentHomeMetrics> | null> {
|
|
return get<any>(`/api/v1/ParentStudentToday/GetTodayInfo?userId=${studentId}`, undefined, { showLoading }).then((res) => {
|
|
if (!res || !res.data) return null
|
|
return {
|
|
code: res.code,
|
|
message: res.message,
|
|
data: {
|
|
studentId: String(res.data.userId ?? studentId),
|
|
onlineToday: Boolean(res.data.todayOnline),
|
|
checkedInToday: Boolean(res.data.todayCheckIn),
|
|
learningMinutesToday: Math.floor((Number(res.data.todayStudyDuration) || 0) / 60),
|
|
accuracyToday: Number(res.data.todayAccuracyRate) || 0,
|
|
learningDaysThisWeek: Number(res.data.weekStudyDays) || 0,
|
|
shouldStudyDaysThisWeek: Number(res.data.weekShouldStudyDays) || 0,
|
|
pendingHomeworkCount: Number(res.data.pendingPackCount) || 0,
|
|
updatedAt: res.data.date || ''
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取指定学生今日学习预警,可选择仅返回已触发的预警规则。
|
|
*/
|
|
export function apiGetStudentLearningWarnings(studentId: string, showLoading: boolean = true, onlyTriggered: boolean = true): Promise<ApiResponse<StudentAlertList> | null> {
|
|
return get<any>(`/api/v1/ParentLearningWarning/GetTodayList?userId=${studentId}`, undefined, { showLoading }).then((res) => {
|
|
if (!res || !res.data) return null
|
|
const items = Array.isArray(res.data.items) ? res.data.items : []
|
|
const list: StudentAlert[] = items
|
|
.filter((item: any) => !onlyTriggered || Boolean(item.isTriggered))
|
|
.map((item: any, index: number) => {
|
|
const warningType = learningWarningTypeMap[Number(item.type)]
|
|
return {
|
|
id: `${studentId}-${item.type ?? index}`,
|
|
level: warningType?.level ?? 'medium',
|
|
title: item.title || warningType?.title || '学习预警',
|
|
content: item.description || item.statusName || '',
|
|
occurredAt: res.data.date || '',
|
|
isRead: !item.isTriggered,
|
|
statusName: item.statusName || (item.isTriggered ? '已触发' : '正常')
|
|
}
|
|
})
|
|
return {
|
|
code: res.code,
|
|
message: res.message,
|
|
data: {
|
|
list,
|
|
pagination: {
|
|
page: 1,
|
|
pageSize: list.length,
|
|
total: list.length,
|
|
hasMore: false
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取学生今日学习详情及课程打卡记录。
|
|
*/
|
|
export function apiGetTodayLearningDetail(studentId: string, showLoading: boolean = true): Promise<ApiResponse<TodayLearningDetail> | null> {
|
|
return get<TodayLearningDetail>(`/api/parent/today-learning?studentId=${studentId}`, undefined, { showLoading })
|
|
}
|
|
|
|
/**
|
|
* 获取学生学情报告。接口一次返回周报、月报和全部科目趋势数据。
|
|
*/
|
|
export function apiGetStudentStudyReport(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentStudyReport> | null> {
|
|
return get<StudentStudyReport>(`/api/parent/study-report?studentId=${studentId}`, undefined, { showLoading })
|
|
}
|
|
|
|
export function apiGetStudentVocabulary(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentVocabulary> | null> {
|
|
return get<any>(`/api/v1/Parents/GetAllLexiconBooks/${studentId}`, undefined, { showLoading }).then((res) => {
|
|
if (!res || !Array.isArray(res.data)) return null
|
|
const books: VocabularyBook[] = res.data.map((item: any) => {
|
|
const learnedWords = Number(item.lexiconCount) || 0
|
|
const totalWords = Number(item.totalCount) || learnedWords
|
|
const status: VocabularyBook['status'] = totalWords > 0 && learnedWords >= totalWords
|
|
? 'completed'
|
|
: item.isStudying
|
|
? 'learning'
|
|
: learnedWords > 0
|
|
? 'unfinished'
|
|
: 'not_started'
|
|
return {
|
|
id: String(item.id ?? item.title ?? ''),
|
|
name: item.title || '',
|
|
cover: item.cover || '',
|
|
subtitle: `总词量 ${totalWords} 词`,
|
|
totalWords,
|
|
learnedWords,
|
|
status,
|
|
statusText: status === 'completed' ? '已完成' : status === 'learning' ? '学习中' : status === 'unfinished' ? '未学完' : '未开始',
|
|
levelText: item.slogan || '',
|
|
color: status === 'learning' ? 'green' : status === 'unfinished' ? 'yellow' : status === 'completed' ? 'blue' : 'gray'
|
|
}
|
|
})
|
|
return {
|
|
code: res.code,
|
|
message: res.message,
|
|
data: { studentId, studentName: '', studentGrade: '', studentClass: '', bookCount: books.length, books }
|
|
}
|
|
})
|
|
}
|
|
|
|
export function apiGetStudentGrowth(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentGrowth> | null> {
|
|
return get<any>(`/api/v1/StudentPoints/GetStudentPointSummary/${studentId}`, undefined, { showLoading }).then((res) => {
|
|
if (!res || !res.data) return null
|
|
return {
|
|
code: res.code,
|
|
message: res.message,
|
|
data: {
|
|
studentId: String(res.data.userId ?? studentId),
|
|
studentName: '',
|
|
studentGrade: '',
|
|
studentClass: '',
|
|
pointsBalance: Number(res.data.totalPoints) || 0,
|
|
level: Number(res.data.level) || 0,
|
|
rank: Number(res.data.classRank) || 0,
|
|
streakDays: Number(res.data.noPointDays) || 0,
|
|
totalPoints: Number(res.data.todayPoints) || 0,
|
|
records: []
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 分页获取学生积分流水。
|
|
*/
|
|
export function apiGetStudentPointRecords(studentId: string, pageIndex: number = 1, pageSize: number = 20, prevPageId?: number, showLoading: boolean = true): Promise<ApiResponse<StudentPointRecordsPage> | null> {
|
|
const prevPageQuery = prevPageId === undefined || prevPageId === null ? '' : `&PrevPageId=${prevPageId}`
|
|
return get<any>(`/api/v1/StudentPoints/GetStudentPointRecords?UserId=${studentId}&PageIndex=${pageIndex}&PageSize=${pageSize}${prevPageQuery}`, undefined, { showLoading }).then((res) => {
|
|
if (!res || !res.data) return null
|
|
const page = res.data
|
|
const records: GrowthPointRecord[] = (Array.isArray(page.data) ? page.data : []).map((item: any) => ({
|
|
id: String(item.id),
|
|
title: item.pointMessage || '积分变动',
|
|
timeText: formatPointRecordTime(item.bizTime),
|
|
points: Number(item.points) || 0,
|
|
icon: pointRecordIcon(Number(item.type))
|
|
}))
|
|
return {
|
|
code: res.code,
|
|
message: res.message,
|
|
data: {
|
|
records,
|
|
total: Number(page.total) || 0,
|
|
pageIndex: Number(page.pageIndex) || pageIndex,
|
|
pageSize: Number(page.pageSize) || pageSize,
|
|
lastId: page.id === null || page.id === undefined ? null : Number(page.id)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function pointRecordIcon(type: number): GrowthPointRecord['icon'] {
|
|
if (type === 1) return 'calendar'
|
|
if (type === 2 || type === 3) return 'assignment'
|
|
if (type === 4) return 'streak'
|
|
if (type === 5 || type === 6) return 'accuracy'
|
|
if (type === 7 || type === 8) return 'mastery'
|
|
return 'note'
|
|
}
|
|
|
|
function formatPointRecordTime(value: any): string {
|
|
if (!value) return '--'
|
|
const text = String(value)
|
|
return text.length >= 16 ? text.substring(5, 16).replace('T', ' ') : text
|
|
}
|
|
|
|
export function apiGetStudentLearningTasks(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentLearningTasks> | null> {
|
|
return apiGetParentStudyPacks(studentId, 0, showLoading)
|
|
}
|
|
|
|
/**
|
|
* 获取学生当天学习包任务。Status: 0 全部,1 待完成,2 已完成,3 未通过。
|
|
*/
|
|
export function apiGetParentStudyPacks(studentId: string, status: ParentStudyPackStatus = 0, showLoading: boolean = true): Promise<ApiResponse<StudentLearningTasks> | null> {
|
|
return get<any>(`/api/v1/ParentStudyPack/GetTodayList?UserId=${studentId}&Status=${status}`, undefined, { showLoading }).then((res) => {
|
|
if (!res || !res.data) return null
|
|
const statusMap: { [key: number]: LearningTaskItem['status'] } = {
|
|
0: 'not_started',
|
|
1: 'in_progress',
|
|
2: 'success',
|
|
3: 'failed'
|
|
}
|
|
const statusTextMap: { [key: string]: string } = {
|
|
not_started: '待完成',
|
|
in_progress: '进行中',
|
|
success: '已完成',
|
|
failed: '未通过'
|
|
}
|
|
const courses = Array.isArray(res.data.courses) ? res.data.courses : []
|
|
const groups: LearningTaskGroup[] = courses.map((course: any) => {
|
|
const tasks: LearningTaskItem[] = (Array.isArray(course.packs) ? course.packs : []).map((pack: any) => {
|
|
const taskStatus = statusMap[Number(pack.status)] || 'not_started'
|
|
const accuracy = pack.latestAccuracyRate === null || pack.latestAccuracyRate === undefined ? '--' : `${Number(pack.latestAccuracyRate) || 0}%`
|
|
return {
|
|
id: String(pack.packId ?? pack.knowledgePointId ?? ''),
|
|
title: pack.name || '未命名学习任务',
|
|
status: taskStatus,
|
|
statusText: pack.statusName || statusTextMap[taskStatus],
|
|
accuracyText: accuracy,
|
|
answerText: pack.completedAttemptCount ? `已完成 ${pack.completedAttemptCount} 次` : '尚未完成',
|
|
deadlineText: pack.deadlineTime || ''
|
|
}
|
|
})
|
|
return {
|
|
id: String(course.courseId ?? course.sectionCount ?? ''),
|
|
subject: course.baseSubjectName || '',
|
|
unit: course.studyStageName || '',
|
|
lesson: course.courseName || course.sectionName || '',
|
|
startText: formatTaskTime(course.planStartTime),
|
|
endText: formatTaskTime(course.planEndTime),
|
|
knowledgeCount: Number(course.knowledgePointCount) || tasks.length,
|
|
note: course.deadlineTime ? `截止时间:${formatTaskTime(course.deadlineTime)}` : '',
|
|
tasks
|
|
}
|
|
})
|
|
return {
|
|
code: res.code,
|
|
message: res.message,
|
|
data: {
|
|
studentId: String(res.data.userId ?? studentId),
|
|
studentName: res.data.userName || '',
|
|
studentGrade: '',
|
|
studentClass: '',
|
|
pendingCount: Number(res.data.pendingCount) || 0,
|
|
completedCount: Number(res.data.completedCount) || 0,
|
|
failedCount: Number(res.data.failedCount) || 0,
|
|
groups
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function formatTaskTime(value: any): string {
|
|
if (!value) return '--'
|
|
const text = String(value)
|
|
return text.length >= 16 ? text.substring(11, 16) : text
|
|
}
|
|
|
|
/**
|
|
* 获取当前学生的预警信息。首页调用时 limit 固定为 3。
|
|
*/
|
|
export function apiGetStudentAlerts(studentId: string, page: number = 1, pageSize: number = 20, limit?: number, showLoading: boolean = true): Promise<ApiResponse<StudentAlertList> | null> {
|
|
const suffix = limit ? `limit=${limit}` : `page=${page}&pageSize=${pageSize}`
|
|
return get<StudentAlertList>(`/api/parent/alerts?studentId=${studentId}&${suffix}`, undefined, { showLoading })
|
|
}
|