接入学生学习数据接口

This commit is contained in:
tanxin
2026-11-05 11:01:12 +08:00
parent d1d44e250e
commit af73453035
15 changed files with 336 additions and 48 deletions
+2 -1
View File
@@ -4,6 +4,7 @@
import { post } from '../utils/request' import { post } from '../utils/request'
import type { ApiResponse } from '../utils/request' import type { ApiResponse } from '../utils/request'
import { md5 } from '../utils/md5'
// ==================== 类型定义 ==================== // ==================== 类型定义 ====================
@@ -85,7 +86,7 @@ export type RefreshTokenResponse = {
* 密码登录 * 密码登录
*/ */
export function apiLogin(params: LoginParams): Promise<ApiResponse<UserInfo> | null> { export function apiLogin(params: LoginParams): Promise<ApiResponse<UserInfo> | null> {
return post<any>('/api/v1/Parents/Login', { phone: params.phone, password: params.password, loginType: 0 }).then((res) => { return post<any>('/api/v1/Parents/Login', { phone: params.phone, password: md5(params.password), loginType: 0 }).then((res) => {
if (!res || !res.data) return null if (!res || !res.data) return null
return { code: res.code, message: res.message, data: { userId: String(res.data.parentId), phone: res.data.phone, nickname: res.data.name, avatar: '', token: res.data.token, refreshToken: '' } } return { code: res.code, message: res.message, data: { userId: String(res.data.parentId), phone: res.data.phone, nickname: res.data.name, avatar: '', token: res.data.token, refreshToken: '' } }
}) })
+196 -6
View File
@@ -14,7 +14,8 @@ export type StudentInfo = {
account: string account: string
avatar: string avatar: string
grade: string grade: string
class: string className: string
schoolName: string
} }
// 首页当前学生基础信息 // 首页当前学生基础信息
@@ -24,6 +25,7 @@ export type StudentHomeProfile = {
account: string account: string
grade: string grade: string
school: string school: string
className: string
avatar: string avatar: string
comprehensiveLevel: 'excellent' | 'good' | 'qualified' | 'needs_improvement' comprehensiveLevel: 'excellent' | 'good' | 'qualified' | 'needs_improvement'
} }
@@ -35,6 +37,7 @@ export type BoundStudentSummary = {
account: string account: string
grade: string grade: string
school: string school: string
className: string
avatar: string avatar: string
} }
@@ -46,6 +49,7 @@ export type StudentHomeMetrics = {
learningMinutesToday: number learningMinutesToday: number
accuracyToday: number | null accuracyToday: number | null
learningDaysThisWeek: number learningDaysThisWeek: number
shouldStudyDaysThisWeek: number
pendingHomeworkCount: number pendingHomeworkCount: number
updatedAt: string updatedAt: string
} }
@@ -156,6 +160,7 @@ export type TodayLearningDetail = {
export type VocabularyBook = { export type VocabularyBook = {
id: string id: string
name: string name: string
cover: string
subtitle: string subtitle: string
totalWords: number totalWords: number
learnedWords: number learnedWords: number
@@ -228,6 +233,8 @@ export type StudentLearningTasks = {
groups: LearningTaskGroup[] groups: LearningTaskGroup[]
} }
export type ParentStudyPackStatus = 0 | 1 | 2 | 3
export type StudentAlertLevel = 'critical' | 'high' | 'medium' | 'low' export type StudentAlertLevel = 'critical' | 'high' | 'medium' | 'low'
export type StudentAlert = { export type StudentAlert = {
@@ -249,6 +256,22 @@ export type StudentAlertList = {
} }
} }
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[]
}
// 验证学生二维码请求参数 // 验证学生二维码请求参数
export type VerifyQRCodeParams = { export type VerifyQRCodeParams = {
qrCode: string qrCode: string
@@ -277,7 +300,7 @@ export function apiVerifyStudentQRCode(params: VerifyQRCodeParams): Promise<ApiR
export function apiVerifyStudentAccount(params: VerifyAccountParams): Promise<ApiResponse<StudentInfo> | null> { export function apiVerifyStudentAccount(params: VerifyAccountParams): Promise<ApiResponse<StudentInfo> | null> {
return get<any>('/api/v1/Parents/VerifyStudent', { RealName: params.name, Account: params.account }).then((res) => { return get<any>('/api/v1/Parents/VerifyStudent', { RealName: params.name, Account: params.account }).then((res) => {
if (!res || !res.data) return null 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 || '', class: res.data.className || '' } } 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 || '' } }
}) })
} }
@@ -301,14 +324,83 @@ export function apiGetStudentHomeProfile(studentId?: string, showLoading: boolea
* 获取当前家长绑定的全部学生,用于首页切换学生。 * 获取当前家长绑定的全部学生,用于首页切换学生。
*/ */
export function apiGetBoundStudents(): Promise<ApiResponse<{ students: BoundStudentSummary[], currentStudentId: string | null }> | null> { export function apiGetBoundStudents(): Promise<ApiResponse<{ students: BoundStudentSummary[], currentStudentId: string | null }> | null> {
return get<{ students: BoundStudentSummary[], currentStudentId: string | null }>('/api/parent/students') 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> { export function apiGetStudentHomeMetrics(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentHomeMetrics> | null> {
return get<StudentHomeMetrics>(`/api/parent/home-metrics?studentId=${studentId}`, undefined, { showLoading }) 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): Promise<ApiResponse<StudentAlertList> | null> {
return get<any>(`/api/v1/ParentLearningWarning/GetTodayList?userId=${studentId}`, undefined, { showLoading }).then((res) => {
if (!res || !res.data || !Array.isArray(res.data.items)) return null
const list: StudentAlert[] = res.data.items
.filter((item: any) => Boolean(item.isTriggered))
.map((item: any, index: number) => ({
id: `${studentId}-${item.type ?? index}`,
level: 'high',
title: item.title || '学习预警',
content: item.description || item.statusName || '',
occurredAt: res.data.date || '',
isRead: false
}))
return {
code: res.code,
message: res.message,
data: {
list,
pagination: {
page: 1,
pageSize: list.length,
total: list.length,
hasMore: false
}
}
}
})
} }
/** /**
@@ -326,7 +418,37 @@ export function apiGetStudentStudyReport(studentId: string, showLoading: boolean
} }
export function apiGetStudentVocabulary(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentVocabulary> | null> { export function apiGetStudentVocabulary(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentVocabulary> | null> {
return get<StudentVocabulary>(`/api/parent/vocabulary?studentId=${studentId}`, undefined, { showLoading }) 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> { export function apiGetStudentGrowth(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentGrowth> | null> {
@@ -334,7 +456,75 @@ export function apiGetStudentGrowth(studentId: string, showLoading: boolean = tr
} }
export function apiGetStudentLearningTasks(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentLearningTasks> | null> { export function apiGetStudentLearningTasks(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentLearningTasks> | null> {
return get<StudentLearningTasks>(`/api/parent/learning-tasks?studentId=${studentId}`, undefined, { showLoading }) 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
} }
/** /**
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<view class="page-container"> <view class="page-container">
<view class="header-panel"><view class="header-back" @tap="goHome"><UniIcons type="back" class="header-back-icon"></UniIcons></view><view class="header-copy"><text class="header-title">成长激励</text><text class="header-subtitle">{{ growth.studentName }} · {{ growth.studentGrade }}·{{ growth.studentClass }}</text></view><view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view></view> <view class="header-panel"><view class="header-back" @tap="goHome"><UniIcons type="back" class="header-back-icon"></UniIcons></view><view class="header-copy"><text class="header-title">成长激励</text><text class="header-subtitle">{{ homeStudentProfile?.name ?? growth.studentName }} · {{ homeStudentProfile?.school ?? '' }} {{ homeStudentProfile?.className ?? growth.studentClass }}</text></view><view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view></view>
<scroll-view class="content-scroll" direction="vertical" scroll-y="true"><view v-if="isLoading" class="status-panel"><text class="status-text">成长数据加载中</text></view><view v-else class="content-panel"><view class="points-card"><view class="points-heading"><view class="points-copy"><text class="points-label">积分余额</text><text class="points-value">{{ growth.pointsBalance }}</text></view><view class="level-badge"><text class="level-label">等级</text><text class="level-value">Lv.{{ growth.level }}</text></view></view><view class="points-stats"><view class="points-stat"><text class="points-stat-value">{{ growth.rank }}</text><text class="points-stat-label">班级排名</text></view><view class="points-stat points-stat-middle"><text class="points-stat-value">{{ growth.streakDays }}</text><text class="points-stat-label">未能连续</text></view><view class="points-stat"><text class="points-stat-value">{{ growth.totalPoints }}</text><text class="points-stat-label">获得总量</text></view></view></view><view class="flow-card"><text class="section-title">积分流水</text><text class="section-description">记录孩子每天做了哪些正向行为</text><view class="flow-list"><view v-for="record in growth.records" :key="record.id" class="flow-item"><view class="flow-icon" :class="'flow-icon-' + record.icon"><UniIcons :type="record.icon === 'calendar' ? 'calendar' : record.icon === 'assignment' ? 'checkbox' : record.icon === 'streak' ? 'fire' : record.icon === 'accuracy' ? 'circle-filled' : record.icon === 'mastery' ? 'star-filled' : 'compose'" class="flow-icon-glyph"></UniIcons></view><view class="flow-copy"><text class="flow-title">{{ record.title }}</text><text class="flow-time">{{ record.timeText }}</text></view><text class="flow-points">+{{ record.points }}</text></view></view></view></view></scroll-view> <scroll-view class="content-scroll" direction="vertical" scroll-y="true"><view v-if="isLoading" class="status-panel"><text class="status-text">成长数据加载中</text></view><view v-else class="content-panel"><view class="points-card"><view class="points-heading"><view class="points-copy"><text class="points-label">积分余额</text><text class="points-value">{{ growth.pointsBalance }}</text></view><view class="level-badge"><text class="level-label">等级</text><text class="level-value">Lv.{{ growth.level }}</text></view></view><view class="points-stats"><view class="points-stat"><text class="points-stat-value">{{ growth.rank }}</text><text class="points-stat-label">班级排名</text></view><view class="points-stat points-stat-middle"><text class="points-stat-value">{{ growth.streakDays }}</text><text class="points-stat-label">未能连续</text></view><view class="points-stat"><text class="points-stat-value">{{ growth.totalPoints }}</text><text class="points-stat-label">获得总量</text></view></view></view><view class="flow-card"><text class="section-title">积分流水</text><text class="section-description">记录孩子每天做了哪些正向行为</text><view class="flow-list"><view v-for="record in growth.records" :key="record.id" class="flow-item"><view class="flow-icon" :class="'flow-icon-' + record.icon"><UniIcons :type="record.icon === 'calendar' ? 'calendar' : record.icon === 'assignment' ? 'checkbox' : record.icon === 'streak' ? 'fire' : record.icon === 'accuracy' ? 'circle-filled' : record.icon === 'mastery' ? 'star-filled' : 'compose'" class="flow-icon-glyph"></UniIcons></view><view class="flow-copy"><text class="flow-title">{{ record.title }}</text><text class="flow-time">{{ record.timeText }}</text></view><text class="flow-points">+{{ record.points }}</text></view></view></view></view></scroll-view>
</view> </view>
</template> </template>
+41 -20
View File
@@ -3,7 +3,7 @@
<!-- 固定顶部信息区 --> <!-- 固定顶部信息区 -->
<view class="header-panel"> <view class="header-panel">
<view class="header-copy"> <view class="header-copy">
<text class="header-greeting">下午好,王女士</text> <text class="header-greeting">下午好,{{ parentName }}</text>
<text class="header-title">孩子今天的学习情况</text> <text class="header-title">孩子今天的学习情况</text>
<text class="header-update">数据更新时间 {{ metricsUpdateText }} · 系统定时预计暂停,非实时</text> <text class="header-update">数据更新时间 {{ metricsUpdateText }} · 系统定时预计暂停,非实时</text>
</view> </view>
@@ -25,7 +25,7 @@
<view class="student-identity"> <view class="student-identity">
<view class="student-name-row"> <view class="student-name-row">
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
<text class="student-grade">{{ student.grade }}</text> <text class="student-grade">{{ student.className }}</text>
</view> </view>
<view class="student-level-row"> <view class="student-level-row">
<view class="level-medal"></view> <view class="level-medal"></view>
@@ -58,7 +58,7 @@
</view> </view>
<view class="metric-item"> <view class="metric-item">
<text class="metric-label">本周学习天数</text> <text class="metric-label">本周学习天数</text>
<text class="metric-value metric-blue">{{ metrics.learningDaysThisWeek }}/7 天</text> <text class="metric-value metric-blue">{{ metrics.learningDaysThisWeek }}/{{ metrics.shouldStudyDaysThisWeek }} 天</text>
</view> </view>
<view class="metric-item"> <view class="metric-item">
<text class="metric-label">待完成作业</text> <text class="metric-label">待完成作业</text>
@@ -102,7 +102,7 @@
<view v-else-if="boundStudents.length === 0" class="student-picker-empty"><text class="student-picker-empty-text">暂无已绑定学生</text></view> <view v-else-if="boundStudents.length === 0" class="student-picker-empty"><text class="student-picker-empty-text">暂无已绑定学生</text></view>
<view v-else v-for="item in boundStudents" :key="item.id" class="student-picker-item" :class="selectedStudentId === item.id ? 'student-picker-item-selected' : ''" @tap="selectStudent(item.id)"> <view v-else v-for="item in boundStudents" :key="item.id" class="student-picker-item" :class="selectedStudentId === item.id ? 'student-picker-item-selected' : ''" @tap="selectStudent(item.id)">
<view class="student-picker-avatar"><image v-if="item.avatar" class="student-picker-avatar-image" :src="item.avatar" mode="aspectFill"></image><text v-else class="student-picker-avatar-text">{{ item.name.substring(0, 1) }}</text></view> <view class="student-picker-avatar"><image v-if="item.avatar" class="student-picker-avatar-image" :src="item.avatar" mode="aspectFill"></image><text v-else class="student-picker-avatar-text">{{ item.name.substring(0, 1) }}</text></view>
<view class="student-picker-info"><view class="student-picker-name-row"><text class="student-picker-name">{{ item.name }}</text><text class="student-picker-grade">{{ item.grade }}</text></view><text class="student-picker-detail">账号:{{ item.account }}</text><text class="student-picker-detail">学校:{{ item.school }}</text></view> <view class="student-picker-info"><view class="student-picker-name-row"><text class="student-picker-name">{{ item.name }}</text><text class="student-picker-grade">{{ item.school }} {{ item.className }}</text></view><text class="student-picker-detail">账号:{{ item.account }}</text></view>
<view v-if="selectedStudentId === item.id" class="student-picker-check"><text class="student-picker-check-text">已选</text></view> <view v-if="selectedStudentId === item.id" class="student-picker-check"><text class="student-picker-check-text">已选</text></view>
</view> </view>
</scroll-view> </scroll-view>
@@ -115,8 +115,9 @@
<script setup lang="ts"> <script setup lang="ts">
import MainTabBar from '../../components/main-tab-bar.uvue' import MainTabBar from '../../components/main-tab-bar.uvue'
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue' import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
import { apiGetBoundStudents, apiGetStudentAlerts, apiGetStudentHomeMetrics, apiGetStudentHomeProfile, type BoundStudentSummary, type StudentAlert, type StudentAlertLevel, type StudentHomeMetrics, type StudentHomeProfile } from '../../api/student' import { apiGetBoundStudents, apiGetStudentHomeMetrics, apiGetStudentLearningWarnings, type BoundStudentSummary, type StudentAlert, type StudentAlertLevel, type StudentHomeMetrics, type StudentHomeProfile } from '../../api/student'
import { homeStudentAlerts, homeStudentMetrics, homeStudentProfile, setHomeStudentAlerts, setHomeStudentMetrics, setHomeStudentProfile } from '../../store/home' import { applyBoundStudent, getSavedSelectedStudent, homeStudentAlerts, homeStudentMetrics, homeStudentProfile, saveSelectedStudent, setHomeStudentAlerts, setHomeStudentMetrics } from '../../store/home'
import type { UserInfo } from '../../api/auth'
const loadingStudent = ref<StudentHomeProfile>({ const loadingStudent = ref<StudentHomeProfile>({
id: '', id: '',
@@ -124,6 +125,7 @@
account: '', account: '',
grade: '', grade: '',
school: '', school: '',
className: '',
avatar: '', avatar: '',
comprehensiveLevel: 'qualified' comprehensiveLevel: 'qualified'
}) })
@@ -135,9 +137,13 @@
const student = computed((): StudentHomeProfile => homeStudentProfile.value ?? loadingStudent.value) const student = computed((): StudentHomeProfile => homeStudentProfile.value ?? loadingStudent.value)
const metrics = computed((): StudentHomeMetrics => homeStudentMetrics.value ?? { const metrics = computed((): StudentHomeMetrics => homeStudentMetrics.value ?? {
studentId: '', onlineToday: false, checkedInToday: false, learningMinutesToday: 0, studentId: '', onlineToday: false, checkedInToday: false, learningMinutesToday: 0,
accuracyToday: null, learningDaysThisWeek: 0, pendingHomeworkCount: 0, updatedAt: '' accuracyToday: null, learningDaysThisWeek: 0, shouldStudyDaysThisWeek: 0, pendingHomeworkCount: 0, updatedAt: ''
}) })
const alerts = computed((): StudentAlert[] => homeStudentAlerts.value ?? []) const alerts = computed((): StudentAlert[] => homeStudentAlerts.value ?? [])
const parentName = computed((): string => {
const userInfo = uni.getStorageSync('userInfo') as UserInfo | null
return userInfo?.nickname || '家长'
})
const studentInitial = computed((): string => { const studentInitial = computed((): string => {
return student.value.name.length > 0 ? student.value.name.substring(0, 1) : '学' return student.value.name.length > 0 ? student.value.name.substring(0, 1) : '学'
@@ -165,18 +171,9 @@
}) })
onLoad(() => { onLoad(() => {
if (!homeStudentProfile.value || !homeStudentMetrics.value || homeStudentAlerts.value === null) {
loadHomeData() loadHomeData()
}
}) })
async function loadStudentHomeProfile(studentId?: string, showLoading: boolean = true) {
const res = await apiGetStudentHomeProfile(studentId, showLoading)
if (res && res.data) {
setHomeStudentProfile(res.data)
}
}
async function loadStudentHomeMetrics(studentId: string, showLoading: boolean = true) { async function loadStudentHomeMetrics(studentId: string, showLoading: boolean = true) {
const res = await apiGetStudentHomeMetrics(studentId, showLoading) const res = await apiGetStudentHomeMetrics(studentId, showLoading)
if (res && res.data) { if (res && res.data) {
@@ -185,18 +182,37 @@
} }
async function loadStudentAlerts(studentId: string, showLoading: boolean = true) { async function loadStudentAlerts(studentId: string, showLoading: boolean = true) {
const res = await apiGetStudentAlerts(studentId, 1, 20, 3, showLoading) const res = await apiGetStudentLearningWarnings(studentId, showLoading)
if (res && res.data) { if (res && res.data) {
setHomeStudentAlerts(res.data.list) setHomeStudentAlerts(res.data.list)
} }
} }
async function loadHomeData(studentId?: string, showLoading: boolean = true) { async function loadHomeData(studentId?: string, showLoading: boolean = true) {
await loadStudentHomeProfile(studentId, showLoading) let selectedId = studentId || homeStudentProfile.value?.id || ''
if (selectedId && (!homeStudentProfile.value || homeStudentProfile.value.id !== selectedId)) {
const savedStudent = getSavedSelectedStudent()
if (savedStudent?.id === selectedId) applyBoundStudent(savedStudent)
}
if (!selectedId) {
const savedStudent = getSavedSelectedStudent()
if (savedStudent) {
selectedId = savedStudent.id
applyBoundStudent(savedStudent)
} else {
const res = await apiGetBoundStudents()
const firstStudent = res?.data?.students[0]
if (firstStudent) {
saveSelectedStudent(firstStudent)
applyBoundStudent(firstStudent)
selectedId = firstStudent.id
}
}
}
if (student.value.id) { if (student.value.id) {
await Promise.all([ await Promise.all([
loadStudentHomeMetrics(student.value.id, showLoading), loadStudentHomeMetrics(selectedId || student.value.id, showLoading),
loadStudentAlerts(student.value.id, showLoading) loadStudentAlerts(selectedId || student.value.id, showLoading)
]) ])
} }
} }
@@ -261,6 +277,11 @@
return return
} }
closeStudentPicker() closeStudentPicker()
const selectedStudent = boundStudents.value.find((item) => item.id === selectedStudentId.value)
if (selectedStudent) {
saveSelectedStudent(selectedStudent)
applyBoundStudent(selectedStudent)
}
if (selectedStudentId.value !== student.value.id) { if (selectedStudentId.value !== student.value.id) {
await loadHomeData(selectedStudentId.value) await loadHomeData(selectedStudentId.value)
} }
+19
View File
@@ -66,7 +66,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { apiLogin } from '../../api/auth' import { apiLogin } from '../../api/auth'
import { apiGetBoundStudents, type BoundStudentSummary } from '../../api/student'
import { setUserInfo, saveRememberInfo, getRememberInfo } from '../../store/user' import { setUserInfo, saveRememberInfo, getRememberInfo } from '../../store/user'
import { applyBoundStudent, saveSelectedStudent } from '../../store/home'
type LoginForm = { type LoginForm = {
phone: string phone: string
@@ -111,6 +113,7 @@
if (res && res.data) { if (res && res.data) {
// 保存用户信息 // 保存用户信息
setUserInfo(res.data) setUserInfo(res.data)
await restoreSelectedStudent(res.data.userId)
// 保存记住密码信息 // 保存记住密码信息
saveRememberInfo(form.phone, form.password, form.remember) saveRememberInfo(form.phone, form.password, form.remember)
@@ -129,6 +132,22 @@
} }
} }
/**
* 登录后加载家长绑定的学生,并恢复或初始化当前学生。
*/
async function restoreSelectedStudent(userId: string) {
const res = await apiGetBoundStudents()
if (!res || !res.data || res.data.students.length === 0) return
const savedStudent = uni.getStorageSync(`selectedStudent_${userId}`) as BoundStudentSummary | null
const selectedStudent = savedStudent && res.data.students.some((item) => item.id === savedStudent.id)
? res.data.students.find((item) => item.id === savedStudent.id)!
: res.data.students[0]
saveSelectedStudent(selectedStudent)
applyBoundStudent(selectedStudent)
}
/** /**
* 忘记密码 * 忘记密码
*/ */
+6 -3
View File
@@ -31,7 +31,7 @@
<text class="child-role">{{ index === 0 ? '儿子' : index === 1 ? '侄子' : '女儿' }}</text> <text class="child-role">{{ index === 0 ? '儿子' : index === 1 ? '侄子' : '女儿' }}</text>
<text class="child-status">{{ index === 1 ? '中上' : '优秀' }}</text> <text class="child-status">{{ index === 1 ? '中上' : '优秀' }}</text>
</view> </view>
<text class="child-school">{{ student.school }} · {{ student.grade }}({{ index + 1 }})班</text> <text class="child-school">{{ student.school }} · {{ student.className }}</text>
</view> </view>
<text class="child-action" :class="selectedStudentId === student.id ? 'child-action-selected' : ''">{{ selectedStudentId === student.id ? '当前查看' : '切换' }}</text> <text class="child-action" :class="selectedStudentId === student.id ? 'child-action-selected' : ''">{{ selectedStudentId === student.id ? '当前查看' : '切换' }}</text>
</view> </view>
@@ -62,7 +62,7 @@
<script setup lang="ts"> <script setup lang="ts">
import MainTabBar from '../../components/main-tab-bar.uvue' import MainTabBar from '../../components/main-tab-bar.uvue'
import { apiGetBoundStudents, type BoundStudentSummary } from '../../api/student' import { apiGetBoundStudents, type BoundStudentSummary } from '../../api/student'
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home' import { applyBoundStudent, homeStudentProfile, saveSelectedStudent } from '../../store/home'
import type { UserInfo } from '../../api/auth' import type { UserInfo } from '../../api/auth'
import { clearUserInfo } from '../../store/user' import { clearUserInfo } from '../../store/user'
@@ -91,7 +91,10 @@
function selectStudent(studentId: string) { function selectStudent(studentId: string) {
selectedStudentId.value = studentId selectedStudentId.value = studentId
const student = students.value.find((item) => item.id === studentId) const student = students.value.find((item) => item.id === studentId)
if (student) setHomeStudentProfile({ id: student.id, name: student.name, account: student.account, grade: student.grade, school: student.school, avatar: student.avatar, comprehensiveLevel: 'good' }) if (student) {
saveSelectedStudent(student)
applyBoundStudent(student)
}
} }
function openVerification(mode: string) { uni.navigateTo({ url: '/pages/profile/verify-student?mode=' + mode }) } function openVerification(mode: string) { uni.navigateTo({ url: '/pages/profile/verify-student?mode=' + mode }) }
+1 -1
View File
@@ -79,7 +79,7 @@
<image class="student-avatar" :src="student.avatar || '/static/avatar-default.png'" mode="aspectFill"></image> <image class="student-avatar" :src="student.avatar || '/static/avatar-default.png'" mode="aspectFill"></image>
<view class="student-info"> <view class="student-info">
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
<text class="student-detail">{{ student.grade }} {{ student.class }} | {{ student.account }}</text> <text class="student-detail">{{ student.schoolName }} {{ student.className }} | {{ student.account }}</text>
</view> </view>
<button class="remove-btn" @tap="handleRemoveStudent(index)">移除</button> <button class="remove-btn" @tap="handleRemoveStudent(index)">移除</button>
</view> </view>
+2 -1
View File
@@ -113,6 +113,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { apiRegister } from '../../api/auth' import { apiRegister } from '../../api/auth'
import { registrationStudents } from '../../store/registration' import { registrationStudents } from '../../store/registration'
import { md5 } from '../../utils/md5'
type ParentAccountForm = { type ParentAccountForm = {
parentName: string parentName: string
@@ -190,7 +191,7 @@
parentName: form.parentName, parentName: form.parentName,
phone: form.phone, phone: form.phone,
relationship: form.relationship, relationship: form.relationship,
password: form.password, password: md5(form.password),
students: studentIds students: studentIds
}) })
} catch (error) { } catch (error) {
+1 -1
View File
@@ -58,7 +58,7 @@
<image class="student-avatar" :src="student.avatar || '/static/avatar-default.png'" mode="aspectFill"></image> <image class="student-avatar" :src="student.avatar || '/static/avatar-default.png'" mode="aspectFill"></image>
<view class="student-info"> <view class="student-info">
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
<text class="student-detail">{{ student.grade }} {{ student.class }} | {{ student.account }}</text> <text class="student-detail">{{ student.schoolName }} {{ student.className }} | {{ student.account }}</text>
</view> </view>
<button class="remove-btn" @tap="handleRemoveStudent(index)">移除</button> <button class="remove-btn" @tap="handleRemoveStudent(index)">移除</button>
</view> </view>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<view class="page-container"> <view class="page-container">
<view class="header-panel"> <view class="header-panel">
<view class="header-copy"><text class="header-title">学情报告</text><text class="header-subtitle">{{ report.studentName }} · {{ report.studentGrade }} · {{ report.studentClass }}</text></view> <view class="header-copy"><text class="header-title">学情报告</text><text class="header-subtitle">{{ homeStudentProfile?.name ?? report.studentName }} · {{ homeStudentProfile?.school ?? '' }} {{ homeStudentProfile?.className ?? report.studentClass }}</text></view>
<view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view> <view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view>
</view> </view>
<view class="report-meta"><text class="report-meta-text">{{ activeReport.rangeText }} · 数据更新到 {{ report.updatedAtText }}</text></view> <view class="report-meta"><text class="report-meta-text">{{ activeReport.rangeText }} · 数据更新到 {{ report.updatedAtText }}</text></view>
+18 -3
View File
@@ -5,15 +5,19 @@
<script setup lang="ts"> <script setup lang="ts">
import MainTabBar from '../../components/main-tab-bar.uvue' import MainTabBar from '../../components/main-tab-bar.uvue'
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue' import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
import { apiGetStudentHomeProfile, apiGetStudentLearningTasks, type LearningTaskItem, type StudentLearningTasks } from '../../api/student' import { apiGetParentStudyPacks, apiGetStudentHomeProfile, type LearningTaskItem, type ParentStudyPackStatus, type StudentLearningTasks } from '../../api/student'
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home' import { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
const filters = [{ id: 'all', name: '全部' }, { id: 'pending', name: '待完成' }, { id: 'completed', name: '已完成' }, { id: 'failed', name: '未通过' }] const filters = [{ id: 'all', name: '全部' }, { id: 'pending', name: '待完成' }, { id: 'completed', name: '已完成' }, { id: 'failed', name: '未通过' }]
const activeFilter = ref('all') const activeFilter = ref('all')
const activeStatus = computed((): ParentStudyPackStatus => activeFilter.value === 'pending' ? 1 : activeFilter.value === 'completed' ? 2 : activeFilter.value === 'failed' ? 3 : 0)
const isLoading = ref(true) const isLoading = ref(true)
const isRefreshing = ref(false) const isRefreshing = ref(false)
const tasks = ref<StudentLearningTasks>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', pendingCount: 0, completedCount: 0, failedCount: 0, groups: [] }) const tasks = ref<StudentLearningTasks>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', pendingCount: 0, completedCount: 0, failedCount: 0, groups: [] })
const visibleGroups = computed(() => tasks.value.groups.filter((group) => group.tasks.length === 0 || group.tasks.some((task) => matchesFilter(task)))) const visibleGroups = computed(() => tasks.value.groups.filter((group) => group.tasks.length === 0 || group.tasks.some((task) => matchesFilter(task))))
const headerStudentName = computed((): string => homeStudentProfile.value?.name ?? tasks.value.studentName)
const headerStudentGrade = computed((): string => homeStudentProfile.value?.grade ?? tasks.value.studentGrade)
const headerStudentClass = computed((): string => homeStudentProfile.value?.className ?? tasks.value.studentClass)
onLoad(() => { loadTasks() }) onLoad(() => { loadTasks() })
@@ -23,7 +27,15 @@
const profileRes = await apiGetStudentHomeProfile(undefined, false) const profileRes = await apiGetStudentHomeProfile(undefined, false)
if (profileRes && profileRes.data) { setHomeStudentProfile(profileRes.data); studentId = profileRes.data.id } if (profileRes && profileRes.data) { setHomeStudentProfile(profileRes.data); studentId = profileRes.data.id }
} }
if (studentId) { const res = await apiGetStudentLearningTasks(studentId); if (res && res.data) tasks.value = res.data } if (studentId) {
const res = await apiGetParentStudyPacks(studentId, activeStatus.value)
if (res && res.data) {
tasks.value = {
...res.data,
studentName: `${homeStudentProfile.value?.name ?? res.data.studentName} · ${homeStudentProfile.value?.school ?? ''} ${homeStudentProfile.value?.className ?? res.data.studentClass}`
}
}
}
isLoading.value = false isLoading.value = false
} }
@@ -33,7 +45,10 @@
isRefreshing.value = false isRefreshing.value = false
} }
function setFilter(filter: string) { activeFilter.value = filter } async function setFilter(filter: string) {
activeFilter.value = filter
await loadTasks()
}
function matchesFilter(task: LearningTaskItem): boolean { function matchesFilter(task: LearningTaskItem): boolean {
if (activeFilter.value === 'all') return true if (activeFilter.value === 'all') return true
if (activeFilter.value === 'pending') return task.status === 'not_started' || task.status === 'in_progress' if (activeFilter.value === 'pending') return task.status === 'not_started' || task.status === 'in_progress'
+1 -1
View File
@@ -2,7 +2,7 @@
<view class="page-container"> <view class="page-container">
<view class="header-panel"> <view class="header-panel">
<view class="header-back" @tap="goBack"><UniIcons type="back" class="header-back-icon"></UniIcons></view> <view class="header-back" @tap="goBack"><UniIcons type="back" class="header-back-icon"></UniIcons></view>
<view class="header-copy"><text class="header-title">今日学习</text><text class="header-subtitle">{{ detail.dateText }} {{ detail.studentName }}</text></view> <view class="header-copy"><text class="header-title">今日学习</text><text class="header-subtitle">{{ detail.dateText }} {{ homeStudentProfile?.name ?? detail.studentName }} · {{ homeStudentProfile?.school ?? '' }} {{ homeStudentProfile?.className ?? '' }}</text></view>
<view class="header-action" @tap="goBack"><UniIcons type="home-filled" class="header-action-icon"></UniIcons></view> <view class="header-action" @tap="goBack"><UniIcons type="home-filled" class="header-action-icon"></UniIcons></view>
</view> </view>
+12 -4
View File
@@ -1,7 +1,7 @@
<template> <template>
<view class="page-container"> <view class="page-container">
<view class="header-panel"><view class="header-back" @tap="goHome"><UniIcons type="back" class="header-back-icon"></UniIcons></view><view class="header-copy"><text class="header-title">英语词汇</text><text class="header-subtitle">{{ vocabulary.studentName }} · {{ vocabulary.studentGrade }}·{{ vocabulary.studentClass }}</text></view><view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view></view> <view class="header-panel"><view class="header-back" @tap="goHome"><UniIcons type="back" class="header-back-icon"></UniIcons></view><view class="header-copy"><text class="header-title">英语词汇</text><text class="header-subtitle">{{ homeStudentProfile?.name ?? vocabulary.studentName }} · {{ homeStudentProfile?.school ?? '' }} {{ homeStudentProfile?.className ?? vocabulary.studentClass }}</text></view><view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view></view>
<scroll-view class="content-scroll" direction="vertical" scroll-y="true"><view v-if="isLoading" class="status-panel"><text class="status-text">词汇书加载中</text></view><view v-else class="content-panel"><view class="list-heading"><text class="list-title">全部词书</text><text class="book-count">{{ vocabulary.bookCount }} 本</text></view><text class="list-description">每本词书的总词量与已学进度</text><view v-for="book in vocabulary.books" :key="book.id" class="book-card" :class="'book-card-' + book.color"><view class="book-heading"><view class="book-icon" :class="'book-icon-' + book.color"><UniIcons type="font" class="book-icon-glyph" :class="'book-icon-glyph-' + book.color"></UniIcons></view><view class="book-copy"><text class="book-name">{{ book.name }}</text><view class="book-meta"><text class="book-total">{{ book.subtitle }}</text><text class="book-level">{{ book.levelText }}</text></view></view><text class="book-status" :class="'book-status-' + book.color">{{ book.statusText }}</text></view><view class="progress-heading"><text class="progress-label">学习进度</text><text class="progress-value">{{ book.learnedWords > 0 ? '已学 ' + book.learnedWords + ' / ' + book.totalWords + ' 词 · ' + progressPercent(book) + '%' : '未开始' }}</text></view><view class="progress-track"><view class="progress-fill" :class="'progress-fill-' + book.color" :style="{ width: progressPercent(book) + '%' }"></view></view></view></view></scroll-view> <scroll-view class="content-scroll" direction="vertical" scroll-y="true"><view v-if="isLoading" class="status-panel"><text class="status-text">词汇书加载中</text></view><view v-else class="content-panel"><view class="list-heading"><text class="list-title">全部词书</text><text class="book-count">{{ vocabulary.bookCount }} 本</text></view><text class="list-description">每本词书的总词量与已学进度</text><view v-for="book in vocabulary.books" :key="book.id" class="book-card" :class="'book-card-' + book.color"><view class="book-heading"><view class="book-icon" :class="'book-icon-' + book.color"><image v-if="book.cover" class="book-cover" :src="book.cover" mode="aspectFill"></image><UniIcons v-else type="font" class="book-icon-glyph" :class="'book-icon-glyph-' + book.color"></UniIcons></view><view class="book-copy"><text class="book-name">{{ book.name }}</text><view class="book-meta"><text class="book-total">{{ book.subtitle }}</text><text class="book-level">{{ book.levelText }}</text></view></view><text class="book-status" :class="'book-status-' + book.color">{{ book.statusText }}</text></view><view class="progress-heading"><text class="progress-label">学习进度</text><text class="progress-value">{{ book.learnedWords > 0 ? '已学 ' + book.learnedWords + ' / ' + book.totalWords + ' 词 · ' + progressPercent(book) + '%' : '未开始' }}</text></view><view class="progress-track"><view class="progress-fill" :class="'progress-fill-' + book.color" :style="{ width: progressPercent(book) + '%' }"></view></view></view></view></scroll-view>
</view> </view>
</template> </template>
@@ -26,7 +26,14 @@
} }
if (studentId) { if (studentId) {
const res = await apiGetStudentVocabulary(studentId) const res = await apiGetStudentVocabulary(studentId)
if (res && res.data) vocabulary.value = res.data if (res && res.data) {
vocabulary.value = {
...res.data,
studentName: homeStudentProfile.value?.name ?? '',
studentGrade: homeStudentProfile.value?.grade ?? '',
studentClass: homeStudentProfile.value?.className ?? ''
}
}
} }
isLoading.value = false isLoading.value = false
} }
@@ -63,7 +70,8 @@
.book-card-blue { border-color: #9cd7ff; background-color: #eff8ff; } .book-card-blue { border-color: #9cd7ff; background-color: #eff8ff; }
.book-card-gray { border-color: #dbe2eb; background-color: #f2f6fa; } .book-card-gray { border-color: #dbe2eb; background-color: #f2f6fa; }
.book-heading { display: flex; flex-direction: row; align-items: flex-start; } .book-heading { display: flex; flex-direction: row; align-items: flex-start; }
.book-icon { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 11px; } .book-icon { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 64px; height: 64px; flex-shrink: 0; border-radius: 11px; }
.book-cover { width: 64px; height: 64px; border-radius: 11px; }
.book-icon-green { background-color: #16bd88; } .book-icon-green { background-color: #16bd88; }
.book-icon-yellow { background-color: #fff0c5; } .book-icon-yellow { background-color: #fff0c5; }
.book-icon-blue { background-color: #d9f0ff; } .book-icon-blue { background-color: #d9f0ff; }
+31 -1
View File
@@ -6,7 +6,37 @@ export const homeStudentMetrics = ref<StudentHomeMetrics | null>(null)
export const homeStudentAlerts = ref<StudentAlert[] | null>(null) export const homeStudentAlerts = ref<StudentAlert[] | null>(null)
export function setHomeStudentProfile(profile: StudentHomeProfile) { export function setHomeStudentProfile(profile: StudentHomeProfile) {
homeStudentProfile.value = profile const userInfo = uni.getStorageSync('userInfo') as { userId?: string } | null
const boundStudent = userInfo?.userId
? uni.getStorageSync(`selectedStudent_${userInfo.userId}`) as { id?: string, name?: string, account?: string, grade?: string, school?: string, className?: string, avatar?: string } | null
: null
const currentProfile = homeStudentProfile.value?.id === profile.id ? homeStudentProfile.value : null
const savedStudent = boundStudent?.id === profile.id ? boundStudent : null
homeStudentProfile.value = {
...profile,
name: savedStudent?.name ?? currentProfile?.name ?? profile.name,
account: savedStudent?.account ?? currentProfile?.account ?? profile.account,
grade: savedStudent?.grade ?? currentProfile?.grade ?? profile.grade,
school: savedStudent?.school ?? currentProfile?.school ?? profile.school,
className: savedStudent?.className ?? currentProfile?.className ?? profile.className,
avatar: savedStudent?.avatar ?? currentProfile?.avatar ?? profile.avatar
}
}
export function saveSelectedStudent(student: { id: string, name: string, account: string, grade: string, school: string, className: string, avatar: string }) {
const userInfo = uni.getStorageSync('userInfo') as { userId?: string } | null
if (userInfo?.userId) uni.setStorageSync(`selectedStudent_${userInfo.userId}`, student)
}
export function getSavedSelectedStudent(): { id: string, name: string, account: string, grade: string, school: string, className: string, avatar: string } | null {
const userInfo = uni.getStorageSync('userInfo') as { userId?: string } | null
if (!userInfo?.userId) return null
return uni.getStorageSync(`selectedStudent_${userInfo.userId}`) as { id: string, name: string, account: string, grade: string, school: string, className: string, avatar: string } | null
}
export function applyBoundStudent(student: { id: string, name: string, account: string, grade: string, school: string, className: string, avatar: string }) {
const comprehensiveLevel = homeStudentProfile.value?.id === student.id ? homeStudentProfile.value.comprehensiveLevel : 'good'
setHomeStudentProfile({ ...student, comprehensiveLevel })
} }
export function setHomeStudentMetrics(metrics: StudentHomeMetrics) { export function setHomeStudentMetrics(metrics: StudentHomeMetrics) {
+2 -2
View File
@@ -210,10 +210,10 @@ function createMockResponse<T>(config: RequestConfig): ApiResponse<T> {
const isSecondStudent = config.url.includes('studentId=test-student-002') const isSecondStudent = config.url.includes('studentId=test-student-002')
data = isSecondStudent ? { data = isSecondStudent ? {
studentId: 'test-student-002', onlineToday: true, checkedInToday: false, learningMinutesToday: 78, studentId: 'test-student-002', onlineToday: true, checkedInToday: false, learningMinutesToday: 78,
accuracyToday: 88, learningDaysThisWeek: 4, pendingHomeworkCount: 2, updatedAt: '2026-10-28T08:30:00+08:00' accuracyToday: 88, learningDaysThisWeek: 4, shouldStudyDaysThisWeek: 5, pendingHomeworkCount: 2, updatedAt: '2026-10-28T08:30:00+08:00'
} : { } : {
studentId: 'test-student-001', onlineToday: true, checkedInToday: true, learningMinutesToday: 155, studentId: 'test-student-001', onlineToday: true, checkedInToday: true, learningMinutesToday: 155,
accuracyToday: 92, learningDaysThisWeek: 5, pendingHomeworkCount: 1, updatedAt: '2026-10-28T08:30:00+08:00' accuracyToday: 92, learningDaysThisWeek: 5, shouldStudyDaysThisWeek: 5, pendingHomeworkCount: 1, updatedAt: '2026-10-28T08:30:00+08:00'
} }
} else if (config.url.includes('/api/parent/today-learning')) { } else if (config.url.includes('/api/parent/today-learning')) {
const isSecondStudent = config.url.includes('studentId=test-student-002') const isSecondStudent = config.url.includes('studentId=test-student-002')