Files
2026-11-10 11:25:04 +08:00

1023 lines
34 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
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<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<ParentStudentTodayInfoResult>(`/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<ApiResponse<StudentAlertList> | null> {
return get<StudentLearningWarningResult>(`/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<ApiResponse<TodayLearningDetail> | null> {
return get<ParentTodayLearningSituationResult>(`/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<ApiResponse<StudentStudyReport> | null> {
const weekRes = await get<ParentLearningReportResult>(`/api/v1/ParentLearningReport/Get?userId=${encodeURIComponent(studentId)}&reportType=1`, undefined, { showLoading })
const monthRes = await get<ParentLearningReportResult>(`/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<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=${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<ApiResponse<StudentAlertList> | null> {
return apiGetStudentLearningWarnings(studentId, showLoading, limit)
}