/** * 学生相关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 pendingPackCount: number updatedAt: string } export type ParentStudentTodayInfoResult = { userId: number | string date: string todayOnline: boolean todayCheckIn: boolean todayStudyDuration: number todayAccuracyRate: number weekStudyDays: number weekShouldStudyDays: number pendingPackCount: number } export type TodayLearningCourse = { id: string time: string subject: string content: string stageName: string checkedIn: boolean statusText: string } export type TodayLearningScore = { accuracy: number | null yesterdayAccuracy: number | null change: number | null hasYesterdayData: boolean } export type TodayLearningPoint = { id: string label: string points: number } /** 接口原始结构:今日学习概览 */ export type ParentTodayLearningOverview = { StudyDurationMinute: number AnswerDurationMinute: number AnswerQuestionCount: number CompletedPackCount: number TotalPackCount: number OvercomeKnowledgePointCount: number IsCheckedIn: boolean CheckInStatusName: string } /** 接口原始结构:今日课节与打卡 */ export type ParentTodayCourseItem = { CourseId: string BaseSubjectId: string BaseSubjectName: string CourseName: string StudyStage: number StudyStageName: string PlanStartTime: string PlanEndTime: string TimeText: string IsCheckedIn: boolean CheckInStatusName: string } /** 接口原始结构:正确率对比 */ export type ParentTodayAccuracyComparison = { TodayAccuracyRate: number | null YesterdayAccuracyRate: number | null AccuracyRateDiff: number | null HasYesterdayData: boolean } /** 接口原始结构:积分明细 */ export type ParentTodayPointItem = { Type: number Name: string Points: number } /** 接口原始结构:今日获得积分 */ export type ParentTodayPointResult = { TotalPoints: number Items: ParentTodayPointItem[] } /** 接口原始结构:今日学习情况 */ export type ParentTodayLearningSituationResult = { UserId: string Date: string Overview: ParentTodayLearningOverview Courses: ParentTodayCourseItem[] Accuracy: ParentTodayAccuracyComparison Points: ParentTodayPointResult } 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[] hasWeekReport: boolean hasMonthReport: boolean week: StudyReportPeriod month: StudyReportPeriod } export type ParentLearningReportType = 1 | 2 export type ParentLearningDurationDetail = { StartDate: string EndDate: string StudyDurationTime: number OvercomeStudyDurationTime: number UnOvercomeStudyDurationTime: number } export type ParentLearningReportDetailItem = { SubjectId: string SubjectName: string | null NewKnowledgePointCount: number OvercomeKnowledgePointCount: number ContinuousStudyDays: number AverageAccuracy: number StudyDurationTime: number OvercomeStudyDurationTime: number UnOvercomeStudyDurationTime: number StudyDurationDetails: ParentLearningDurationDetail[] } export type ParentLearningReportResult = { UserId: string UserName: string | null ClassId: string ClassName: string | null SchoolId: string SchoolName: string | null CloudSchoolId: string CloudSchoolName: string | null ReportType: ParentLearningReportType ReportTypeName: string HasReport: boolean PeriodStart: string | null PeriodEnd: string | null Summary: ParentLearningReportDetailItem | null Subjects: ParentLearningReportDetailItem[] } export type TodayLearningDetail = { studentId: string studentName: string dateText: string checkedIn: boolean checkInStatusName: string 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 resultText: string accuracyText: string answerText: string attemptText: string deadlineText: string metaText: 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 totalCount: number 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 isTriggered: 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: number | 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 | null> { return apiVerifyStudentAccount({ account: params.account, name: params.name }) } /** * 验证学生账号 */ export function apiVerifyStudentAccount(params: VerifyAccountParams): Promise | null> { return get('/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 | null> { return get<{ exists: boolean }>(`/api/parent/check-phone?phone=${phone}`) } /** * 获取当前家长默认绑定学生的首页基础信息。 * 服务端根据 Authorization Token 识别家长并校验学生绑定关系。 */ export function apiGetStudentHomeProfile(studentId?: string, showLoading: boolean = true): Promise | null> { const url = studentId ? `/api/parent/home-profile?studentId=${studentId}` : '/api/parent/home-profile' return get(url, undefined, { showLoading }) } /** * 获取当前家长绑定的全部学生,用于首页切换学生。 */ export function apiGetBoundStudents(): Promise | null> { return get('/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 | null> { return get(`/api/v1/ParentStudentToday/GetTodayInfo?userId=${encodeURIComponent(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), accuracyToday: res.data.todayAccuracyRate === null || res.data.todayAccuracyRate === undefined ? null : Number(res.data.todayAccuracyRate) || 0, learningDaysThisWeek: Number(res.data.weekStudyDays) || 0, shouldStudyDaysThisWeek: Number(res.data.weekShouldStudyDays) || 0, pendingPackCount: Number(res.data.pendingPackCount) || 0, updatedAt: res.data.date || '' } } }) } /** * 获取指定学生今日学习预警。已触发预警优先,其后展示未触发规则,可按数量裁剪。 */ export function apiGetStudentLearningWarnings(studentId: string, showLoading: boolean = true, limit?: number, onlyTriggered: boolean = false): Promise | null> { return get(`/api/v1/ParentLearningWarning/GetTodayList?userId=${encodeURIComponent(studentId)}`, undefined, { showLoading }).then((res) => { if (!res || !res.data || !Array.isArray(res.data.items)) return null const warningData = res.data const triggeredItems = warningData.items.filter((item: StudentLearningWarningItem) => Boolean(item.isTriggered)) const normalItems = warningData.items.filter((item: StudentLearningWarningItem) => !Boolean(item.isTriggered)) const orderedItems = triggeredItems.concat(normalItems) const sourceItems = onlyTriggered ? triggeredItems : orderedItems const visibleItems = limit === undefined ? sourceItems : sourceItems.slice(0, limit) const list: StudentAlert[] = visibleItems.map((item: StudentLearningWarningItem, index: number) => { const warningType = learningWarningTypeMap[Number(item.type)] return { id: `${studentId}-${item.type ?? index}`, level: item.isTriggered ? warningType?.level ?? 'high' : 'low', title: item.title || warningType?.title || '学习预警', content: item.description || item.statusName || '', occurredAt: warningData.date || '', isRead: !item.isTriggered, isTriggered: Boolean(item.isTriggered), statusName: item.statusName || (item.isTriggered ? '已触发' : '正常') } }) return { code: res.code, message: res.message, data: { list, pagination: { page: 1, pageSize: list.length, total: sourceItems.length, hasMore: limit !== undefined && sourceItems.length > list.length } } } }) } /** * 获取学生今日学习情况。 * 接口字段为 PascalCase,这里统一转换为页面使用的驼峰结构。 */ export function apiGetTodayLearningDetail(studentId: string, showLoading: boolean = true): Promise | null> { return get(`/api/v1/ParentLearningSituation/GetToday?userId=${encodeURIComponent(studentId)}`, undefined, { showLoading }).then((res) => { if (!res || !res.data) return null const data = res.data const overview = data.Overview const accuracy = data.Accuracy const points = data.Points const courses: TodayLearningCourse[] = (Array.isArray(data.Courses) ? data.Courses : []).map((course: ParentTodayCourseItem) => { const checkedIn = Boolean(course.IsCheckedIn) return { id: String(course.CourseId ?? ''), time: course.TimeText || formatTodayLearningTime(course.PlanStartTime), subject: course.BaseSubjectName || '', content: course.CourseName || '', stageName: course.StudyStageName || '', checkedIn, statusText: course.CheckInStatusName || (checkedIn ? '已打卡' : '未打卡') } }) const pointDetails: TodayLearningPoint[] = (Array.isArray(points?.Items) ? points.Items : []).map((item: ParentTodayPointItem) => ({ id: String(item.Type ?? ''), label: item.Name || '其他积分', points: Number(item.Points) || 0 })) return { code: res.code, message: res.message, data: { studentId: String(data.UserId ?? studentId), studentName: '', dateText: formatTodayLearningDateText(data.Date), checkedIn: Boolean(overview?.IsCheckedIn), checkInStatusName: overview?.CheckInStatusName || (overview?.IsCheckedIn ? '已打卡' : '未打卡'), learningMinutes: Number(overview?.StudyDurationMinute) || 0, answerMinutes: Number(overview?.AnswerDurationMinute) || 0, answerCount: Number(overview?.AnswerQuestionCount) || 0, completedPackages: Number(overview?.CompletedPackCount) || 0, totalPackages: Number(overview?.TotalPackCount) || 0, masteredPoints: Number(overview?.OvercomeKnowledgePointCount) || 0, courses, score: { accuracy: toNullableNumber(accuracy?.TodayAccuracyRate), yesterdayAccuracy: toNullableNumber(accuracy?.YesterdayAccuracyRate), change: toNullableNumber(accuracy?.AccuracyRateDiff), hasYesterdayData: Boolean(accuracy?.HasYesterdayData) }, pointsTotal: Number(points?.TotalPoints) || 0, pointDetails } } }) } function toNullableNumber(value: number | null | undefined): number | null { if (value === null || value === undefined) return null return Number(value) } function formatTodayLearningTime(value: string | null | undefined): string { if (!value) return '--' const text = String(value) return text.length >= 16 ? text.substring(11, 16) : text } function formatTodayLearningDateText(value: string | null | undefined): string { if (!value) return '' const text = String(value) if (text.length < 10) return text const year = Number(text.substring(0, 4)) const month = Number(text.substring(5, 7)) const day = Number(text.substring(8, 10)) return `${month}月${day}日 ${weekdayName(year, month, day)}` } function weekdayName(year: number, month: number, day: number): string { const offsets = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4] const adjustedYear = month < 3 ? year - 1 : year const index = (adjustedYear + Math.floor(adjustedYear / 4) - Math.floor(adjustedYear / 100) + Math.floor(adjustedYear / 400) + offsets[month - 1] + day) % 7 return ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][index] } /** * 获取学生最新一期学情报告。新接口按报告类型分别返回周报、月报,这里转换为页面统一结构。 */ export async function apiGetStudentStudyReport(studentId: string, showLoading: boolean = true): Promise | null> { const weekRes = await get(`/api/v1/ParentLearningReport/Get?userId=${encodeURIComponent(studentId)}&reportType=1`, undefined, { showLoading }) const monthRes = await get(`/api/v1/ParentLearningReport/Get?userId=${encodeURIComponent(studentId)}&reportType=2`, undefined, { showLoading: false }) const weekData = weekRes?.data ?? null const monthData = monthRes?.data ?? null if (!weekData && !monthData) return null const sourceData = weekData ?? monthData return { code: weekRes?.code ?? monthRes?.code ?? 200, message: weekRes?.message ?? monthRes?.message ?? 'SUCCESS', data: { studentId: String(sourceData?.UserId ?? studentId), studentName: sourceData?.UserName ?? '', studentGrade: '', studentClass: sourceData?.ClassName ?? '', updatedAtText: formatReportDate(sourceData?.PeriodEnd), subjects: buildStudyReportSubjects(weekData, monthData), hasWeekReport: Boolean(weekData?.HasReport && weekData.Summary), hasMonthReport: Boolean(monthData?.HasReport && monthData.Summary), week: buildStudyReportPeriod(weekData, 1), month: buildStudyReportPeriod(monthData, 2) } } } function buildStudyReportSubjects(weekData: ParentLearningReportResult | null, monthData: ParentLearningReportResult | null): StudyReportSubject[] { const subjects: StudyReportSubject[] = [{ id: '0', name: '全部科目' }] const sourceSubjects = weekData?.Subjects?.length ? weekData.Subjects : monthData?.Subjects ?? [] sourceSubjects.forEach((item: ParentLearningReportDetailItem) => { const id = String(item.SubjectId ?? '') if (id && subjects.every((subject: StudyReportSubject) => subject.id !== id)) { subjects.push({ id, name: item.SubjectName ?? '未命名科目' }) } }) return subjects } function buildStudyReportPeriod(report: ParentLearningReportResult | null, reportType: ParentLearningReportType): StudyReportPeriod { if (!report?.HasReport || !report.Summary) return createEmptyStudyReportPeriod(reportType) const summary = report.Summary const detailItems = [summary].concat(Array.isArray(report.Subjects) ? report.Subjects : []) let maxMinutes = 1 detailItems.forEach((item: ParentLearningReportDetailItem) => { const details = Array.isArray(item.StudyDurationDetails) ? item.StudyDurationDetails : [] details.forEach((detail: ParentLearningDurationDetail) => { maxMinutes = Math.max(maxMinutes, secondsToMinutes(detail.StudyDurationTime)) }) }) return { rangeText: formatReportRange(report.PeriodStart, report.PeriodEnd), statusTitle: `${report.ReportTypeName || (reportType === 1 ? '周报' : '月报')}已攻克 ${summary.OvercomeKnowledgePointCount || 0} 个知识点`, statusDescription: `新增 ${summary.NewKnowledgePointCount || 0} 个知识点,累计学习 ${formatSecondsDuration(summary.StudyDurationTime)},其中攻克成功学习 ${formatSecondsDuration(summary.OvercomeStudyDurationTime)}。`, newMasteredPoints: Number(summary.OvercomeKnowledgePointCount) || 0, consecutiveDays: Number(summary.ContinuousStudyDays) || 0, averageAccuracy: Number(summary.AverageAccuracy) || 0, scaleLabels: buildDurationScaleLabels(maxMinutes), trends: detailItems.map((item: ParentLearningReportDetailItem) => buildStudyReportTrend(item, maxMinutes, reportType)), activity: buildStudyReportActivity(summary, maxMinutes, reportType), subjectMasteries: (Array.isArray(report.Subjects) ? report.Subjects : []).map((item: ParentLearningReportDetailItem) => buildStudyReportSubjectMastery(item)) } } function createEmptyStudyReportPeriod(reportType: ParentLearningReportType): StudyReportPeriod { return { rangeText: reportType === 1 ? '暂无周报' : '暂无月报', statusTitle: '暂无学情报告', statusDescription: '当前周期暂无报告数据。', newMasteredPoints: 0, consecutiveDays: 0, averageAccuracy: 0, scaleLabels: ['120分', '80分', '40分', '0分'], trends: [], activity: { summary: '暂无学习时长数据', activeDays: 0, totalDurationText: '0分钟', points: [] }, subjectMasteries: [] } } function buildStudyReportTrend(item: ParentLearningReportDetailItem, maxMinutes: number, reportType: ParentLearningReportType): StudyReportTrend { const details = Array.isArray(item.StudyDurationDetails) ? item.StudyDurationDetails : [] return { subjectId: String(item.SubjectId ?? '0'), points: details.map((detail: ParentLearningDurationDetail, index: number) => { const studyMinutes = secondsToMinutes(detail.StudyDurationTime) const overcomeMinutes = secondsToMinutes(detail.OvercomeStudyDurationTime) return { label: formatDurationDetailLabel(detail, index, reportType), value: studyMinutes, average: overcomeMinutes, valueHeight: scaleChartHeight(studyMinutes, maxMinutes, 72), averageHeight: scaleChartHeight(overcomeMinutes, maxMinutes, 72) } }) } } function buildStudyReportActivity(summary: ParentLearningReportDetailItem, maxMinutes: number, reportType: ParentLearningReportType): StudyReportActivity { const details = Array.isArray(summary.StudyDurationDetails) ? summary.StudyDurationDetails : [] return { summary: `总学习 ${formatSecondsDuration(summary.StudyDurationTime)},未攻克学习 ${formatSecondsDuration(summary.UnOvercomeStudyDurationTime)}。`, activeDays: Number(summary.ContinuousStudyDays) || 0, totalDurationText: formatSecondsDuration(summary.StudyDurationTime), points: details.map((detail: ParentLearningDurationDetail, index: number) => { const studyMinutes = secondsToMinutes(detail.StudyDurationTime) const overcomeMinutes = secondsToMinutes(detail.OvercomeStudyDurationTime) const unOvercomeMinutes = secondsToMinutes(detail.UnOvercomeStudyDurationTime) return { label: formatDurationDetailLabel(detail, index, reportType), studyMinutes, plannedMinutes: overcomeMinutes, newMasteredCount: unOvercomeMinutes, studyHeight: scaleChartHeight(studyMinutes, maxMinutes, 90), plannedHeight: scaleChartHeight(overcomeMinutes, maxMinutes, 90), newMasteredHeight: unOvercomeMinutes > 0 ? scaleChartHeight(unOvercomeMinutes, maxMinutes, 72) : 0 } }) } } function buildStudyReportSubjectMastery(item: ParentLearningReportDetailItem): StudyReportSubjectMastery { const newCount = Number(item.NewKnowledgePointCount) || 0 const overcomeCount = Number(item.OvercomeKnowledgePointCount) || 0 return { subjectName: item.SubjectName ?? '未命名科目', masteredCount: overcomeCount, totalCount: newCount, masteredPercent: newCount > 0 ? Math.min(100, Math.round(overcomeCount / newCount * 100)) : 0 } } function secondsToMinutes(seconds: number): number { return Math.round((Number(seconds) || 0) / 60) } function scaleChartHeight(value: number, maxValue: number, maxHeight: number): number { if (value <= 0 || maxValue <= 0) return 0 return Math.max(4, Math.round(value / maxValue * maxHeight)) } function buildDurationScaleLabels(maxMinutes: number): string[] { const top = Math.max(60, Math.ceil(maxMinutes / 30) * 30) return [`${top}分`, `${Math.round(top * 2 / 3)}分`, `${Math.round(top / 3)}分`, '0分'] } function formatSecondsDuration(seconds: number): string { const totalMinutes = secondsToMinutes(seconds) const hours = Math.floor(totalMinutes / 60) const minutes = totalMinutes % 60 if (hours > 0 && minutes > 0) return `${hours}小时${minutes}分` if (hours > 0) return `${hours}小时` return `${minutes}分钟` } function formatReportRange(start: string | null | undefined, end: string | null | undefined): string { if (!start || !end) return '暂无统计周期' return `${formatReportDate(start)} - ${formatReportDate(end)}` } function formatReportDate(value: string | null | undefined): string { if (!value) return '--' const text = String(value) if (text.length >= 10) return text.substring(5, 10).replace('-', '月') + '日' return text } function formatDurationDetailLabel(detail: ParentLearningDurationDetail, index: number, reportType: ParentLearningReportType): string { if (reportType === 2) return `第${index + 1}周` return formatReportDate(detail.StartDate) } export function apiGetStudentVocabulary(studentId: string, showLoading: boolean = true): Promise | null> { return get(`/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 | null> { return get(`/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 | null> { const prevPageQuery = prevPageId === undefined || prevPageId === null ? '' : `&PrevPageId=${prevPageId}` return get(`/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 | null> { return apiGetParentStudyPacks(studentId, 0, showLoading) } /** * 获取学生当天学习包任务。Status: 0 全部,1 待完成,2 已完成,3 未通过。 */ export function apiGetParentStudyPacks(studentId: string, status: ParentStudyPackStatus = 0, showLoading: boolean = true): Promise | null> { return get(`/api/v1/ParentStudyPack/GetTodayList?userId=${encodeURIComponent(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}%` const maxCount = Number(pack.maxOvercomeCount) || 0 const currentCount = Number(pack.currentAttemptNo || pack.completedAttemptCount) || 0 const remainingCount = Number(pack.remainingAttemptCount) || 0 const durationSecond = Number(pack.answerDurationSecond) || 0 const durationMinute = Math.floor(durationSecond / 60) const attemptText = taskStatus === 'not_started' ? `尚未开始 · 最多 ${maxCount || 2} 次机会` : `第 ${currentCount || 1} 次攻克 · 共 ${maxCount || currentCount || 1} 次` const resultText = taskStatus === 'success' ? `第 ${currentCount || 1} 次攻克成功 · 共 ${maxCount || currentCount || 1} 次` : taskStatus === 'failed' ? `攻克失败 · 剩余 ${remainingCount} 次机会` : taskStatus === 'in_progress' ? `攻克中 · 剩余 ${remainingCount} 次机会` : attemptText const answerText = durationMinute > 0 ? `答题时长 ${durationMinute} 分钟` : '暂无答题时长' const recordTime = formatTaskDateTime(pack.latestRecordTime) const deadlineText = formatTaskDateTime(pack.deadlineTime) const metaParts: string[] = [`正确率 ${accuracy}`, answerText] if (recordTime !== '--') metaParts.push(`${recordTime} 完成`) else if (deadlineText !== '--') metaParts.push(`${deadlineText} 截止`) return { id: String(pack.packId ?? pack.knowledgePointId ?? ''), title: pack.name || '未命名学习任务', status: taskStatus, statusText: pack.statusName || statusTextMap[taskStatus], resultText, accuracyText: accuracy, answerText, attemptText, deadlineText, metaText: metaParts.join(' · ') } }) 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, totalCount: Number(res.data.totalCount) || groups.reduce((total: number, group: LearningTaskGroup) => total + group.tasks.length, 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 } function formatTaskDateTime(value: any): string { if (!value) return '--' const text = String(value) if (text.length < 16) return text return `今天 ${text.substring(11, 16)}` } /** * 兼容旧调用签名,实际统一使用今日学习预警接口。 */ export function apiGetStudentAlerts(studentId: string, _page: number = 1, _pageSize: number = 20, limit?: number, showLoading: boolean = true): Promise | null> { return apiGetStudentLearningWarnings(studentId, showLoading, limit) }