Compare commits
2
Commits
267ef98616
...
08711b7628
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08711b7628 | ||
|
|
4a19192d73 |
+294
-33
@@ -50,10 +50,22 @@ export type StudentHomeMetrics = {
|
|||||||
accuracyToday: number | null
|
accuracyToday: number | null
|
||||||
learningDaysThisWeek: number
|
learningDaysThisWeek: number
|
||||||
shouldStudyDaysThisWeek: number
|
shouldStudyDaysThisWeek: number
|
||||||
pendingHomeworkCount: number
|
pendingPackCount: number
|
||||||
updatedAt: string
|
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 = {
|
export type TodayLearningCourse = {
|
||||||
id: string
|
id: string
|
||||||
time: string
|
time: string
|
||||||
@@ -136,10 +148,53 @@ export type StudentStudyReport = {
|
|||||||
studentClass: string
|
studentClass: string
|
||||||
updatedAtText: string
|
updatedAtText: string
|
||||||
subjects: StudyReportSubject[]
|
subjects: StudyReportSubject[]
|
||||||
|
hasWeekReport: boolean
|
||||||
|
hasMonthReport: boolean
|
||||||
week: StudyReportPeriod
|
week: StudyReportPeriod
|
||||||
month: 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 = {
|
export type TodayLearningDetail = {
|
||||||
studentId: string
|
studentId: string
|
||||||
studentName: string
|
studentName: string
|
||||||
@@ -213,9 +268,12 @@ export type LearningTaskItem = {
|
|||||||
title: string
|
title: string
|
||||||
status: 'not_started' | 'in_progress' | 'success' | 'failed'
|
status: 'not_started' | 'in_progress' | 'success' | 'failed'
|
||||||
statusText: string
|
statusText: string
|
||||||
|
resultText: string
|
||||||
accuracyText: string
|
accuracyText: string
|
||||||
answerText: string
|
answerText: string
|
||||||
|
attemptText: string
|
||||||
deadlineText: string
|
deadlineText: string
|
||||||
|
metaText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LearningTaskGroup = {
|
export type LearningTaskGroup = {
|
||||||
@@ -235,6 +293,7 @@ export type StudentLearningTasks = {
|
|||||||
studentName: string
|
studentName: string
|
||||||
studentGrade: string
|
studentGrade: string
|
||||||
studentClass: string
|
studentClass: string
|
||||||
|
totalCount: number
|
||||||
pendingCount: number
|
pendingCount: number
|
||||||
completedCount: number
|
completedCount: number
|
||||||
failedCount: number
|
failedCount: number
|
||||||
@@ -252,7 +311,8 @@ export type StudentAlert = {
|
|||||||
content: string
|
content: string
|
||||||
occurredAt: string
|
occurredAt: string
|
||||||
isRead: boolean
|
isRead: boolean
|
||||||
statusName?: string
|
isTriggered: boolean
|
||||||
|
statusName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StudentAlertList = {
|
export type StudentAlertList = {
|
||||||
@@ -274,7 +334,7 @@ export type StudentLearningWarningItem = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type StudentLearningWarningResult = {
|
export type StudentLearningWarningResult = {
|
||||||
userId: string
|
userId: number | string
|
||||||
date: string
|
date: string
|
||||||
totalCount: number
|
totalCount: number
|
||||||
triggeredCount: number
|
triggeredCount: number
|
||||||
@@ -369,7 +429,7 @@ export function apiGetBoundStudents(): Promise<ApiResponse<{ students: BoundStud
|
|||||||
* 获取指定学生的首页今日学习概览。
|
* 获取指定学生的首页今日学习概览。
|
||||||
*/
|
*/
|
||||||
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<any>(`/api/v1/ParentStudentToday/GetTodayInfo?userId=${studentId}`, undefined, { showLoading }).then((res) => {
|
return get<ParentStudentTodayInfoResult>(`/api/v1/ParentStudentToday/GetTodayInfo?userId=${encodeURIComponent(studentId)}`, undefined, { showLoading }).then((res) => {
|
||||||
if (!res || !res.data) return null
|
if (!res || !res.data) return null
|
||||||
return {
|
return {
|
||||||
code: res.code,
|
code: res.code,
|
||||||
@@ -378,11 +438,11 @@ export function apiGetStudentHomeMetrics(studentId: string, showLoading: boolean
|
|||||||
studentId: String(res.data.userId ?? studentId),
|
studentId: String(res.data.userId ?? studentId),
|
||||||
onlineToday: Boolean(res.data.todayOnline),
|
onlineToday: Boolean(res.data.todayOnline),
|
||||||
checkedInToday: Boolean(res.data.todayCheckIn),
|
checkedInToday: Boolean(res.data.todayCheckIn),
|
||||||
learningMinutesToday: Math.floor((Number(res.data.todayStudyDuration) || 0) / 60),
|
learningMinutesToday: Math.floor(Number(res.data.todayStudyDuration) || 0),
|
||||||
accuracyToday: Number(res.data.todayAccuracyRate) || 0,
|
accuracyToday: res.data.todayAccuracyRate === null || res.data.todayAccuracyRate === undefined ? null : Number(res.data.todayAccuracyRate) || 0,
|
||||||
learningDaysThisWeek: Number(res.data.weekStudyDays) || 0,
|
learningDaysThisWeek: Number(res.data.weekStudyDays) || 0,
|
||||||
shouldStudyDaysThisWeek: Number(res.data.weekShouldStudyDays) || 0,
|
shouldStudyDaysThisWeek: Number(res.data.weekShouldStudyDays) || 0,
|
||||||
pendingHomeworkCount: Number(res.data.pendingPackCount) || 0,
|
pendingPackCount: Number(res.data.pendingPackCount) || 0,
|
||||||
updatedAt: res.data.date || ''
|
updatedAt: res.data.date || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,26 +450,30 @@ export function apiGetStudentHomeMetrics(studentId: string, showLoading: boolean
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定学生今日学习预警,可选择仅返回已触发的预警规则。
|
* 获取指定学生今日学习预警。已触发预警优先,其后展示未触发规则,可按数量裁剪。
|
||||||
*/
|
*/
|
||||||
export function apiGetStudentLearningWarnings(studentId: string, showLoading: boolean = true, onlyTriggered: boolean = true): Promise<ApiResponse<StudentAlertList> | null> {
|
export function apiGetStudentLearningWarnings(studentId: string, showLoading: boolean = true, limit?: number, onlyTriggered: boolean = false): Promise<ApiResponse<StudentAlertList> | null> {
|
||||||
return get<any>(`/api/v1/ParentLearningWarning/GetTodayList?userId=${studentId}`, undefined, { showLoading }).then((res) => {
|
return get<StudentLearningWarningResult>(`/api/v1/ParentLearningWarning/GetTodayList?userId=${encodeURIComponent(studentId)}`, undefined, { showLoading }).then((res) => {
|
||||||
if (!res || !res.data) return null
|
if (!res || !res.data || !Array.isArray(res.data.items)) return null
|
||||||
const items = Array.isArray(res.data.items) ? res.data.items : []
|
const warningData = res.data
|
||||||
const list: StudentAlert[] = items
|
const triggeredItems = warningData.items.filter((item: StudentLearningWarningItem) => Boolean(item.isTriggered))
|
||||||
.filter((item: any) => !onlyTriggered || Boolean(item.isTriggered))
|
const normalItems = warningData.items.filter((item: StudentLearningWarningItem) => !Boolean(item.isTriggered))
|
||||||
.map((item: any, index: number) => {
|
const orderedItems = triggeredItems.concat(normalItems)
|
||||||
const warningType = learningWarningTypeMap[Number(item.type)]
|
const sourceItems = onlyTriggered ? triggeredItems : orderedItems
|
||||||
return {
|
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}`,
|
id: `${studentId}-${item.type ?? index}`,
|
||||||
level: warningType?.level ?? 'medium',
|
level: item.isTriggered ? warningType?.level ?? 'high' : 'low',
|
||||||
title: item.title || warningType?.title || '学习预警',
|
title: item.title || warningType?.title || '学习预警',
|
||||||
content: item.description || item.statusName || '',
|
content: item.description || item.statusName || '',
|
||||||
occurredAt: res.data.date || '',
|
occurredAt: warningData.date || '',
|
||||||
isRead: !item.isTriggered,
|
isRead: !item.isTriggered,
|
||||||
|
isTriggered: Boolean(item.isTriggered),
|
||||||
statusName: item.statusName || (item.isTriggered ? '已触发' : '正常')
|
statusName: item.statusName || (item.isTriggered ? '已触发' : '正常')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
code: res.code,
|
code: res.code,
|
||||||
message: res.message,
|
message: res.message,
|
||||||
@@ -418,8 +482,8 @@ export function apiGetStudentLearningWarnings(studentId: string, showLoading: bo
|
|||||||
pagination: {
|
pagination: {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: list.length,
|
pageSize: list.length,
|
||||||
total: list.length,
|
total: sourceItems.length,
|
||||||
hasMore: false
|
hasMore: limit !== undefined && sourceItems.length > list.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,10 +498,176 @@ export function apiGetTodayLearningDetail(studentId: string, showLoading: boolea
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取学生学情报告。接口一次返回周报、月报和全部科目趋势数据。
|
* 获取学生最新一期学情报告。新接口按报告类型分别返回周报、月报,这里转换为页面统一结构。
|
||||||
*/
|
*/
|
||||||
export function apiGetStudentStudyReport(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentStudyReport> | null> {
|
export async function apiGetStudentStudyReport(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentStudyReport> | null> {
|
||||||
return get<StudentStudyReport>(`/api/parent/study-report?studentId=${studentId}`, undefined, { showLoading })
|
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> {
|
export function apiGetStudentVocabulary(studentId: string, showLoading: boolean = true): Promise<ApiResponse<StudentVocabulary> | null> {
|
||||||
@@ -548,7 +778,7 @@ export function apiGetStudentLearningTasks(studentId: string, showLoading: boole
|
|||||||
* 获取学生当天学习包任务。Status: 0 全部,1 待完成,2 已完成,3 未通过。
|
* 获取学生当天学习包任务。Status: 0 全部,1 待完成,2 已完成,3 未通过。
|
||||||
*/
|
*/
|
||||||
export function apiGetParentStudyPacks(studentId: string, status: ParentStudyPackStatus = 0, showLoading: boolean = true): Promise<ApiResponse<StudentLearningTasks> | null> {
|
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) => {
|
return get<any>(`/api/v1/ParentStudyPack/GetTodayList?userId=${encodeURIComponent(studentId)}&status=${status}`, undefined, { showLoading }).then((res) => {
|
||||||
if (!res || !res.data) return null
|
if (!res || !res.data) return null
|
||||||
const statusMap: { [key: number]: LearningTaskItem['status'] } = {
|
const statusMap: { [key: number]: LearningTaskItem['status'] } = {
|
||||||
0: 'not_started',
|
0: 'not_started',
|
||||||
@@ -567,14 +797,38 @@ export function apiGetParentStudyPacks(studentId: string, status: ParentStudyPac
|
|||||||
const tasks: LearningTaskItem[] = (Array.isArray(course.packs) ? course.packs : []).map((pack: any) => {
|
const tasks: LearningTaskItem[] = (Array.isArray(course.packs) ? course.packs : []).map((pack: any) => {
|
||||||
const taskStatus = statusMap[Number(pack.status)] || 'not_started'
|
const taskStatus = statusMap[Number(pack.status)] || 'not_started'
|
||||||
const accuracy = pack.latestAccuracyRate === null || pack.latestAccuracyRate === undefined ? '--' : `${Number(pack.latestAccuracyRate) || 0}%`
|
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 {
|
return {
|
||||||
id: String(pack.packId ?? pack.knowledgePointId ?? ''),
|
id: String(pack.packId ?? pack.knowledgePointId ?? ''),
|
||||||
title: pack.name || '未命名学习任务',
|
title: pack.name || '未命名学习任务',
|
||||||
status: taskStatus,
|
status: taskStatus,
|
||||||
statusText: pack.statusName || statusTextMap[taskStatus],
|
statusText: pack.statusName || statusTextMap[taskStatus],
|
||||||
|
resultText,
|
||||||
accuracyText: accuracy,
|
accuracyText: accuracy,
|
||||||
answerText: pack.completedAttemptCount ? `已完成 ${pack.completedAttemptCount} 次` : '尚未完成',
|
answerText,
|
||||||
deadlineText: pack.deadlineTime || ''
|
attemptText,
|
||||||
|
deadlineText,
|
||||||
|
metaText: metaParts.join(' · ')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
@@ -598,6 +852,7 @@ export function apiGetParentStudyPacks(studentId: string, status: ParentStudyPac
|
|||||||
studentGrade: '',
|
studentGrade: '',
|
||||||
studentClass: '',
|
studentClass: '',
|
||||||
pendingCount: Number(res.data.pendingCount) || 0,
|
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,
|
completedCount: Number(res.data.completedCount) || 0,
|
||||||
failedCount: Number(res.data.failedCount) || 0,
|
failedCount: Number(res.data.failedCount) || 0,
|
||||||
groups
|
groups
|
||||||
@@ -612,10 +867,16 @@ function formatTaskTime(value: any): string {
|
|||||||
return text.length >= 16 ? text.substring(11, 16) : text
|
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)}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前学生的预警信息。首页调用时 limit 固定为 3。
|
* 兼容旧调用签名,实际统一使用今日学习预警接口。
|
||||||
*/
|
*/
|
||||||
export function apiGetStudentAlerts(studentId: string, page: number = 1, pageSize: number = 20, limit?: number, showLoading: boolean = true): Promise<ApiResponse<StudentAlertList> | null> {
|
export function apiGetStudentAlerts(studentId: string, _page: number = 1, _pageSize: number = 20, limit?: number, showLoading: boolean = true): Promise<ApiResponse<StudentAlertList> | null> {
|
||||||
const suffix = limit ? `limit=${limit}` : `page=${page}&pageSize=${pageSize}`
|
return apiGetStudentLearningWarnings(studentId, showLoading, limit)
|
||||||
return get<StudentAlertList>(`/api/parent/alerts?studentId=${studentId}&${suffix}`, undefined, { showLoading })
|
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-32
@@ -16,11 +16,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// #ifdef WEB
|
|
||||||
import { BrowserQRCodeReader } from '@zxing/browser'
|
|
||||||
import type { IScannerControls } from '@zxing/browser'
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cooldown?: number
|
cooldown?: number
|
||||||
active?: boolean
|
active?: boolean
|
||||||
@@ -39,10 +34,10 @@
|
|||||||
let lastScannedAt = 0
|
let lastScannedAt = 0
|
||||||
// #ifdef WEB
|
// #ifdef WEB
|
||||||
const videoContainerId = `qr-scanner-${Date.now()}`
|
const videoContainerId = `qr-scanner-${Date.now()}`
|
||||||
let scannerControls: IScannerControls | null = null
|
|
||||||
let scanVideo: HTMLVideoElement | null = null
|
let scanVideo: HTMLVideoElement | null = null
|
||||||
|
let scanStream: MediaStream | null = null
|
||||||
let isPageActive = false
|
let isPageActive = false
|
||||||
let restartTimer: number | null = null
|
let scanTimer: number | null = null
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
function emitScan(result: string) {
|
function emitScan(result: string) {
|
||||||
@@ -77,7 +72,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startWebScanner() {
|
async function startWebScanner() {
|
||||||
if (!isPageActive || !props.active || scannerControls != null) return
|
if (!isPageActive || !props.active || scanStream != null) return
|
||||||
const video = getScanVideo()
|
const video = getScanVideo()
|
||||||
if (video == null) {
|
if (video == null) {
|
||||||
handleCameraError()
|
handleCameraError()
|
||||||
@@ -85,39 +80,55 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reader = new BrowserQRCodeReader()
|
const BarcodeDetectorClass = (window as any).BarcodeDetector
|
||||||
scannerControls = await reader.decodeFromConstraints(
|
if (BarcodeDetectorClass == null || navigator.mediaDevices == null) {
|
||||||
{ video: { facingMode: 'environment' } },
|
handleCameraError()
|
||||||
video,
|
return
|
||||||
(result, _error, controls) => {
|
}
|
||||||
if (!result) return
|
|
||||||
controls.stop()
|
scanStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
||||||
scannerControls = null
|
video.srcObject = scanStream
|
||||||
emitScan(result.getText())
|
await video.play()
|
||||||
scheduleRestart()
|
const detector = new BarcodeDetectorClass({ formats: ['qr_code'] })
|
||||||
}
|
void scanWebFrame(detector)
|
||||||
)
|
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
|
stopWebScanner()
|
||||||
handleCameraError()
|
handleCameraError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleRestart() {
|
async function scanWebFrame(detector: any) {
|
||||||
if (!isPageActive || !props.active || restartTimer != null) return
|
if (!isPageActive || !props.active || scanVideo == null || scanStream == null) return
|
||||||
restartTimer = setTimeout(() => {
|
|
||||||
restartTimer = null
|
try {
|
||||||
void startWebScanner()
|
const results = await detector.detect(scanVideo)
|
||||||
}, props.cooldown)
|
if (results.length > 0) {
|
||||||
|
emitScan(results[0].rawValue as string)
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
handleCameraError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scanTimer = setTimeout(() => {
|
||||||
|
scanTimer = null
|
||||||
|
void scanWebFrame(detector)
|
||||||
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopWebScanner() {
|
function stopWebScanner() {
|
||||||
if (restartTimer != null) {
|
if (scanTimer != null) {
|
||||||
clearTimeout(restartTimer)
|
clearTimeout(scanTimer)
|
||||||
restartTimer = null
|
scanTimer = null
|
||||||
}
|
}
|
||||||
if (scannerControls != null) {
|
if (scanStream != null) {
|
||||||
scannerControls.stop()
|
scanStream.getTracks().forEach((track) => {
|
||||||
scannerControls = null
|
track.stop()
|
||||||
|
})
|
||||||
|
scanStream = null
|
||||||
|
}
|
||||||
|
if (scanVideo != null) {
|
||||||
|
scanVideo.srcObject = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="activity-chart"><view class="activity-scale"><text class="activity-scale-text">时长(分)</text><text class="activity-scale-text">200</text><text class="activity-scale-text">100</text><text class="activity-scale-text">0</text></view><view class="activity-columns"><view v-for="point in points" :key="point.label" class="activity-column"><view class="activity-bar-wrap"><view class="activity-grid-line activity-grid-line-top"></view><view class="activity-grid-line activity-grid-line-middle"></view><view class="activity-grid-line activity-grid-line-bottom"></view><view class="activity-planned-bar" :style="{ height: point.plannedHeight + 'px' }"></view><view class="activity-study-bar" :style="{ height: point.studyHeight + 'px' }"></view><view v-if="point.newMasteredCount > 0" class="activity-mastery-point" :style="{ bottom: point.newMasteredHeight + 'px' }"><text class="activity-mastery-count">{{ point.newMasteredCount }}</text></view></view><text class="activity-day">{{ point.label }}</text></view></view><view class="activity-mastery-scale"><text class="activity-mastery-scale-text">新增掌握(个)</text><text class="activity-mastery-scale-text">3 个</text><text class="activity-mastery-scale-text">2 个</text><text class="activity-mastery-scale-text">1 个</text><text class="activity-mastery-scale-text">0 个</text></view></view>
|
<view class="activity-chart"><view class="activity-scale"><text class="activity-scale-text">时长(分)</text><text class="activity-scale-text">200</text><text class="activity-scale-text">100</text><text class="activity-scale-text">0</text></view><view class="activity-columns"><view v-for="point in points" :key="point.label" class="activity-column"><view class="activity-bar-wrap"><view class="activity-grid-line activity-grid-line-top"></view><view class="activity-grid-line activity-grid-line-middle"></view><view class="activity-grid-line activity-grid-line-bottom"></view><view class="activity-planned-bar" :style="{ height: point.plannedHeight + 'px' }"></view><view class="activity-study-bar" :style="{ height: point.studyHeight + 'px' }"></view><view v-if="point.newMasteredCount > 0" class="activity-mastery-point" :style="{ bottom: point.newMasteredHeight + 'px' }"><text class="activity-mastery-count">{{ point.newMasteredCount }}</text></view></view><text class="activity-day">{{ point.label }}</text></view></view><view class="activity-mastery-scale"><text class="activity-mastery-scale-text">未攻克(分)</text><text class="activity-mastery-scale-text">90</text><text class="activity-mastery-scale-text">60</text><text class="activity-mastery-scale-text">30</text><text class="activity-mastery-scale-text">0</text></view></view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
Generated
+1
-48
@@ -7,54 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "app.k12.parents",
|
"name": "app.k12.parents",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC"
|
||||||
"dependencies": {
|
|
||||||
"@zxing/browser": "^0.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@zxing/browser": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.2.1.tgz",
|
|
||||||
"integrity": "sha512-92pVfVDUXbc15xu9vIEmhNyqIEASMEkDF92CwT6T+7sg2EHXJRktH9CQKoQSXe51UtbNsj+Fm09H/JBWHMs/Sw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@zxing/text-encoding": "^0.9.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@zxing/library": "^0.23.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@zxing/library": {
|
|
||||||
"version": "0.23.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.23.0.tgz",
|
|
||||||
"integrity": "sha512-6fkkoFwP8CHxl6ugnPsj74PLJgX2iRv5zczGAyt5OBzQgxFhuhF0NCEc4t4OvSr8xAv2MRLlI0Iu9ZGDZQ2urA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"ts-custom-error": "^3.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 24.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@zxing/text-encoding": "~0.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@zxing/text-encoding": {
|
|
||||||
"version": "0.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
|
||||||
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
|
|
||||||
"license": "(Unlicense OR Apache-2.0)",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/ts-custom-error": {
|
|
||||||
"version": "3.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
|
|
||||||
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -11,7 +11,5 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {}
|
||||||
"@zxing/browser": "^0.2.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-28
@@ -1,70 +1,195 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container">
|
<view class="page-container">
|
||||||
<view class="header-panel"><text class="header-title">预警信息</text><text class="header-subtitle">{{ studentName }}的学习动态</text></view>
|
<view class="header-panel">
|
||||||
|
<view class="back-button" @tap="goBack"><text class="back-icon">‹</text></view>
|
||||||
|
<view class="header-copy"><text class="header-title">学习预警</text><text class="header-subtitle">{{ studentName }}</text></view>
|
||||||
|
<view class="header-action" @tap="openStudentPicker"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view>
|
||||||
|
</view>
|
||||||
<scroll-view class="content-scroll" direction="vertical" scroll-y="true">
|
<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-if="isLoading" class="status-panel"><text class="status-text">预警信息加载中</text></view>
|
||||||
<view v-else-if="alerts.length === 0" class="status-panel"><text class="status-text">暂无预警信息</text></view>
|
<view v-else-if="alerts.length === 0" class="status-panel"><text class="status-text">暂无预警信息</text></view>
|
||||||
<view v-else v-for="alert in alerts" :key="alert.id" class="alert-card">
|
<view v-else class="content-body">
|
||||||
<view class="alert-card-top"><view class="alert-level-dot" :class="levelDotClass(alert.level)"></view><text class="alert-title">{{ alert.title }}</text><text class="alert-status" :class="alert.isRead ? 'alert-status-normal' : 'alert-status-triggered'">{{ alert.statusName }}</text></view>
|
<view class="summary-card">
|
||||||
<text class="alert-content">{{ alert.content }}</text>
|
<text class="summary-title">今日触发预警</text>
|
||||||
<text class="alert-time">{{ formatTime(alert.occurredAt) }}</text>
|
<view class="summary-count-row"><text class="summary-count">{{ triggeredCount }}</text><text class="summary-unit">条</text></view>
|
||||||
|
<text class="summary-desc">其中 {{ triggeredCount }} 条与作业截止相关,建议今晚提醒孩子提交。</text>
|
||||||
|
</view>
|
||||||
|
<view class="list-heading"><text class="list-title">全部预警规则</text><text class="list-total">共 {{ alerts.length }} 条</text></view>
|
||||||
|
<view v-for="alert in alerts" :key="alert.id" class="alert-card">
|
||||||
|
<view class="alert-card-top"><view class="alert-level-dot" :class="alert.isTriggered ? 'alert-level-triggered' : 'alert-level-normal'"></view><text class="alert-title">{{ alert.title }}</text><text class="alert-status" :class="alert.isTriggered ? 'alert-status-triggered' : 'alert-status-normal'">{{ alert.isTriggered ? '已触发' : '正常' }}</text></view>
|
||||||
|
<text class="alert-content">{{ alert.content }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
<page-container :show="showStudentPicker" position="bottom" :round="true" :overlay="true" :close-on-slide-down="true" @beforeleave="closeStudentPicker">
|
||||||
|
<view class="student-picker-panel">
|
||||||
|
<view class="student-picker-header">
|
||||||
|
<text class="student-picker-title">切换孩子</text>
|
||||||
|
<text class="student-picker-subtitle">切换后将刷新当前孩子的预警规则</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="student-picker-list" direction="vertical" scroll-y="true">
|
||||||
|
<view v-if="isStudentsLoading" 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, index) 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" :class="studentPickerAvatarClass(index)"><image v-if="item.avatar" class="student-picker-avatar-image" :src="item.avatar" mode="aspectFill"></image><text v-else class="student-picker-avatar-text">{{ studentPickerInitial(item.name) }}</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>
|
||||||
|
<text class="student-picker-action" :class="selectedStudentId === item.id ? 'student-picker-action-selected' : ''">{{ selectedStudentId === item.id ? '当前查看' : '切换' }}</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="student-picker-footer"><view class="student-picker-cancel" @tap="closeStudentPicker"><text class="student-picker-cancel-text">取消</text></view><view class="student-picker-confirm" @tap="confirmStudent"><text class="student-picker-confirm-text">确定</text></view></view>
|
||||||
|
</view>
|
||||||
|
</page-container>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { apiGetStudentLearningWarnings, type StudentAlert, type StudentAlertLevel } from '../../api/student'
|
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
|
||||||
import { homeStudentProfile } from '../../store/home'
|
import { apiGetBoundStudents, apiGetStudentLearningWarnings, type BoundStudentSummary, type StudentAlert } from '../../api/student'
|
||||||
|
import { homeStudentProfile, selectedStudentChangeVersion, switchBoundStudent } from '../../store/home'
|
||||||
|
|
||||||
const alerts = ref<StudentAlert[]>([])
|
const alerts = ref<StudentAlert[]>([])
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const isStudentsLoading = ref(false)
|
||||||
|
const showStudentPicker = ref(false)
|
||||||
|
const boundStudents = ref<BoundStudentSummary[]>([])
|
||||||
|
const selectedStudentId = ref('')
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
const studentName = computed((): string => homeStudentProfile.value?.name ?? '孩子')
|
const studentName = computed((): string => homeStudentProfile.value?.name ?? '孩子')
|
||||||
|
const triggeredCount = computed((): number => alerts.value.filter((item: StudentAlert) => item.isTriggered).length)
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
loadAlerts()
|
loadAlerts()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
loadAlerts()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadAlerts() {
|
async function loadAlerts() {
|
||||||
|
isLoading.value = true
|
||||||
const studentId = homeStudentProfile.value?.id ?? ''
|
const studentId = homeStudentProfile.value?.id ?? ''
|
||||||
if (!studentId) {
|
if (!studentId) {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await apiGetStudentLearningWarnings(studentId, true, false)
|
const res = await apiGetStudentLearningWarnings(studentId)
|
||||||
if (res && res.data) {
|
if (res && res.data) {
|
||||||
alerts.value = res.data.list
|
alerts.value = res.data.list
|
||||||
}
|
}
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function levelDotClass(level: StudentAlertLevel): string {
|
function goBack() {
|
||||||
return level === 'low' ? 'alert-level-low' : level === 'medium' ? 'alert-level-medium' : 'alert-level-high'
|
uni.navigateBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(value: string): string {
|
async function openStudentPicker() {
|
||||||
return value.length > 0 ? value.substring(5, 16).replace('T', ' ') : ''
|
showStudentPicker.value = true
|
||||||
|
selectedStudentId.value = homeStudentProfile.value?.id ?? ''
|
||||||
|
isStudentsLoading.value = true
|
||||||
|
const res = await apiGetBoundStudents()
|
||||||
|
if (res && res.data) {
|
||||||
|
boundStudents.value = res.data.students
|
||||||
|
if (!selectedStudentId.value && res.data.currentStudentId) selectedStudentId.value = res.data.currentStudentId
|
||||||
|
}
|
||||||
|
isStudentsLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStudentPicker() {
|
||||||
|
showStudentPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectStudent(studentId: string) {
|
||||||
|
selectedStudentId.value = studentId
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmStudent() {
|
||||||
|
if (!selectedStudentId.value) {
|
||||||
|
uni.showToast({ title: '请选择学生', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedStudent = boundStudents.value.find((item) => item.id === selectedStudentId.value)
|
||||||
|
if (selectedStudent) {
|
||||||
|
switchBoundStudent(selectedStudent)
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
}
|
||||||
|
closeStudentPicker()
|
||||||
|
await loadAlerts()
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerInitial(name: string): string {
|
||||||
|
return name.length > 1 ? name.substring(1, 2) : name.substring(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerAvatarClass(index: number): string {
|
||||||
|
if (index === 0) return 'student-picker-avatar-0'
|
||||||
|
if (index === 1) return 'student-picker-avatar-1'
|
||||||
|
return 'student-picker-avatar-2'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f5f7fb; }
|
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f6f7fb; }
|
||||||
.header-panel { display: flex; flex-direction: column; padding: 18px 20px; background-color: #5d3de8; }
|
.header-panel { display: flex; flex-direction: row; align-items: center; padding: 6px 4px 8px; background-color: #ffffff; }
|
||||||
.header-title { font-size: 21px; font-weight: 700; color: #ffffff; }
|
.back-button { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 18px; background-color: #eef2f7; }
|
||||||
.header-subtitle { margin-top: 6px; font-size: 13px; color: #e5ddff; }
|
.back-icon { font-size: 30px; line-height: 32px; color: #415066; }
|
||||||
.content-scroll { flex: 1; padding: 16px 20px; }
|
.header-copy { display: flex; flex-direction: column; flex: 1; margin-left: 7px; }
|
||||||
|
.header-title { font-size: 15px; font-weight: 700; color: #111827; }
|
||||||
|
.header-subtitle { margin-top: 4px; font-size: 11px; color: #7c8aa0; }
|
||||||
|
.header-action { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; margin-right: 4px; border-radius: 18px; background-color: #edf1ff; }
|
||||||
|
.header-action-icon { font-size: 18px; color: #5947ea; }
|
||||||
|
.content-scroll { flex: 1; padding: 14px 4px 8px; }
|
||||||
|
.content-body { display: flex; flex-direction: column; }
|
||||||
.status-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 160px; }
|
.status-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 160px; }
|
||||||
.status-text { font-size: 14px; color: #8992a5; }
|
.status-text { font-size: 14px; color: #8992a5; }
|
||||||
.alert-card { display: flex; flex-direction: column; margin-bottom: 12px; padding: 16px; border-radius: 14px; background-color: #ffffff; }
|
.summary-card { display: flex; flex-direction: column; padding: 15px 16px 16px; margin-bottom: 14px; border-radius: 12px; background-color: #683dec; }
|
||||||
|
.summary-title { font-size: 12px; font-weight: 600; color: #ffffff; }
|
||||||
|
.summary-count-row { display: flex; flex-direction: row; align-items: flex-end; margin-top: 9px; }
|
||||||
|
.summary-count { font-size: 26px; line-height: 30px; font-weight: 800; color: #ffffff; }
|
||||||
|
.summary-unit { margin-left: 6px; margin-bottom: 3px; font-size: 13px; font-weight: 600; color: #ffffff; }
|
||||||
|
.summary-desc { margin-top: 10px; font-size: 12px; line-height: 18px; color: #ffffff; }
|
||||||
|
.list-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin-bottom: 9px; }
|
||||||
|
.list-title { font-size: 15px; font-weight: 700; color: #121826; }
|
||||||
|
.list-total { font-size: 12px; color: #7e8ba0; }
|
||||||
|
.alert-card { display: flex; flex-direction: column; margin-bottom: 10px; padding: 15px 14px; border-radius: 13px; background-color: #ffffff; border: 1px solid #edf0f5; }
|
||||||
.alert-card-top { display: flex; flex-direction: row; align-items: center; }
|
.alert-card-top { display: flex; flex-direction: row; align-items: center; }
|
||||||
.alert-level-dot { width: 9px; height: 9px; margin-right: 8px; border-radius: 5px; }
|
.alert-level-dot { width: 7px; height: 7px; margin-right: 11px; border-radius: 4px; }
|
||||||
.alert-level-high { background-color: #e25a5a; }
|
.alert-level-triggered { background-color: #ff3f6b; }
|
||||||
.alert-level-medium { background-color: #e8a129; }
|
.alert-level-normal { background-color: #d2dce8; }
|
||||||
.alert-level-low { background-color: #4d87cf; }
|
.alert-title { flex: 1; font-size: 14px; font-weight: 700; color: #1f2a3d; }
|
||||||
.alert-title { flex: 1; font-size: 15px; font-weight: 700; color: #30384a; }
|
.alert-status { padding: 4px 8px; border-radius: 12px; font-size: 10px; }
|
||||||
.alert-status { padding: 3px 7px; border-radius: 8px; font-size: 10px; }
|
.alert-status-triggered { color: #ff3f6b; background-color: #fff0f4; }
|
||||||
.alert-status-triggered { color: #e25a5a; background-color: #fff0f1; }
|
.alert-status-normal { color: #00a86b; background-color: #eafaf3; }
|
||||||
.alert-status-normal { color: #16a979; background-color: #e8fbf3; }
|
.alert-content { margin-top: 10px; margin-left: 18px; font-size: 12px; line-height: 19px; color: #536278; }
|
||||||
.alert-content { margin-top: 10px; font-size: 13px; line-height: 20px; color: #626c7e; }
|
.student-picker-panel { display: flex; flex-direction: column; background-color: #ffffff; }
|
||||||
.alert-time { margin-top: 10px; font-size: 11px; color: #9aa2b1; }
|
.student-picker-header { display: flex; flex-direction: column; padding: 18px 20px 12px; }
|
||||||
|
.student-picker-title { font-size: 18px; font-weight: 700; color: #242b3d; }
|
||||||
|
.student-picker-subtitle { margin-top: 5px; font-size: 12px; color: #8992a5; }
|
||||||
|
.student-picker-list { max-height: 340px; padding: 0 20px; }
|
||||||
|
.student-picker-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; }
|
||||||
|
.student-picker-empty-text { font-size: 13px; color: #8992a5; }
|
||||||
|
.student-picker-item { display: flex; flex-direction: row; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid #edf0f5; border-radius: 15px; background-color: #ffffff; }
|
||||||
|
.student-picker-item-selected { background-color: #f0efff; }
|
||||||
|
.student-picker-avatar { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 14px; background-color: #7161ed; }
|
||||||
|
.student-picker-avatar-0 { background-color: #6558e9; }
|
||||||
|
.student-picker-avatar-1 { background-color: #12a9df; }
|
||||||
|
.student-picker-avatar-2 { background-color: #e63a9c; }
|
||||||
|
.student-picker-avatar-image { width: 46px; height: 46px; border-radius: 14px; }
|
||||||
|
.student-picker-avatar-text { font-size: 18px; font-weight: 700; color: #ffffff; }
|
||||||
|
.student-picker-info { display: flex; flex-direction: column; flex: 1; margin-left: 11px; }
|
||||||
|
.student-picker-name-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 5px; }
|
||||||
|
.student-picker-name { margin-right: 8px; font-size: 15px; font-weight: 700; color: #2a3042; }
|
||||||
|
.student-picker-grade { padding: 2px 6px; border-radius: 7px; font-size: 10px; color: #554be9; background-color: #e8e7ff; }
|
||||||
|
.student-picker-detail { margin-top: 2px; font-size: 11px; color: #7e8799; }
|
||||||
|
.student-picker-action { flex-shrink: 0; padding: 5px 8px; border-radius: 10px; font-size: 9px; color: #6e7c91; background-color: #f1f4f8; }
|
||||||
|
.student-picker-action-selected { color: #ffffff; background-color: #554be9; }
|
||||||
|
.student-picker-footer { display: flex; flex-direction: row; padding: 12px 20px 24px; border-top: 1px solid #edf0f5; }
|
||||||
|
.student-picker-cancel, .student-picker-confirm { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 44px; border-radius: 11px; }
|
||||||
|
.student-picker-cancel { margin-right: 10px; background-color: #f0f2f7; }
|
||||||
|
.student-picker-confirm { background-color: #554be9; }
|
||||||
|
.student-picker-cancel-text { font-size: 15px; color: #596275; }
|
||||||
|
.student-picker-confirm-text { font-size: 15px; color: #ffffff; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+10
-1
@@ -8,10 +8,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
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 { apiGetStudentGrowth, apiGetStudentHomeProfile, apiGetStudentPointRecords, type StudentGrowth } from '../../api/student'
|
import { apiGetStudentGrowth, apiGetStudentHomeProfile, apiGetStudentPointRecords, type StudentGrowth } from '../../api/student'
|
||||||
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
|
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile } from '../../store/home'
|
||||||
|
|
||||||
const growth = ref<StudentGrowth>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', pointsBalance: 0, level: 0, rank: 0, streakDays: 0, totalPoints: 0, records: [] })
|
const growth = ref<StudentGrowth>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', pointsBalance: 0, level: 0, rank: 0, streakDays: 0, totalPoints: 0, records: [] })
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
const isRefreshing = ref(false)
|
const isRefreshing = ref(false)
|
||||||
const isLoadingMore = ref(false)
|
const isLoadingMore = ref(false)
|
||||||
const hasMoreRecords = ref(true)
|
const hasMoreRecords = ref(true)
|
||||||
@@ -21,7 +22,15 @@
|
|||||||
|
|
||||||
onLoad(() => { loadGrowth() })
|
onLoad(() => { loadGrowth() })
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
loadGrowth()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadGrowth() {
|
async function loadGrowth() {
|
||||||
|
isLoading.value = true
|
||||||
let studentId = homeStudentProfile.value?.id ?? ''
|
let studentId = homeStudentProfile.value?.id ?? ''
|
||||||
if (!studentId) {
|
if (!studentId) {
|
||||||
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
||||||
|
|||||||
+59
-16
@@ -61,8 +61,8 @@
|
|||||||
<text class="metric-value metric-blue">{{ metrics.learningDaysThisWeek }}/{{ metrics.shouldStudyDaysThisWeek }} 天</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>
|
||||||
<text class="metric-value metric-orange">{{ metrics.pendingHomeworkCount }} 个</text>
|
<text class="metric-value metric-orange">{{ metrics.pendingPackCount }} 个</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -100,10 +100,10 @@
|
|||||||
<scroll-view class="student-picker-list" direction="vertical" scroll-y="true">
|
<scroll-view class="student-picker-list" direction="vertical" scroll-y="true">
|
||||||
<view v-if="isStudentsLoading" class="student-picker-empty"><text class="student-picker-empty-text">学生信息加载中</text></view>
|
<view v-if="isStudentsLoading" 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-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, index) 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" :class="studentPickerAvatarClass(index)"><image v-if="item.avatar" class="student-picker-avatar-image" :src="item.avatar" mode="aspectFill"></image><text v-else class="student-picker-avatar-text">{{ studentPickerInitial(item.name) }}</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 class="student-picker-info"><view class="student-picker-name-row"><text class="student-picker-name">{{ item.name }}</text><text class="student-picker-role">{{ studentPickerRole(index) }}</text><text class="student-picker-status">{{ studentPickerStatus(index) }}</text></view><text class="student-picker-detail">{{ item.school }} · {{ item.className }}</text></view>
|
||||||
<view v-if="selectedStudentId === item.id" class="student-picker-check"><text class="student-picker-check-text">已选</text></view>
|
<text class="student-picker-action" :class="selectedStudentId === item.id ? 'student-picker-action-selected' : ''">{{ selectedStudentId === item.id ? '当前查看' : '切换' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<view class="student-picker-footer"><view class="student-picker-cancel" @tap="closeStudentPicker"><text class="student-picker-cancel-text">取消</text></view><view class="student-picker-confirm" @tap="confirmStudent"><text class="student-picker-confirm-text">确定</text></view></view>
|
<view class="student-picker-footer"><view class="student-picker-cancel" @tap="closeStudentPicker"><text class="student-picker-cancel-text">取消</text></view><view class="student-picker-confirm" @tap="confirmStudent"><text class="student-picker-confirm-text">确定</text></view></view>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
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, apiGetStudentHomeMetrics, apiGetStudentLearningWarnings, 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 { applyBoundStudent, getSavedSelectedStudent, homeStudentAlerts, homeStudentMetrics, homeStudentProfile, saveSelectedStudent, setHomeStudentAlerts, setHomeStudentMetrics } from '../../store/home'
|
import { applyBoundStudent, getSavedSelectedStudent, homeStudentAlerts, homeStudentMetrics, homeStudentProfile, saveSelectedStudent, selectedStudentChangeVersion, setHomeStudentAlerts, setHomeStudentMetrics, switchBoundStudent } from '../../store/home'
|
||||||
import type { UserInfo } from '../../api/auth'
|
import type { UserInfo } from '../../api/auth'
|
||||||
|
|
||||||
const loadingStudent = ref<StudentHomeProfile>({
|
const loadingStudent = ref<StudentHomeProfile>({
|
||||||
@@ -134,10 +134,11 @@
|
|||||||
const isStudentsLoading = ref(false)
|
const isStudentsLoading = ref(false)
|
||||||
const boundStudents = ref<BoundStudentSummary[]>([])
|
const boundStudents = ref<BoundStudentSummary[]>([])
|
||||||
const selectedStudentId = ref('')
|
const selectedStudentId = ref('')
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
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, shouldStudyDaysThisWeek: 0, pendingHomeworkCount: 0, updatedAt: ''
|
accuracyToday: null, learningDaysThisWeek: 0, shouldStudyDaysThisWeek: 0, pendingPackCount: 0, updatedAt: ''
|
||||||
})
|
})
|
||||||
const alerts = computed((): StudentAlert[] => homeStudentAlerts.value ?? [])
|
const alerts = computed((): StudentAlert[] => homeStudentAlerts.value ?? [])
|
||||||
const parentName = computed((): string => {
|
const parentName = computed((): string => {
|
||||||
@@ -174,6 +175,24 @@
|
|||||||
loadHomeData()
|
loadHomeData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
syncSelectedStudentFromStorage()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function syncSelectedStudentFromStorage() {
|
||||||
|
const savedStudent = getSavedSelectedStudent()
|
||||||
|
if (savedStudent && savedStudent.id !== homeStudentProfile.value?.id) {
|
||||||
|
applyBoundStudent(savedStudent)
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
await loadHomeData(savedStudent.id, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
await loadHomeData(homeStudentProfile.value?.id ?? '', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -182,7 +201,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadStudentAlerts(studentId: string, showLoading: boolean = true) {
|
async function loadStudentAlerts(studentId: string, showLoading: boolean = true) {
|
||||||
const res = await apiGetStudentLearningWarnings(studentId, showLoading)
|
const res = await apiGetStudentLearningWarnings(studentId, showLoading, 3)
|
||||||
if (res && res.data) {
|
if (res && res.data) {
|
||||||
setHomeStudentAlerts(res.data.list)
|
setHomeStudentAlerts(res.data.list)
|
||||||
}
|
}
|
||||||
@@ -227,7 +246,6 @@
|
|||||||
async function openStudentPicker() {
|
async function openStudentPicker() {
|
||||||
showStudentPicker.value = true
|
showStudentPicker.value = true
|
||||||
selectedStudentId.value = student.value.id
|
selectedStudentId.value = student.value.id
|
||||||
if (boundStudents.value.length > 0) return
|
|
||||||
isStudentsLoading.value = true
|
isStudentsLoading.value = true
|
||||||
const res = await apiGetBoundStudents()
|
const res = await apiGetBoundStudents()
|
||||||
if (res && res.data) {
|
if (res && res.data) {
|
||||||
@@ -243,6 +261,26 @@
|
|||||||
selectedStudentId.value = studentId
|
selectedStudentId.value = studentId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function studentPickerInitial(name: string): string {
|
||||||
|
return name.length > 1 ? name.substring(1, 2) : name.substring(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerAvatarClass(index: number): string {
|
||||||
|
if (index === 0) return 'student-picker-avatar-0'
|
||||||
|
if (index === 1) return 'student-picker-avatar-1'
|
||||||
|
return 'student-picker-avatar-2'
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerRole(index: number): string {
|
||||||
|
if (index === 0) return '儿子'
|
||||||
|
if (index === 1) return '侄子'
|
||||||
|
return '女儿'
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerStatus(index: number): string {
|
||||||
|
return index === 1 ? '中上' : '优秀'
|
||||||
|
}
|
||||||
|
|
||||||
function alertLevelClass(level: StudentAlertLevel): string {
|
function alertLevelClass(level: StudentAlertLevel): string {
|
||||||
return level === 'low' ? 'reminder-blue' : level === 'medium' ? 'reminder-yellow' : 'reminder-red'
|
return level === 'low' ? 'reminder-blue' : level === 'medium' ? 'reminder-yellow' : 'reminder-red'
|
||||||
}
|
}
|
||||||
@@ -279,11 +317,9 @@
|
|||||||
closeStudentPicker()
|
closeStudentPicker()
|
||||||
const selectedStudent = boundStudents.value.find((item) => item.id === selectedStudentId.value)
|
const selectedStudent = boundStudents.value.find((item) => item.id === selectedStudentId.value)
|
||||||
if (selectedStudent) {
|
if (selectedStudent) {
|
||||||
saveSelectedStudent(selectedStudent)
|
switchBoundStudent(selectedStudent)
|
||||||
applyBoundStudent(selectedStudent)
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
}
|
await loadHomeData(selectedStudent.id)
|
||||||
if (selectedStudentId.value !== student.value.id) {
|
|
||||||
await loadHomeData(selectedStudentId.value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -412,18 +448,25 @@
|
|||||||
.student-picker-list { max-height: 340px; padding: 0 20px; }
|
.student-picker-list { max-height: 340px; padding: 0 20px; }
|
||||||
.student-picker-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; }
|
.student-picker-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; }
|
||||||
.student-picker-empty-text { font-size: 13px; color: #8992a5; }
|
.student-picker-empty-text { font-size: 13px; color: #8992a5; }
|
||||||
.student-picker-item { display: flex; flex-direction: row; align-items: center; padding: 14px 12px; margin-bottom: 10px; border-radius: 13px; background-color: #f7f9fc; }
|
.student-picker-item { display: flex; flex-direction: row; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid #edf0f5; border-radius: 15px; background-color: #ffffff; }
|
||||||
.student-picker-item-selected { background-color: #f0efff; }
|
.student-picker-item-selected { background-color: #f0efff; }
|
||||||
.student-picker-avatar { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 14px; background-color: #7161ed; }
|
.student-picker-avatar { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 14px; background-color: #7161ed; }
|
||||||
|
.student-picker-avatar-0 { background-color: #6558e9; }
|
||||||
|
.student-picker-avatar-1 { background-color: #12a9df; }
|
||||||
|
.student-picker-avatar-2 { background-color: #e63a9c; }
|
||||||
.student-picker-avatar-image { width: 46px; height: 46px; border-radius: 14px; }
|
.student-picker-avatar-image { width: 46px; height: 46px; border-radius: 14px; }
|
||||||
.student-picker-avatar-text { font-size: 18px; font-weight: 700; color: #ffffff; }
|
.student-picker-avatar-text { font-size: 18px; font-weight: 700; color: #ffffff; }
|
||||||
.student-picker-info { display: flex; flex-direction: column; flex: 1; margin-left: 11px; }
|
.student-picker-info { display: flex; flex-direction: column; flex: 1; margin-left: 11px; }
|
||||||
.student-picker-name-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 5px; }
|
.student-picker-name-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 5px; }
|
||||||
.student-picker-name { margin-right: 8px; font-size: 15px; font-weight: 700; color: #2a3042; }
|
.student-picker-name { margin-right: 8px; font-size: 15px; font-weight: 700; color: #2a3042; }
|
||||||
.student-picker-grade { padding: 2px 6px; border-radius: 7px; font-size: 10px; color: #554be9; background-color: #e8e7ff; }
|
.student-picker-grade { padding: 2px 6px; border-radius: 7px; font-size: 10px; color: #554be9; background-color: #e8e7ff; }
|
||||||
|
.student-picker-role { margin-left: 7px; padding: 2px 6px; border-radius: 8px; font-size: 9px; color: #7c8ba1; background-color: #f1f4f8; }
|
||||||
|
.student-picker-status { margin-left: 7px; padding: 2px 6px; border-radius: 8px; font-size: 9px; color: #16a979; background-color: #e7faf2; }
|
||||||
.student-picker-detail { margin-top: 2px; font-size: 11px; color: #7e8799; }
|
.student-picker-detail { margin-top: 2px; font-size: 11px; color: #7e8799; }
|
||||||
.student-picker-check { padding: 4px 7px; border-radius: 9px; background-color: #554be9; }
|
.student-picker-check { padding: 4px 7px; border-radius: 9px; background-color: #554be9; }
|
||||||
.student-picker-check-text { font-size: 10px; color: #ffffff; }
|
.student-picker-check-text { font-size: 10px; color: #ffffff; }
|
||||||
|
.student-picker-action { flex-shrink: 0; padding: 5px 8px; border-radius: 10px; font-size: 9px; color: #6e7c91; background-color: #f1f4f8; }
|
||||||
|
.student-picker-action-selected { color: #ffffff; background-color: #554be9; }
|
||||||
.student-picker-footer { display: flex; flex-direction: row; padding: 12px 20px calc(12px + var(--uni-safe-area-inset-bottom)); border-top: 1px solid #edf0f5; }
|
.student-picker-footer { display: flex; flex-direction: row; padding: 12px 20px calc(12px + var(--uni-safe-area-inset-bottom)); border-top: 1px solid #edf0f5; }
|
||||||
.student-picker-cancel, .student-picker-confirm { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 44px; border-radius: 11px; }
|
.student-picker-cancel, .student-picker-confirm { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 44px; border-radius: 11px; }
|
||||||
.student-picker-cancel { margin-right: 10px; background-color: #f0f2f7; }
|
.student-picker-cancel { margin-right: 10px; background-color: #f0f2f7; }
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
import { apiLogin } from '../../api/auth'
|
import { apiLogin } from '../../api/auth'
|
||||||
import { apiGetBoundStudents, type BoundStudentSummary } from '../../api/student'
|
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'
|
import { switchBoundStudent } from '../../store/home'
|
||||||
|
|
||||||
type LoginForm = {
|
type LoginForm = {
|
||||||
phone: string
|
phone: string
|
||||||
@@ -144,8 +144,7 @@
|
|||||||
? res.data.students.find((item) => item.id === savedStudent.id)!
|
? res.data.students.find((item) => item.id === savedStudent.id)!
|
||||||
: res.data.students[0]
|
: res.data.students[0]
|
||||||
|
|
||||||
saveSelectedStudent(selectedStudent)
|
switchBoundStudent(selectedStudent)
|
||||||
applyBoundStudent(selectedStudent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 { applyBoundStudent, homeStudentProfile, saveSelectedStudent } from '../../store/home'
|
import { homeStudentProfile, selectedStudentChangeVersion, switchBoundStudent } 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'
|
||||||
|
|
||||||
@@ -72,9 +72,17 @@
|
|||||||
const selectedStudentId = ref(homeStudentProfile.value?.id ?? '')
|
const selectedStudentId = ref(homeStudentProfile.value?.id ?? '')
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const listFailed = ref(false)
|
const listFailed = ref(false)
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
|
|
||||||
onLoad(() => { loadStudents() })
|
onLoad(() => { loadStudents() })
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
selectedStudentId.value = homeStudentProfile.value?.id ?? selectedStudentId.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadStudents() {
|
async function loadStudents() {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
listFailed.value = false
|
listFailed.value = false
|
||||||
@@ -92,8 +100,8 @@
|
|||||||
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) {
|
if (student) {
|
||||||
saveSelectedStudent(student)
|
switchBoundStudent(student)
|
||||||
applyBoundStudent(student)
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+124
-10
@@ -2,7 +2,7 @@
|
|||||||
<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">{{ homeStudentProfile?.name ?? report.studentName }} · {{ homeStudentProfile?.school ?? '' }} {{ homeStudentProfile?.className ?? 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" @tap="openStudentPicker"><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>
|
||||||
|
|
||||||
@@ -14,16 +14,38 @@
|
|||||||
<scroll-view class="subject-scroll" direction="horizontal" scroll-x="true"><view class="subject-tabs"><view v-for="subject in report.subjects" :key="subject.id" class="subject-tab" :class="activeSubjectId === subject.id ? 'subject-tab-active' : ''" @tap="setSubject(subject.id)"><text class="subject-tab-text" :class="activeSubjectId === subject.id ? 'subject-tab-text-active' : ''">{{ subject.name }}</text></view></view></scroll-view>
|
<scroll-view class="subject-scroll" direction="horizontal" scroll-x="true"><view class="subject-tabs"><view v-for="subject in report.subjects" :key="subject.id" class="subject-tab" :class="activeSubjectId === subject.id ? 'subject-tab-active' : ''" @tap="setSubject(subject.id)"><text class="subject-tab-text" :class="activeSubjectId === subject.id ? 'subject-tab-text-active' : ''">{{ subject.name }}</text></view></view></scroll-view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="summary-card"><text class="summary-title">{{ activeReport.statusTitle }}</text><text class="summary-description">{{ activeReport.statusDescription }}</text><view class="summary-stats"><view class="summary-stat"><text class="summary-value">{{ activeReport.newMasteredPoints }} 个</text><text class="summary-label">新攻克知识点</text></view><view class="summary-stat summary-stat-middle"><text class="summary-value">{{ activeReport.consecutiveDays }} 天</text><text class="summary-label">连续学习</text></view><view class="summary-stat"><text class="summary-value">{{ activeReport.averageAccuracy }}%</text><text class="summary-label">平均正确率</text></view></view></view>
|
<view v-if="!activeHasReport" class="empty-report-card"><text class="empty-report-title">暂无{{ reportPeriodLabel }}报告</text><text class="empty-report-text">当前周期还没有生成学情报告,生成后会在这里展示学习时长、知识点攻克和正确率数据。</text></view>
|
||||||
|
<view v-else class="report-content">
|
||||||
|
<view class="summary-card"><text class="summary-title">{{ activeReport.statusTitle }}</text><text class="summary-description">{{ activeReport.statusDescription }}</text><view class="summary-stats"><view class="summary-stat"><text class="summary-value">{{ activeReport.newMasteredPoints }} 个</text><text class="summary-label">攻克知识点</text></view><view class="summary-stat summary-stat-middle"><text class="summary-value">{{ activeReport.consecutiveDays }} 天</text><text class="summary-label">连续学习</text></view><view class="summary-stat"><text class="summary-value">{{ activeReport.averageAccuracy }}%</text><text class="summary-label">平均正确率</text></view></view></view>
|
||||||
|
|
||||||
<view class="trend-card"><text class="section-title">已掌握知识点成长曲线</text><view class="trend-legend"><view class="legend-item"><view class="legend-mark legend-mark-student"></view><text class="legend-text">本学生</text></view><view class="legend-item"><view class="legend-mark legend-mark-average"></view><text class="legend-text">班级平均</text></view></view><StudyTrendChart :scale-labels="activeReport.scaleLabels" :points="activeTrend"></StudyTrendChart></view>
|
<view class="trend-card"><text class="section-title">学习时长趋势</text><view class="trend-legend"><view class="legend-item"><view class="legend-mark legend-mark-student"></view><text class="legend-text">总学习时长</text></view><view class="legend-item"><view class="legend-mark legend-mark-average"></view><text class="legend-text">攻克成功时长</text></view></view><StudyTrendChart :scale-labels="activeReport.scaleLabels" :points="activeTrend"></StudyTrendChart></view>
|
||||||
|
|
||||||
<view class="activity-card"><text class="section-title">{{ reportPeriodLabel }}学习活跃度与效率</text><text class="activity-summary">{{ activeReport.activity.summary }}</text><text class="activity-total">学习 {{ activeReport.activity.activeDays }} 天,累计 {{ activeReport.activity.totalDurationText }}</text><StudyActivityChart :points="activeReport.activity.points"></StudyActivityChart><view class="activity-legend"><view class="legend-item activity-legend-item"><view class="legend-mark legend-mark-student"></view><text class="legend-text">高质量(有产出)</text></view><view class="legend-item activity-legend-item"><view class="legend-mark legend-mark-plan"></view><text class="legend-text">普通学习(无新产出)</text></view><view class="legend-item activity-legend-item"><view class="legend-mark legend-mark-mastery"></view><text class="legend-text">新增掌握</text></view></view></view>
|
<view class="activity-card"><text class="section-title">{{ reportPeriodLabel }}学习时长明细</text><text class="activity-summary">{{ activeReport.activity.summary }}</text><text class="activity-total">连续学习 {{ activeReport.activity.activeDays }} 天,累计 {{ activeReport.activity.totalDurationText }}</text><StudyActivityChart :points="activeReport.activity.points"></StudyActivityChart><view class="activity-legend"><view class="legend-item activity-legend-item"><view class="legend-mark legend-mark-student"></view><text class="legend-text">总学习时长</text></view><view class="legend-item activity-legend-item"><view class="legend-mark legend-mark-plan"></view><text class="legend-text">攻克成功时长</text></view><view class="legend-item activity-legend-item"><view class="legend-mark legend-mark-mastery"></view><text class="legend-text">未攻克时长</text></view></view></view>
|
||||||
|
|
||||||
<view v-if="activeSubjectId === 'all'" class="mastery-card"><text class="section-title">各科知识点掌握情况</text><view v-for="mastery in activeReport.subjectMasteries" :key="mastery.subjectName" class="mastery-item"><view class="mastery-heading"><text class="mastery-subject">{{ mastery.subjectName }}</text><text class="mastery-value">{{ mastery.masteredCount }} / {{ mastery.totalCount }}</text></view><view class="mastery-track"><view class="mastery-progress" :style="{ width: mastery.masteredPercent + '%' }"></view><view class="mastery-remaining"></view></view></view><view class="mastery-legend"><view class="mastery-legend-item"><view class="mastery-legend-color mastery-legend-mastered"></view><text class="mastery-legend-text">已掌握</text></view><view class="mastery-legend-item"><view class="mastery-legend-color mastery-legend-unmastered"></view><text class="mastery-legend-text">未掌握</text></view></view></view>
|
<view v-if="activeSubjectId === '0'" class="mastery-card"><text class="section-title">各科知识点攻克情况</text><view v-for="mastery in activeReport.subjectMasteries" :key="mastery.subjectName" class="mastery-item"><view class="mastery-heading"><text class="mastery-subject">{{ mastery.subjectName }}</text><text class="mastery-value">{{ mastery.masteredCount }} / {{ mastery.totalCount }}</text></view><view class="mastery-track"><view class="mastery-progress" :style="{ width: mastery.masteredPercent + '%' }"></view><view class="mastery-remaining"></view></view></view><view class="mastery-legend"><view class="mastery-legend-item"><view class="mastery-legend-color mastery-legend-mastered"></view><text class="mastery-legend-text">已攻克</text></view><view class="mastery-legend-item"><view class="mastery-legend-color mastery-legend-unmastered"></view><text class="mastery-legend-text">未攻克</text></view></view></view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<MainTabBar active-tab="study"></MainTabBar>
|
<MainTabBar active-tab="study"></MainTabBar>
|
||||||
|
|
||||||
|
<page-container :show="showStudentPicker" position="bottom" :round="true" :overlay="true" :close-on-slide-down="true" @beforeleave="closeStudentPicker">
|
||||||
|
<view class="student-picker-panel">
|
||||||
|
<view class="student-picker-header">
|
||||||
|
<text class="student-picker-title">切换孩子</text>
|
||||||
|
<text class="student-picker-subtitle">切换后将刷新当前孩子的学情报告</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="student-picker-list" direction="vertical" scroll-y="true">
|
||||||
|
<view v-if="isStudentsLoading" 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, index) 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" :class="studentPickerAvatarClass(index)"><image v-if="item.avatar" class="student-picker-avatar-image" :src="item.avatar" mode="aspectFill"></image><text v-else class="student-picker-avatar-text">{{ studentPickerInitial(item.name) }}</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>
|
||||||
|
<text class="student-picker-action" :class="selectedStudentId === item.id ? 'student-picker-action-selected' : ''">{{ selectedStudentId === item.id ? '当前查看' : '切换' }}</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="student-picker-footer"><view class="student-picker-cancel" @tap="closeStudentPicker"><text class="student-picker-cancel-text">取消</text></view><view class="student-picker-confirm" @tap="confirmStudent"><text class="student-picker-confirm-text">确定</text></view></view>
|
||||||
|
</view>
|
||||||
|
</page-container>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -32,18 +54,24 @@
|
|||||||
import StudyActivityChart from '../../components/study-activity-chart.uvue'
|
import StudyActivityChart from '../../components/study-activity-chart.uvue'
|
||||||
import StudyTrendChart from '../../components/study-trend-chart.uvue'
|
import StudyTrendChart from '../../components/study-trend-chart.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, apiGetStudentStudyReport, type StudentStudyReport, type StudyReportActivity, type StudyReportPeriod, type StudyReportSubjectMastery, type StudyReportTrendPoint } from '../../api/student'
|
import { apiGetBoundStudents, apiGetStudentHomeProfile, apiGetStudentStudyReport, type BoundStudentSummary, type StudentStudyReport, type StudyReportActivity, type StudyReportPeriod, type StudyReportSubjectMastery, type StudyReportTrendPoint } from '../../api/student'
|
||||||
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
|
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile, switchBoundStudent } from '../../store/home'
|
||||||
|
|
||||||
const emptyActivity: StudyReportActivity = { summary: '', activeDays: 0, totalDurationText: '', points: [] }
|
const emptyActivity: StudyReportActivity = { summary: '', activeDays: 0, totalDurationText: '', points: [] }
|
||||||
const emptyPeriod: StudyReportPeriod = { rangeText: '', statusTitle: '', statusDescription: '', newMasteredPoints: 0, consecutiveDays: 0, averageAccuracy: 0, scaleLabels: [], trends: [], activity: emptyActivity, subjectMasteries: [] as StudyReportSubjectMastery[] }
|
const emptyPeriod: StudyReportPeriod = { rangeText: '', statusTitle: '', statusDescription: '', newMasteredPoints: 0, consecutiveDays: 0, averageAccuracy: 0, scaleLabels: [], trends: [], activity: emptyActivity, subjectMasteries: [] as StudyReportSubjectMastery[] }
|
||||||
const report = ref<StudentStudyReport>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', updatedAtText: '', subjects: [], week: emptyPeriod, month: emptyPeriod })
|
const report = ref<StudentStudyReport>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', updatedAtText: '', subjects: [{ id: '0', name: '全部科目' }], hasWeekReport: false, hasMonthReport: false, week: emptyPeriod, month: emptyPeriod })
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const isRefreshing = ref(false)
|
const isRefreshing = ref(false)
|
||||||
|
const isStudentsLoading = ref(false)
|
||||||
|
const showStudentPicker = ref(false)
|
||||||
|
const boundStudents = ref<BoundStudentSummary[]>([])
|
||||||
|
const selectedStudentId = ref('')
|
||||||
const activePeriod = ref('week')
|
const activePeriod = ref('week')
|
||||||
const activeSubjectId = ref('all')
|
const activeSubjectId = ref('0')
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
const activeReport = computed((): StudyReportPeriod => activePeriod.value === 'week' ? report.value.week : report.value.month)
|
const activeReport = computed((): StudyReportPeriod => activePeriod.value === 'week' ? report.value.week : report.value.month)
|
||||||
const reportPeriodLabel = computed((): string => activePeriod.value === 'week' ? '本周' : '本月')
|
const activeHasReport = computed((): boolean => activePeriod.value === 'week' ? report.value.hasWeekReport : report.value.hasMonthReport)
|
||||||
|
const reportPeriodLabel = computed((): string => activePeriod.value === 'week' ? '周报' : '月报')
|
||||||
const activeTrend = computed((): StudyReportTrendPoint[] => {
|
const activeTrend = computed((): StudyReportTrendPoint[] => {
|
||||||
for (let index = 0; index < activeReport.value.trends.length; index++) {
|
for (let index = 0; index < activeReport.value.trends.length; index++) {
|
||||||
const trend = activeReport.value.trends[index]
|
const trend = activeReport.value.trends[index]
|
||||||
@@ -56,7 +84,16 @@
|
|||||||
loadReport()
|
loadReport()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
loadReport()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadReport() {
|
async function loadReport() {
|
||||||
|
isLoading.value = true
|
||||||
|
activeSubjectId.value = '0'
|
||||||
let studentId = homeStudentProfile.value?.id ?? ''
|
let studentId = homeStudentProfile.value?.id ?? ''
|
||||||
if (!studentId) {
|
if (!studentId) {
|
||||||
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
||||||
@@ -79,12 +116,57 @@
|
|||||||
|
|
||||||
function setPeriod(period: string) {
|
function setPeriod(period: string) {
|
||||||
activePeriod.value = period
|
activePeriod.value = period
|
||||||
|
activeSubjectId.value = '0'
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSubject(subjectId: string) {
|
function setSubject(subjectId: string) {
|
||||||
activeSubjectId.value = subjectId
|
activeSubjectId.value = subjectId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openStudentPicker() {
|
||||||
|
showStudentPicker.value = true
|
||||||
|
selectedStudentId.value = homeStudentProfile.value?.id ?? ''
|
||||||
|
isStudentsLoading.value = true
|
||||||
|
const res = await apiGetBoundStudents()
|
||||||
|
if (res && res.data) {
|
||||||
|
boundStudents.value = res.data.students
|
||||||
|
if (!selectedStudentId.value && res.data.currentStudentId) selectedStudentId.value = res.data.currentStudentId
|
||||||
|
}
|
||||||
|
isStudentsLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStudentPicker() {
|
||||||
|
showStudentPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectStudent(studentId: string) {
|
||||||
|
selectedStudentId.value = studentId
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmStudent() {
|
||||||
|
if (!selectedStudentId.value) {
|
||||||
|
uni.showToast({ title: '请选择学生', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedStudent = boundStudents.value.find((item) => item.id === selectedStudentId.value)
|
||||||
|
if (selectedStudent) {
|
||||||
|
switchBoundStudent(selectedStudent)
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
}
|
||||||
|
closeStudentPicker()
|
||||||
|
await loadReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerInitial(name: string): string {
|
||||||
|
return name.length > 1 ? name.substring(1, 2) : name.substring(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerAvatarClass(index: number): string {
|
||||||
|
if (index === 0) return 'student-picker-avatar-0'
|
||||||
|
if (index === 1) return 'student-picker-avatar-1'
|
||||||
|
return 'student-picker-avatar-2'
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -104,6 +186,10 @@
|
|||||||
.status-text { font-size: 13px; color: #8992a5; }
|
.status-text { font-size: 13px; color: #8992a5; }
|
||||||
.content-panel { display: flex; flex-direction: column; padding: 12px 18px 24px; }
|
.content-panel { display: flex; flex-direction: column; padding: 12px 18px 24px; }
|
||||||
.filters-card { display: flex; flex-direction: column; padding: 12px; border-radius: 8px; background-color: #ffffff; }
|
.filters-card { display: flex; flex-direction: column; padding: 12px; border-radius: 8px; background-color: #ffffff; }
|
||||||
|
.report-content { display: flex; flex-direction: column; }
|
||||||
|
.empty-report-card { display: flex; flex-direction: column; align-items: center; justify-content: center; margin-top: 16px; padding: 38px 18px; border-radius: 14px; background-color: #ffffff; }
|
||||||
|
.empty-report-title { font-size: 16px; font-weight: 700; color: #242b3d; }
|
||||||
|
.empty-report-text { margin-top: 8px; font-size: 12px; line-height: 19px; color: #8992a5; text-align: center; }
|
||||||
.period-tabs { display: flex; flex-direction: row; }
|
.period-tabs { display: flex; flex-direction: row; }
|
||||||
.period-tab { padding: 7px 14px; margin-right: 8px; border-radius: 16px; background-color: #f1f4f8; }
|
.period-tab { padding: 7px 14px; margin-right: 8px; border-radius: 16px; background-color: #f1f4f8; }
|
||||||
.period-tab-active { background-color: #554be9; }
|
.period-tab-active { background-color: #554be9; }
|
||||||
@@ -152,4 +238,32 @@
|
|||||||
.mastery-track { display: flex; flex-direction: row; height: 10px; margin-top: 7px; overflow: hidden; border-radius: 5px; background-color: #f1f4f8; }
|
.mastery-track { display: flex; flex-direction: row; height: 10px; margin-top: 7px; overflow: hidden; border-radius: 5px; background-color: #f1f4f8; }
|
||||||
.mastery-progress { height: 10px; border-radius: 5px 0 0 5px; background-color: #16bd88; }
|
.mastery-progress { height: 10px; border-radius: 5px 0 0 5px; background-color: #16bd88; }
|
||||||
.mastery-remaining { flex: 1; border-radius: 0 5px 5px 0; background-color: #f34d54; }
|
.mastery-remaining { flex: 1; border-radius: 0 5px 5px 0; background-color: #f34d54; }
|
||||||
|
.student-picker-panel { display: flex; flex-direction: column; background-color: #ffffff; }
|
||||||
|
.student-picker-header { display: flex; flex-direction: column; padding: 18px 20px 12px; }
|
||||||
|
.student-picker-title { font-size: 18px; font-weight: 700; color: #242b3d; }
|
||||||
|
.student-picker-subtitle { margin-top: 5px; font-size: 12px; color: #8992a5; }
|
||||||
|
.student-picker-list { max-height: 340px; padding: 0 20px; }
|
||||||
|
.student-picker-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; }
|
||||||
|
.student-picker-empty-text { font-size: 13px; color: #8992a5; }
|
||||||
|
.student-picker-item { display: flex; flex-direction: row; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid #edf0f5; border-radius: 15px; background-color: #ffffff; }
|
||||||
|
.student-picker-item-selected { background-color: #f0efff; }
|
||||||
|
.student-picker-avatar { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 14px; background-color: #7161ed; }
|
||||||
|
.student-picker-avatar-0 { background-color: #6558e9; }
|
||||||
|
.student-picker-avatar-1 { background-color: #12a9df; }
|
||||||
|
.student-picker-avatar-2 { background-color: #e63a9c; }
|
||||||
|
.student-picker-avatar-image { width: 46px; height: 46px; border-radius: 14px; }
|
||||||
|
.student-picker-avatar-text { font-size: 18px; font-weight: 700; color: #ffffff; }
|
||||||
|
.student-picker-info { display: flex; flex-direction: column; flex: 1; margin-left: 11px; }
|
||||||
|
.student-picker-name-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 5px; }
|
||||||
|
.student-picker-name { margin-right: 8px; font-size: 15px; font-weight: 700; color: #2a3042; }
|
||||||
|
.student-picker-grade { padding: 2px 6px; border-radius: 7px; font-size: 10px; color: #554be9; background-color: #e8e7ff; }
|
||||||
|
.student-picker-detail { margin-top: 2px; font-size: 11px; color: #7e8799; }
|
||||||
|
.student-picker-action { flex-shrink: 0; padding: 5px 8px; border-radius: 10px; font-size: 9px; color: #6e7c91; background-color: #f1f4f8; }
|
||||||
|
.student-picker-action-selected { color: #ffffff; background-color: #554be9; }
|
||||||
|
.student-picker-footer { display: flex; flex-direction: row; padding: 12px 20px 24px; border-top: 1px solid #edf0f5; }
|
||||||
|
.student-picker-cancel, .student-picker-confirm { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 44px; border-radius: 11px; }
|
||||||
|
.student-picker-cancel { margin-right: 10px; background-color: #f0f2f7; }
|
||||||
|
.student-picker-confirm { background-color: #554be9; }
|
||||||
|
.student-picker-cancel-text { font-size: 15px; color: #596275; }
|
||||||
|
.student-picker-confirm-text { font-size: 15px; color: #ffffff; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+222
-52
@@ -1,100 +1,270 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container"><view class="header-panel"><view class="header-copy"><text class="header-title">作业任务</text><text class="header-subtitle">{{ tasks.studentName }} · 待完成 {{ tasks.pendingCount }} 个</text></view><view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view></view><view class="filter-panel"><view v-for="filter in filters" :key="filter.id" class="filter-tab" :class="activeFilter === filter.id ? 'filter-tab-active' : ''" @tap="setFilter(filter.id)"><text class="filter-text" :class="activeFilter === filter.id ? 'filter-text-active' : ''">{{ filter.name }}</text></view></view><scroll-view class="content-scroll" direction="vertical" scroll-y="true" refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="refreshTasks"><view v-if="isLoading" class="status-panel"><text class="status-text">任务加载中</text></view><view v-else class="content-panel"><view v-for="group in visibleGroups" :key="group.id" class="group-card"><view class="group-heading"><view class="group-tags"><text class="subject-tag">{{ group.subject }}</text><text class="unit-tag">{{ group.unit }}</text><text class="lesson-title">{{ group.lesson }}</text></view><text class="knowledge-count">{{ group.knowledgeCount > 0 ? group.knowledgeCount + ' 个知识点' : '暂无学习内容' }}</text></view><text class="lesson-time">● {{ group.startText }}-{{ group.endText }}</text><view v-if="group.note" class="group-note"><text class="group-note-icon">i</text><text class="group-note-text">{{ group.note }}</text></view><view v-for="task in group.tasks" :key="task.id" v-show="matchesFilter(task)" class="detail-task"><view class="detail-task-heading"><text class="detail-task-title">{{ task.title }}</text><text class="detail-task-status" :class="'detail-task-status-' + task.status">{{ task.statusText }}</text></view><text class="detail-task-result" :class="'detail-task-result-' + task.status">● {{ task.statusText }} · 共 {{ task.status === 'success' ? '2' : '3' }} 次</text><text v-if="task.accuracyText" class="detail-task-meta">{{ task.accuracyText }} · {{ task.answerText }} · {{ task.deadlineText }}</text></view></view><view v-if="visibleGroups.length === 0" class="empty-panel"><text class="empty-text">暂无对应任务</text></view></view></scroll-view><MainTabBar active-tab="tasks"></MainTabBar></view>
|
<view class="page-container">
|
||||||
|
<view class="header-panel">
|
||||||
|
<view class="header-copy">
|
||||||
|
<text class="header-title">作业任务</text>
|
||||||
|
<text class="header-subtitle">{{ headerStudentName }} · 待完成 {{ tasks.pendingCount }} 个</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-action" @tap="openStudentPicker"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-panel">
|
||||||
|
<view v-for="filter in filters" :key="filter.id" class="filter-tab" :class="activeFilter === filter.id ? 'filter-tab-active' : ''" @tap="setFilter(filter.id)">
|
||||||
|
<text class="filter-text" :class="activeFilter === filter.id ? 'filter-text-active' : ''">{{ filter.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="content-scroll" direction="vertical" scroll-y="true" refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="refreshTasks">
|
||||||
|
<view v-if="isLoading" class="status-panel"><text class="status-text">任务加载中</text></view>
|
||||||
|
<view v-else-if="visibleGroups.length === 0" class="empty-panel"><text class="empty-text">暂无任务</text></view>
|
||||||
|
<view v-else class="content-panel">
|
||||||
|
<view v-for="group in visibleGroups" :key="group.id" class="group-card">
|
||||||
|
<view class="group-heading">
|
||||||
|
<view class="group-title-row">
|
||||||
|
<text class="subject-tag">{{ group.subject || '学科' }}</text>
|
||||||
|
<text class="unit-tag">{{ group.unit || '学习' }}</text>
|
||||||
|
<text class="lesson-title">{{ group.lesson }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="knowledge-count">{{ group.knowledgeCount }} 个知识点</text>
|
||||||
|
</view>
|
||||||
|
<view class="lesson-time-row"><view class="time-dot"></view><text class="lesson-time">今天 {{ group.startText }}—{{ group.endText }} 开课</text></view>
|
||||||
|
|
||||||
|
<view v-if="group.tasks.length === 0" class="task-empty"><text class="task-empty-text">暂无学习包</text></view>
|
||||||
|
<view v-for="task in group.tasks" :key="task.id" v-show="matchesFilter(task)" class="task-card">
|
||||||
|
<view class="task-heading"><text class="task-title">{{ task.title }}</text><text class="task-status" :class="taskStatusClass(task)">{{ task.statusText }}</text></view>
|
||||||
|
<view class="task-result-row"><view class="task-result-dot" :class="taskResultDotClass(task)"></view><text class="task-result" :class="taskResultClass(task)">{{ task.resultText }}</text></view>
|
||||||
|
<text class="task-attempt">{{ task.attemptText }}</text>
|
||||||
|
<view class="task-divider"></view>
|
||||||
|
<text class="task-meta">{{ task.metaText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<MainTabBar active-tab="tasks"></MainTabBar>
|
||||||
|
|
||||||
|
<page-container :show="showStudentPicker" position="bottom" :round="true" :overlay="true" :close-on-slide-down="true" @beforeleave="closeStudentPicker">
|
||||||
|
<view class="student-picker-panel">
|
||||||
|
<view class="student-picker-header">
|
||||||
|
<text class="student-picker-title">切换孩子</text>
|
||||||
|
<text class="student-picker-subtitle">切换后将刷新当前孩子的作业任务</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="student-picker-list" direction="vertical" scroll-y="true">
|
||||||
|
<view v-if="isStudentsLoading" 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, index) 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" :class="studentPickerAvatarClass(index)"><image v-if="item.avatar" class="student-picker-avatar-image" :src="item.avatar" mode="aspectFill"></image><text v-else class="student-picker-avatar-text">{{ studentPickerInitial(item.name) }}</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>
|
||||||
|
<text class="student-picker-action" :class="selectedStudentId === item.id ? 'student-picker-action-selected' : ''">{{ selectedStudentId === item.id ? '当前查看' : '切换' }}</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="student-picker-footer"><view class="student-picker-cancel" @tap="closeStudentPicker"><text class="student-picker-cancel-text">取消</text></view><view class="student-picker-confirm" @tap="confirmStudent"><text class="student-picker-confirm-text">确定</text></view></view>
|
||||||
|
</view>
|
||||||
|
</page-container>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 { apiGetParentStudyPacks, apiGetStudentHomeProfile, type LearningTaskItem, type ParentStudyPackStatus, type StudentLearningTasks } from '../../api/student'
|
import { apiGetBoundStudents, apiGetParentStudyPacks, apiGetStudentHomeProfile, type BoundStudentSummary, type LearningTaskItem, type ParentStudyPackStatus, type StudentLearningTasks } from '../../api/student'
|
||||||
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
|
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile, switchBoundStudent } 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 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 isStudentsLoading = ref(false)
|
||||||
|
const showStudentPicker = ref(false)
|
||||||
|
const boundStudents = ref<BoundStudentSummary[]>([])
|
||||||
|
const selectedStudentId = ref('')
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
|
const tasks = ref<StudentLearningTasks>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', totalCount: 0, 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 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() })
|
||||||
|
|
||||||
async function loadTasks() {
|
onShow(() => {
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
loadTasks(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadTasks(showLoading: boolean = true) {
|
||||||
|
isLoading.value = showLoading
|
||||||
let studentId = homeStudentProfile.value?.id ?? ''
|
let studentId = homeStudentProfile.value?.id ?? ''
|
||||||
if (!studentId) {
|
if (!studentId) {
|
||||||
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) {
|
if (studentId) {
|
||||||
const res = await apiGetParentStudyPacks(studentId, activeStatus.value)
|
const res = await apiGetParentStudyPacks(studentId, activeStatus.value, showLoading)
|
||||||
if (res && res.data) {
|
if (res && res.data) tasks.value = 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
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTasks() {
|
async function refreshTasks() {
|
||||||
isRefreshing.value = true
|
isRefreshing.value = true
|
||||||
await loadTasks()
|
await loadTasks(false)
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setFilter(filter: string) {
|
async function setFilter(filter: string) {
|
||||||
|
if (activeFilter.value === filter) return
|
||||||
activeFilter.value = filter
|
activeFilter.value = filter
|
||||||
await loadTasks()
|
await loadTasks(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
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'
|
||||||
if (activeFilter.value === 'completed') return task.status === 'success'
|
if (activeFilter.value === 'completed') return task.status === 'success'
|
||||||
return task.status === 'failed'
|
return task.status === 'failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function taskStatusClass(task: LearningTaskItem): string {
|
||||||
|
return `task-status-${task.status}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskResultClass(task: LearningTaskItem): string {
|
||||||
|
return `task-result-${task.status}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskResultDotClass(task: LearningTaskItem): string {
|
||||||
|
return `task-result-dot-${task.status}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openStudentPicker() {
|
||||||
|
showStudentPicker.value = true
|
||||||
|
selectedStudentId.value = homeStudentProfile.value?.id ?? ''
|
||||||
|
isStudentsLoading.value = true
|
||||||
|
const res = await apiGetBoundStudents()
|
||||||
|
if (res && res.data) {
|
||||||
|
boundStudents.value = res.data.students
|
||||||
|
if (!selectedStudentId.value && res.data.currentStudentId) selectedStudentId.value = res.data.currentStudentId
|
||||||
|
}
|
||||||
|
isStudentsLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStudentPicker() {
|
||||||
|
showStudentPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectStudent(studentId: string) {
|
||||||
|
selectedStudentId.value = studentId
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmStudent() {
|
||||||
|
if (!selectedStudentId.value) {
|
||||||
|
uni.showToast({ title: '请选择学生', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedStudent = boundStudents.value.find((item) => item.id === selectedStudentId.value)
|
||||||
|
if (selectedStudent) {
|
||||||
|
switchBoundStudent(selectedStudent)
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
}
|
||||||
|
closeStudentPicker()
|
||||||
|
await loadTasks(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerInitial(name: string): string {
|
||||||
|
return name.length > 1 ? name.substring(1, 2) : name.substring(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function studentPickerAvatarClass(index: number): string {
|
||||||
|
if (index === 0) return 'student-picker-avatar-0'
|
||||||
|
if (index === 1) return 'student-picker-avatar-1'
|
||||||
|
return 'student-picker-avatar-2'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f5f7fb; }
|
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f5f7fb; }
|
||||||
.header-panel { display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 16px 20px; background-color: #ffffff; border-bottom: 1px solid #edf0f5; }
|
.header-panel { display: flex; flex-direction: row; align-items: center; justify-content: space-between; padding: 4px 8px 10px; background-color: #ffffff; }
|
||||||
.header-copy { display: flex; flex-direction: column; }
|
.header-copy { display: flex; flex-direction: column; }
|
||||||
.header-title { font-size: 18px; font-weight: 700; color: #242b3d; }
|
.header-title { font-size: 17px; font-weight: 800; color: #101827; }
|
||||||
.header-subtitle { margin-top: 4px; font-size: 11px; color: #8992a5; }
|
.header-subtitle { margin-top: 5px; font-size: 11px; color: #7e8da3; }
|
||||||
.header-action { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 18px; background-color: #f0f1ff; }
|
.header-action { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 18px; background-color: #edf1ff; }
|
||||||
.header-action-icon { font-size: 18px; color: #554be9; }
|
.header-action-icon { font-size: 18px; color: #5947ea; }
|
||||||
.filter-panel { display: flex; flex-direction: row; padding: 12px 20px; background-color: #ffffff; }
|
.filter-panel { display: flex; flex-direction: row; padding: 10px 8px 12px; background-color: #ffffff; }
|
||||||
.filter-tab { display: flex; flex-direction: column; align-items: center; flex: 1; margin-right: 8px; padding: 7px 0; border-radius: 16px; text-align: center; background-color: #f1f4f8; }
|
.filter-tab { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 28px; margin-right: 8px; border-radius: 16px; background-color: #eef2f7; }
|
||||||
.filter-tab-active { background-color: #554be9; }
|
.filter-tab-active { background-color: #5a45e8; }
|
||||||
.filter-text { font-size: 11px; color: #64748b; }
|
.filter-text { font-size: 11px; color: #4e5b70; }
|
||||||
.filter-text-active { color: #ffffff; }
|
.filter-text-active { color: #ffffff; }
|
||||||
.content-scroll { flex: 1; }
|
.content-scroll { flex: 1; }
|
||||||
.content-panel { display: flex; flex-direction: column; padding: 14px 16px 24px; }
|
.content-panel { display: flex; flex-direction: column; padding: 14px 8px 24px; }
|
||||||
.status-panel, .empty-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
|
.status-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
|
||||||
.status-text, .empty-text { font-size: 13px; color: #8992a5; }
|
.empty-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
|
||||||
.group-card { display: flex; flex-direction: column; margin-bottom: 12px; padding: 16px; border-radius: 15px; background-color: #ffffff; }
|
.status-text { font-size: 13px; color: #8992a5; }
|
||||||
|
.empty-text { font-size: 13px; color: #8992a5; }
|
||||||
|
.group-card { display: flex; flex-direction: column; margin-bottom: 12px; padding: 16px 14px 14px; border-radius: 14px; background-color: #ffffff; border: 1px solid #edf0f5; }
|
||||||
.group-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
|
.group-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
.group-tags { display: flex; flex-direction: row; align-items: center; flex: 1; }
|
.group-title-row { display: flex; flex-direction: row; align-items: center; flex: 1; min-width: 0; }
|
||||||
.subject-tag, .unit-tag { padding: 3px 7px; margin-right: 6px; border-radius: 8px; font-size: 10px; color: #554be9; background-color: #eeefff; }
|
.subject-tag { padding: 3px 7px; margin-right: 6px; border-radius: 9px; font-size: 10px; font-weight: 700; color: #5947ea; background-color: #eeefff; }
|
||||||
.unit-tag { color: #1598dc; background-color: #e6f5ff; }
|
.unit-tag { padding: 3px 7px; margin-right: 8px; border-radius: 9px; font-size: 10px; font-weight: 700; color: #2f6bea; background-color: #edf5ff; }
|
||||||
.lesson-title { font-size: 13px; font-weight: 700; color: #242b3d; }
|
.lesson-title { flex: 1; font-size: 14px; font-weight: 800; color: #1f2937; }
|
||||||
.knowledge-count { flex-shrink: 0; font-size: 10px; color: #8992a5; }
|
.knowledge-count { flex-shrink: 0; font-size: 11px; color: #8a99ae; }
|
||||||
.lesson-time { margin-top: 7px; font-size: 11px; color: #8ca0bd; }
|
.lesson-time-row { display: flex; flex-direction: row; align-items: center; margin-top: 8px; }
|
||||||
.group-note { display: flex; flex-direction: row; align-items: flex-start; margin-top: 12px; padding: 12px; border-radius: 12px; background-color: #f6f8fb; }
|
.time-dot { width: 8px; height: 8px; margin-right: 6px; border-radius: 4px; background-color: #9caec4; }
|
||||||
.group-note-icon { width: 12px; height: 12px; margin-right: 7px; border-radius: 6px; text-align: center; font-size: 9px; color: #ffffff; background-color: #9aa8bc; }
|
.lesson-time { font-size: 11px; color: #71849d; }
|
||||||
.group-note-text { flex: 1; font-size: 11px; line-height: 18px; color: #68778b; }
|
.task-empty { display: flex; flex-direction: column; align-items: center; padding: 18px 0; }
|
||||||
.detail-task { display: flex; flex-direction: column; margin-top: 10px; padding: 12px; border: 1px solid #edf0f5; border-radius: 12px; background-color: #f9fbfd; }
|
.task-empty-text { font-size: 12px; color: #8992a5; }
|
||||||
.detail-task-heading { display: flex; flex-direction: row; justify-content: space-between; }
|
.task-card { display: flex; flex-direction: column; margin-top: 13px; padding: 12px; border-radius: 12px; background-color: #f8fafc; border: 1px solid #edf1f6; }
|
||||||
.detail-task-title { font-size: 13px; color: #242b3d; }
|
.task-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
.detail-task-status { padding: 3px 7px; border-radius: 8px; font-size: 10px; }
|
.task-title { flex: 1; font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||||
.detail-task-status-success { color: #16a979; background-color: #e8fbf3; }
|
.task-status { flex-shrink: 0; padding: 4px 8px; border-radius: 11px; font-size: 10px; }
|
||||||
.detail-task-status-in_progress { color: #5147e9; background-color: #eeefff; }
|
.task-status-not_started { color: #516175; background-color: #edf2f7; }
|
||||||
.detail-task-status-failed { color: #f34d54; background-color: #fff0f1; }
|
.task-status-in_progress { color: #5947ea; background-color: #eeefff; }
|
||||||
.detail-task-result { margin-top: 8px; font-size: 11px; }
|
.task-status-success { color: #04a86b; background-color: #e8fbf2; }
|
||||||
.detail-task-result-success { color: #16a979; }
|
.task-status-failed { color: #ef4d62; background-color: #fff0f3; }
|
||||||
.detail-task-result-in_progress { color: #5147e9; }
|
.task-result-row { display: flex; flex-direction: row; align-items: center; margin-top: 9px; }
|
||||||
.detail-task-result-failed { color: #f34d54; }
|
.task-result-dot { width: 9px; height: 9px; margin-right: 7px; border-radius: 5px; }
|
||||||
.detail-task-meta { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e4eaf1; font-size: 10px; color: #8992a5; }
|
.task-result-dot-not_started { background-color: #b6c2d0; }
|
||||||
|
.task-result-dot-in_progress { background-color: #5947ea; }
|
||||||
|
.task-result-dot-success { background-color: #04a86b; }
|
||||||
|
.task-result-dot-failed { background-color: #ef4d62; }
|
||||||
|
.task-result { font-size: 12px; font-weight: 700; }
|
||||||
|
.task-result-not_started { color: #64748b; }
|
||||||
|
.task-result-in_progress { color: #5947ea; }
|
||||||
|
.task-result-success { color: #04a86b; }
|
||||||
|
.task-result-failed { color: #ef4d62; }
|
||||||
|
.task-attempt { margin-top: 6px; font-size: 11px; color: #71849d; }
|
||||||
|
.task-divider { height: 1px; margin-top: 11px; background-color: #e3e9f1; }
|
||||||
|
.task-meta { margin-top: 9px; font-size: 11px; color: #71849d; }
|
||||||
|
.student-picker-panel { display: flex; flex-direction: column; background-color: #ffffff; }
|
||||||
|
.student-picker-header { display: flex; flex-direction: column; padding: 18px 20px 12px; }
|
||||||
|
.student-picker-title { font-size: 18px; font-weight: 700; color: #242b3d; }
|
||||||
|
.student-picker-subtitle { margin-top: 5px; font-size: 12px; color: #8992a5; }
|
||||||
|
.student-picker-list { max-height: 340px; padding: 0 20px; }
|
||||||
|
.student-picker-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 150px; }
|
||||||
|
.student-picker-empty-text { font-size: 13px; color: #8992a5; }
|
||||||
|
.student-picker-item { display: flex; flex-direction: row; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid #edf0f5; border-radius: 15px; background-color: #ffffff; }
|
||||||
|
.student-picker-item-selected { background-color: #f0efff; }
|
||||||
|
.student-picker-avatar { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 14px; background-color: #7161ed; }
|
||||||
|
.student-picker-avatar-0 { background-color: #6558e9; }
|
||||||
|
.student-picker-avatar-1 { background-color: #12a9df; }
|
||||||
|
.student-picker-avatar-2 { background-color: #e63a9c; }
|
||||||
|
.student-picker-avatar-image { width: 46px; height: 46px; border-radius: 14px; }
|
||||||
|
.student-picker-avatar-text { font-size: 18px; font-weight: 700; color: #ffffff; }
|
||||||
|
.student-picker-info { display: flex; flex-direction: column; flex: 1; margin-left: 11px; }
|
||||||
|
.student-picker-name-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 5px; }
|
||||||
|
.student-picker-name { margin-right: 8px; font-size: 15px; font-weight: 700; color: #2a3042; }
|
||||||
|
.student-picker-grade { padding: 2px 6px; border-radius: 7px; font-size: 10px; color: #554be9; background-color: #e8e7ff; }
|
||||||
|
.student-picker-detail { margin-top: 2px; font-size: 11px; color: #7e8799; }
|
||||||
|
.student-picker-action { flex-shrink: 0; padding: 5px 8px; border-radius: 10px; font-size: 9px; color: #6e7c91; background-color: #f1f4f8; }
|
||||||
|
.student-picker-action-selected { color: #ffffff; background-color: #554be9; }
|
||||||
|
.student-picker-footer { display: flex; flex-direction: row; padding: 12px 20px 24px; border-top: 1px solid #edf0f5; }
|
||||||
|
.student-picker-cancel, .student-picker-confirm { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; height: 44px; border-radius: 11px; }
|
||||||
|
.student-picker-cancel { margin-right: 10px; background-color: #f0f2f7; }
|
||||||
|
.student-picker-confirm { background-color: #554be9; }
|
||||||
|
.student-picker-cancel-text { font-size: 15px; color: #596275; }
|
||||||
|
.student-picker-confirm-text { font-size: 15px; color: #ffffff; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
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 { apiGetTodayLearningDetail, type TodayLearningDetail } from '../../api/student'
|
import { apiGetTodayLearningDetail, type TodayLearningDetail } from '../../api/student'
|
||||||
import { homeStudentProfile } from '../../store/home'
|
import { homeStudentProfile, selectedStudentChangeVersion } from '../../store/home'
|
||||||
|
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const detail = ref<TodayLearningDetail>({
|
const detail = ref<TodayLearningDetail>({
|
||||||
@@ -64,12 +64,21 @@
|
|||||||
answerCount: 0, completedPackages: 0, totalPackages: 0, masteredPoints: 0, courses: [],
|
answerCount: 0, completedPackages: 0, totalPackages: 0, masteredPoints: 0, courses: [],
|
||||||
score: { accuracy: 0, yesterdayAccuracy: 0, change: 0 }, pointsTotal: 0, pointDetails: []
|
score: { accuracy: 0, yesterdayAccuracy: 0, change: 0 }, pointsTotal: 0, pointDetails: []
|
||||||
})
|
})
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
loadTodayLearning()
|
loadTodayLearning()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
loadTodayLearning()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadTodayLearning() {
|
async function loadTodayLearning() {
|
||||||
|
isLoading.value = true
|
||||||
const studentId = homeStudentProfile.value?.id ?? ''
|
const studentId = homeStudentProfile.value?.id ?? ''
|
||||||
if (!studentId) {
|
if (!studentId) {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
|||||||
@@ -8,14 +8,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
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, apiGetStudentVocabulary, type StudentVocabulary, type VocabularyBook } from '../../api/student'
|
import { apiGetStudentHomeProfile, apiGetStudentVocabulary, type StudentVocabulary, type VocabularyBook } from '../../api/student'
|
||||||
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
|
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile } from '../../store/home'
|
||||||
|
|
||||||
const vocabulary = ref<StudentVocabulary>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', bookCount: 0, books: [] })
|
const vocabulary = ref<StudentVocabulary>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', bookCount: 0, books: [] })
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||||
|
|
||||||
onLoad(() => { loadVocabulary() })
|
onLoad(() => { loadVocabulary() })
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||||
|
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||||
|
loadVocabulary()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadVocabulary() {
|
async function loadVocabulary() {
|
||||||
|
isLoading.value = true
|
||||||
let studentId = homeStudentProfile.value?.id ?? ''
|
let studentId = homeStudentProfile.value?.id ?? ''
|
||||||
if (!studentId) {
|
if (!studentId) {
|
||||||
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
||||||
|
|||||||
+13
-2
@@ -1,9 +1,12 @@
|
|||||||
import type { StudentAlert, StudentHomeMetrics, StudentHomeProfile } from '../api/student'
|
import type { StudentAlert, StudentHomeMetrics, StudentHomeProfile } from '../api/student'
|
||||||
|
|
||||||
|
type BoundStudentLike = { id: string, name: string, account: string, grade: string, school: string, className: string, avatar: string }
|
||||||
|
|
||||||
// 仅在本次登录会话内缓存首页学生资料,切换一级页面时不重复请求。
|
// 仅在本次登录会话内缓存首页学生资料,切换一级页面时不重复请求。
|
||||||
export const homeStudentProfile = ref<StudentHomeProfile | null>(null)
|
export const homeStudentProfile = ref<StudentHomeProfile | null>(null)
|
||||||
export const homeStudentMetrics = ref<StudentHomeMetrics | null>(null)
|
export const homeStudentMetrics = ref<StudentHomeMetrics | null>(null)
|
||||||
export const homeStudentAlerts = ref<StudentAlert[] | null>(null)
|
export const homeStudentAlerts = ref<StudentAlert[] | null>(null)
|
||||||
|
export const selectedStudentChangeVersion = ref(0)
|
||||||
|
|
||||||
export function setHomeStudentProfile(profile: StudentHomeProfile) {
|
export function setHomeStudentProfile(profile: StudentHomeProfile) {
|
||||||
const userInfo = uni.getStorageSync('userInfo') as { userId?: string } | null
|
const userInfo = uni.getStorageSync('userInfo') as { userId?: string } | null
|
||||||
@@ -23,7 +26,7 @@ export function setHomeStudentProfile(profile: StudentHomeProfile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveSelectedStudent(student: { id: string, name: string, account: string, grade: string, school: string, className: string, avatar: string }) {
|
export function saveSelectedStudent(student: BoundStudentLike) {
|
||||||
const userInfo = uni.getStorageSync('userInfo') as { userId?: string } | null
|
const userInfo = uni.getStorageSync('userInfo') as { userId?: string } | null
|
||||||
if (userInfo?.userId) uni.setStorageSync(`selectedStudent_${userInfo.userId}`, student)
|
if (userInfo?.userId) uni.setStorageSync(`selectedStudent_${userInfo.userId}`, student)
|
||||||
}
|
}
|
||||||
@@ -34,11 +37,19 @@ export function getSavedSelectedStudent(): { id: string, name: string, account:
|
|||||||
return uni.getStorageSync(`selectedStudent_${userInfo.userId}`) as { id: string, name: string, account: string, grade: string, school: string, className: string, avatar: string } | 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 }) {
|
export function applyBoundStudent(student: BoundStudentLike) {
|
||||||
const comprehensiveLevel = homeStudentProfile.value?.id === student.id ? homeStudentProfile.value.comprehensiveLevel : 'good'
|
const comprehensiveLevel = homeStudentProfile.value?.id === student.id ? homeStudentProfile.value.comprehensiveLevel : 'good'
|
||||||
setHomeStudentProfile({ ...student, comprehensiveLevel })
|
setHomeStudentProfile({ ...student, comprehensiveLevel })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function switchBoundStudent(student: BoundStudentLike) {
|
||||||
|
saveSelectedStudent(student)
|
||||||
|
applyBoundStudent(student)
|
||||||
|
homeStudentMetrics.value = null
|
||||||
|
homeStudentAlerts.value = null
|
||||||
|
selectedStudentChangeVersion.value++
|
||||||
|
}
|
||||||
|
|
||||||
export function setHomeStudentMetrics(metrics: StudentHomeMetrics) {
|
export function setHomeStudentMetrics(metrics: StudentHomeMetrics) {
|
||||||
homeStudentMetrics.value = metrics
|
homeStudentMetrics.value = metrics
|
||||||
}
|
}
|
||||||
|
|||||||
+251
-2
@@ -21,6 +21,8 @@ export type RequestConfig = {
|
|||||||
|
|
||||||
const BASE_URL = 'https://localhost:7145'
|
const BASE_URL = 'https://localhost:7145'
|
||||||
const TIMEOUT = 10000
|
const TIMEOUT = 10000
|
||||||
|
const USE_MOCK_API = true
|
||||||
|
const MOCK_RESPONSE_MAX_DELAY = 2000
|
||||||
let loadingCount = 0
|
let loadingCount = 0
|
||||||
|
|
||||||
function showLoading(text: string = '加载中...') {
|
function showLoading(text: string = '加载中...') {
|
||||||
@@ -54,7 +56,10 @@ function requestInterceptor(config: RequestConfig): RequestConfig {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseInterceptor<T>(response: UniNamespace.RequestSuccessCallbackResult, config: RequestConfig): ApiResponse<T> | null {
|
/**
|
||||||
|
* 响应拦截器 - 统一处理响应
|
||||||
|
*/
|
||||||
|
function responseInterceptor<T>(response: any, config: RequestConfig): ApiResponse<T> | null {
|
||||||
if (config.showLoading !== false) hideLoading()
|
if (config.showLoading !== false) hideLoading()
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
@@ -62,7 +67,14 @@ function responseInterceptor<T>(response: UniNamespace.RequestSuccessCallbackRes
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = response.data as ApiResponse<T>
|
// 解析响应数据,兼容后端 PascalCase 与前端 camelCase 两种包装格式。
|
||||||
|
const rawResult = response.data as any
|
||||||
|
const result: ApiResponse<T> = {
|
||||||
|
code: Number(rawResult?.code ?? rawResult?.Code),
|
||||||
|
message: String(rawResult?.message ?? rawResult?.Message ?? ''),
|
||||||
|
data: (rawResult?.data ?? rawResult?.Data ?? null) as T | null
|
||||||
|
}
|
||||||
|
|
||||||
if (result.code !== 200) {
|
if (result.code !== 200) {
|
||||||
handleError(result.code, result.message, config.showError)
|
handleError(result.code, result.message, config.showError)
|
||||||
return null
|
return null
|
||||||
@@ -93,9 +105,246 @@ function handleError(code: number, message: string, showError: boolean = true) {
|
|||||||
uni.showToast({ title: errorMsg, icon: 'none', duration: 2000 })
|
uni.showToast({ title: errorMsg, icon: 'none', duration: 2000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据接口生成页面流程所需的测试数据。
|
||||||
|
*/
|
||||||
|
function createMockResponse<T>(config: RequestConfig): ApiResponse<T> {
|
||||||
|
const requestData = (config.data ?? {}) as Record<string, any>
|
||||||
|
let data: any = null
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.url.includes('/api/auth/login') ||
|
||||||
|
config.url.includes('/api/auth/login-by-code') ||
|
||||||
|
config.url.includes('/api/auth/register')
|
||||||
|
) {
|
||||||
|
data = {
|
||||||
|
userId: 'test-parent-001',
|
||||||
|
phone: requestData.phone ?? '13800000000',
|
||||||
|
nickname: requestData.parentName ?? '测试家长',
|
||||||
|
avatar: '',
|
||||||
|
token: 'test-token',
|
||||||
|
refreshToken: 'test-refresh-token'
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/auth/send-code')) {
|
||||||
|
data = { expireTime: 300 }
|
||||||
|
} else if (config.url.includes('/api/auth/forgot-password/verify')) {
|
||||||
|
data = { token: 'test-reset-token' }
|
||||||
|
} else if (config.url.includes('/api/auth/refresh-token')) {
|
||||||
|
data = { token: 'test-token', refreshToken: 'test-refresh-token' }
|
||||||
|
} else if (
|
||||||
|
config.url.includes('/api/student/verify-qrcode') ||
|
||||||
|
config.url.includes('/api/student/verify-account')
|
||||||
|
) {
|
||||||
|
const studentAccount = requestData.account ?? 'test-student'
|
||||||
|
data = {
|
||||||
|
id: `test-student-${studentAccount}`,
|
||||||
|
name: requestData.name ?? '测试学生',
|
||||||
|
account: studentAccount,
|
||||||
|
avatar: '',
|
||||||
|
grade: '三年级',
|
||||||
|
class: '一班'
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/check-phone')) {
|
||||||
|
data = { exists: false }
|
||||||
|
} else if (config.url.includes('/api/parent/students')) {
|
||||||
|
data = {
|
||||||
|
currentStudentId: 'test-student-001',
|
||||||
|
students: [
|
||||||
|
{ id: 'test-student-001', name: '张一诺', account: '20241001', grade: '初二', school: '实验中学', avatar: '' },
|
||||||
|
{ id: 'test-student-002', name: '张一鸣', account: '20241002', grade: '五年级', school: '实验小学', avatar: '' },
|
||||||
|
{ id: 'test-student-003', name: '张一宁', account: '20241003', grade: '高二', school: '实验中学', avatar: '' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/home-metrics')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
data = isSecondStudent ? {
|
||||||
|
studentId: 'test-student-002', onlineToday: true, checkedInToday: false, learningMinutesToday: 78,
|
||||||
|
accuracyToday: 88, learningDaysThisWeek: 4, shouldStudyDaysThisWeek: 5, pendingPackCount: 2, updatedAt: '2026-10-28T08:30:00+08:00'
|
||||||
|
} : {
|
||||||
|
studentId: 'test-student-001', onlineToday: true, checkedInToday: true, learningMinutesToday: 155,
|
||||||
|
accuracyToday: 92, learningDaysThisWeek: 5, shouldStudyDaysThisWeek: 5, pendingPackCount: 1, updatedAt: '2026-10-28T08:30:00+08:00'
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/today-learning')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
data = isSecondStudent ? {
|
||||||
|
studentId: 'test-student-002', studentName: '张一鸣', dateText: '10月13日 周一', checkedIn: false,
|
||||||
|
learningMinutes: 78, answerMinutes: 56, answerCount: 42, completedPackages: 3, totalPackages: 6, masteredPoints: 8,
|
||||||
|
score: { accuracy: 84, yesterdayAccuracy: 81, change: 3 }, pointsTotal: 28,
|
||||||
|
pointDetails: [
|
||||||
|
{ id: 'point-201', label: '每日登录', points: 10 },
|
||||||
|
{ id: 'point-202', label: '每日提交作业', points: 8 },
|
||||||
|
{ id: 'point-203', label: '连续打卡', points: 6 },
|
||||||
|
{ id: 'point-204', label: '攻克知识点', points: 4 }
|
||||||
|
],
|
||||||
|
courses: [
|
||||||
|
{ id: 'course-201', time: '08:30', subject: '语文', content: '古诗词诵读与默写', checkedIn: true },
|
||||||
|
{ id: 'course-202', time: '10:00', subject: '数学', content: '分数应用题练习', checkedIn: true },
|
||||||
|
{ id: 'course-203', time: '15:30', subject: '英语', content: 'Unit 4 单词听写', checkedIn: false }
|
||||||
|
]
|
||||||
|
} : {
|
||||||
|
studentId: 'test-student-001', studentName: '张一诺', dateText: '10月13日 周一', checkedIn: true,
|
||||||
|
learningMinutes: 155, answerMinutes: 118, answerCount: 68, completedPackages: 5, totalPackages: 6, masteredPoints: 12,
|
||||||
|
score: { accuracy: 89, yesterdayAccuracy: 85, change: 4 }, pointsTotal: 42,
|
||||||
|
pointDetails: [
|
||||||
|
{ id: 'point-101', label: '每日登录', points: 10 },
|
||||||
|
{ id: 'point-102', label: '每日提交作业', points: 12 },
|
||||||
|
{ id: 'point-103', label: '连续打卡', points: 8 },
|
||||||
|
{ id: 'point-104', label: '攻克知识点', points: 12 }
|
||||||
|
],
|
||||||
|
courses: [
|
||||||
|
{ id: 'course-101', time: '08:00', subject: '语文', content: '《背影》精读与批注', checkedIn: true },
|
||||||
|
{ id: 'course-102', time: '10:20', subject: '数学', content: '一元二次方程专项训练', checkedIn: true },
|
||||||
|
{ id: 'course-103', time: '14:00', subject: '英语', content: 'Unit 5 单词听写与跟读', checkedIn: true },
|
||||||
|
{ id: 'course-104', time: '19:30', subject: '物理', content: '声现象章节测试', checkedIn: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/vocabulary')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
data = isSecondStudent ? {
|
||||||
|
studentId: 'test-student-002', studentName: '张一鸣', studentGrade: '五年级', studentClass: '二班', bookCount: 4,
|
||||||
|
books: [
|
||||||
|
{ id: 'vocab-201', name: '人教版八年级上册', subtitle: '总词量 620 词', totalWords: 620, learnedWords: 420, status: 'learning', statusText: '在学', levelText: '初中同步', color: 'green' },
|
||||||
|
{ id: 'vocab-202', name: '中考核心词汇 1600', subtitle: '总词量 1600 词', totalWords: 1600, learnedWords: 280, status: 'unfinished', statusText: '未学完', levelText: '中考冲刺', color: 'yellow' },
|
||||||
|
{ id: 'vocab-203', name: '人教版三年级起点·五年级上', subtitle: '总词量 380 词', totalWords: 380, learnedWords: 380, status: 'completed', statusText: '已学完', levelText: '小学基础', color: 'blue' },
|
||||||
|
{ id: 'vocab-204', name: '高中必修 5 核心词汇', subtitle: '总词量 1080 词', totalWords: 1080, learnedWords: 0, status: 'not_started', statusText: '未开始', levelText: '高中同步', color: 'gray' }
|
||||||
|
]
|
||||||
|
} : {
|
||||||
|
studentId: 'test-student-001', studentName: '张一诺', studentGrade: '初二', studentClass: '3班', bookCount: 4,
|
||||||
|
books: [
|
||||||
|
{ id: 'vocab-101', name: '人教版八年级上册', subtitle: '总词量 620 词', totalWords: 620, learnedWords: 520, status: 'learning', statusText: '在学', levelText: '初中同步', color: 'green' },
|
||||||
|
{ id: 'vocab-102', name: '中考核心词汇 1600', subtitle: '总词量 1600 词', totalWords: 1600, learnedWords: 860, status: 'unfinished', statusText: '未学完', levelText: '中考冲刺', color: 'yellow' },
|
||||||
|
{ id: 'vocab-103', name: '人教版三年级起点·五年级上', subtitle: '总词量 380 词', totalWords: 380, learnedWords: 380, status: 'completed', statusText: '已学完', levelText: '小学基础', color: 'blue' },
|
||||||
|
{ id: 'vocab-104', name: '高中必修 5 核心词汇', subtitle: '总词量 1080 词', totalWords: 1080, learnedWords: 0, status: 'not_started', statusText: '未开始', levelText: '高中同步', color: 'gray' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/learning-tasks')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
data = isSecondStudent ? {
|
||||||
|
studentId: 'test-student-002', studentName: '张一鸣', studentGrade: '五年级', studentClass: '二班', pendingCount: 2, completedCount: 3, failedCount: 0,
|
||||||
|
groups: [
|
||||||
|
{ id: 'task-group-201', subject: '生物', unit: '预习', lesson: '第 3 课节', startText: '今天 14:00', endText: '14:40 开课', knowledgeCount: 0, note: '学生尚未进入课程,暂无学习句与知识点,进入课程后将自动生成', tasks: [] },
|
||||||
|
{ id: 'task-group-202', subject: '物理', unit: '复习', lesson: '第 4 课节', startText: '今天 16:00', endText: '16:40 开课', knowledgeCount: 3, note: '', tasks: [{ id: 'task-201', title: '声音的特性', status: 'success', statusText: '攻克成功', accuracyText: '正确率 92%', answerText: '答题时长 6 分钟', deadlineText: '今天 16:35 完成' }, { id: 'task-202', title: '回声与测距', status: 'in_progress', statusText: '攻克中', accuracyText: '正确率 78%', answerText: '答题时长 12 分钟', deadlineText: '今晚 21:00 截止' }, { id: 'task-203', title: '噪声的防治', status: 'failed', statusText: '攻克失败', accuracyText: '', answerText: '', deadlineText: '' }] }
|
||||||
|
]
|
||||||
|
} : {
|
||||||
|
studentId: 'test-student-001', studentName: '张一诺', studentGrade: '初二', studentClass: '3班', pendingCount: 3, completedCount: 5, failedCount: 1,
|
||||||
|
groups: [{ id: 'task-group-101', subject: '生物', unit: '预习', lesson: '第 3 课节', startText: '今天 14:00', endText: '14:40 开课', knowledgeCount: 0, note: '学生尚未进入课程,暂无学习句与知识点,进入课程后将自动生成', tasks: [] }, { id: 'task-group-102', subject: '物理', unit: '复习', lesson: '第 4 课节', startText: '今天 16:00', endText: '16:40 开课', knowledgeCount: 3, note: '', tasks: [{ id: 'task-101', title: '声音的特性', status: 'success', statusText: '攻克成功', accuracyText: '正确率 92%', answerText: '答题时长 6 分钟', deadlineText: '今天 16:35 完成' }, { id: 'task-102', title: '回声与测距', status: 'in_progress', statusText: '攻克中', accuracyText: '正确率 78%', answerText: '答题时长 12 分钟', deadlineText: '今晚 21:00 截止' }, { id: 'task-103', title: '噪声的防治', status: 'failed', statusText: '攻克失败', accuracyText: '', answerText: '', deadlineText: '' }] }]
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/growth')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
data = isSecondStudent ? {
|
||||||
|
studentId: 'test-student-002', studentName: '张一鸣', studentGrade: '五年级', studentClass: '二班', pointsBalance: 862, level: 4, rank: 128, streakDays: 3, totalPoints: 24,
|
||||||
|
records: [
|
||||||
|
{ id: 'growth-201', title: '每日登录', timeText: '今天 07:35', points: 10, icon: 'calendar' },
|
||||||
|
{ id: 'growth-202', title: '每日提交作业', timeText: '今天 19:22', points: 8, icon: 'assignment' },
|
||||||
|
{ id: 'growth-203', title: '连续打卡', timeText: '今天 07:40', points: 6, icon: 'streak' },
|
||||||
|
{ id: 'growth-204', title: '正确率≥80%', timeText: '昨天 20:05', points: 10, icon: 'accuracy' },
|
||||||
|
{ id: 'growth-205', title: '攻克知识点', timeText: '昨天 19:40', points: 8, icon: 'mastery' },
|
||||||
|
{ id: 'growth-206', title: '上传笔记', timeText: '10月12日 18:20', points: 5, icon: 'note' }
|
||||||
|
]
|
||||||
|
} : {
|
||||||
|
studentId: 'test-student-001', studentName: '张一诺', studentGrade: '初二', studentClass: '3班', pointsBalance: 1268, level: 7, rank: 86, streakDays: 3, totalPoints: 42,
|
||||||
|
records: [
|
||||||
|
{ id: 'growth-101', title: '每日登录', timeText: '今天 07:35', points: 10, icon: 'calendar' },
|
||||||
|
{ id: 'growth-102', title: '每日提交作业', timeText: '今天 19:22', points: 12, icon: 'assignment' },
|
||||||
|
{ id: 'growth-103', title: '连续打卡', timeText: '今天 07:40', points: 8, icon: 'streak' },
|
||||||
|
{ id: 'growth-104', title: '正确率≥80%', timeText: '昨天 20:05', points: 10, icon: 'accuracy' },
|
||||||
|
{ id: 'growth-105', title: '攻克知识点', timeText: '昨天 19:40', points: 12, icon: 'mastery' },
|
||||||
|
{ id: 'growth-106', title: '上传笔记', timeText: '10月12日 18:20', points: 5, icon: 'note' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/study-report')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
const subjects = [
|
||||||
|
{ id: 'all', name: '全部科目' }, { id: 'math', name: '数学' }, { id: 'chinese', name: '语文' },
|
||||||
|
{ id: 'english', name: '英语' }, { id: 'physics', name: '物理' }
|
||||||
|
]
|
||||||
|
const studentOneWeekActivity = { summary: '周六学习最投入,周四效率最高。', activeDays: 6, totalDurationText: '14小时20分', points: [{ label: '周一', studyMinutes: 0, plannedMinutes: 120, newMasteredCount: 0, studyHeight: 0, plannedHeight: 49, newMasteredHeight: 0 }, { label: '周二', studyMinutes: 0, plannedMinutes: 95, newMasteredCount: 0, studyHeight: 0, plannedHeight: 39, newMasteredHeight: 0 }, { label: '周三', studyMinutes: 142, plannedMinutes: 120, newMasteredCount: 2, studyHeight: 58, plannedHeight: 49, newMasteredHeight: 34 }, { label: '周四', studyMinutes: 88, plannedMinutes: 88, newMasteredCount: 2, studyHeight: 36, plannedHeight: 36, newMasteredHeight: 34 }, { label: '周五', studyMinutes: 0, plannedMinutes: 148, newMasteredCount: 0, studyHeight: 0, plannedHeight: 61, newMasteredHeight: 0 }, { label: '周六', studyMinutes: 178, plannedMinutes: 166, newMasteredCount: 1, studyHeight: 74, plannedHeight: 68, newMasteredHeight: 17 }, { label: '周日', studyMinutes: 0, plannedMinutes: 152, newMasteredCount: 0, studyHeight: 0, plannedHeight: 62, newMasteredHeight: 0 }] }
|
||||||
|
const studentOneMonthActivity = { summary: '本月学习节奏稳定,周末自主学习投入更高。', activeDays: 24, totalDurationText: '52小时40分', points: [{ label: '第1周', studyMinutes: 610, plannedMinutes: 650, newMasteredCount: 3, studyHeight: 52, plannedHeight: 56, newMasteredHeight: 24 }, { label: '第2周', studyMinutes: 690, plannedMinutes: 660, newMasteredCount: 4, studyHeight: 60, plannedHeight: 57, newMasteredHeight: 33 }, { label: '第3周', studyMinutes: 720, plannedMinutes: 680, newMasteredCount: 4, studyHeight: 64, plannedHeight: 59, newMasteredHeight: 33 }, { label: '第4周', studyMinutes: 780, plannedMinutes: 700, newMasteredCount: 3, studyHeight: 72, plannedHeight: 61, newMasteredHeight: 24 }] }
|
||||||
|
const studentOneWeekMasteries = [{ subjectName: '数学', masteredCount: 38, totalCount: 40, masteredPercent: 95 }, { subjectName: '语文', masteredCount: 26, totalCount: 28, masteredPercent: 93 }, { subjectName: '英语', masteredCount: 18, totalCount: 33, masteredPercent: 55 }, { subjectName: '物理', masteredCount: 9, totalCount: 18, masteredPercent: 50 }]
|
||||||
|
const studentOneMonthMasteries = [{ subjectName: '数学', masteredCount: 112, totalCount: 120, masteredPercent: 93 }, { subjectName: '语文', masteredCount: 84, totalCount: 92, masteredPercent: 91 }, { subjectName: '英语', masteredCount: 68, totalCount: 96, masteredPercent: 71 }, { subjectName: '物理', masteredCount: 42, totalCount: 68, masteredPercent: 62 }]
|
||||||
|
const studentTwoWeekActivity = { summary: '本周学习节奏平稳,周末阅读投入较多。', activeDays: 5, totalDurationText: '11小时10分', points: [{ label: '周一', studyMinutes: 82, plannedMinutes: 100, newMasteredCount: 1, studyHeight: 34, plannedHeight: 42, newMasteredHeight: 17 }, { label: '周二', studyMinutes: 0, plannedMinutes: 92, newMasteredCount: 0, studyHeight: 0, plannedHeight: 38, newMasteredHeight: 0 }, { label: '周三', studyMinutes: 110, plannedMinutes: 105, newMasteredCount: 1, studyHeight: 46, plannedHeight: 43, newMasteredHeight: 17 }, { label: '周四', studyMinutes: 96, plannedMinutes: 100, newMasteredCount: 1, studyHeight: 40, plannedHeight: 42, newMasteredHeight: 17 }, { label: '周五', studyMinutes: 0, plannedMinutes: 110, newMasteredCount: 0, studyHeight: 0, plannedHeight: 46, newMasteredHeight: 0 }, { label: '周六', studyMinutes: 152, plannedMinutes: 128, newMasteredCount: 2, studyHeight: 63, plannedHeight: 53, newMasteredHeight: 34 }, { label: '周日', studyMinutes: 130, plannedMinutes: 126, newMasteredCount: 1, studyHeight: 54, plannedHeight: 52, newMasteredHeight: 17 }] }
|
||||||
|
const studentTwoMonthActivity = { summary: '本月完成大部分学习计划,建议保持阅读习惯。', activeDays: 21, totalDurationText: '45小时30分', points: [{ label: '第1周', studyMinutes: 520, plannedMinutes: 600, newMasteredCount: 2, studyHeight: 44, plannedHeight: 51, newMasteredHeight: 17 }, { label: '第2周', studyMinutes: 590, plannedMinutes: 610, newMasteredCount: 3, studyHeight: 50, plannedHeight: 52, newMasteredHeight: 24 }, { label: '第3周', studyMinutes: 640, plannedMinutes: 620, newMasteredCount: 2, studyHeight: 55, plannedHeight: 53, newMasteredHeight: 17 }, { label: '第4周', studyMinutes: 670, plannedMinutes: 640, newMasteredCount: 3, studyHeight: 58, plannedHeight: 55, newMasteredHeight: 24 }] }
|
||||||
|
const studentTwoWeekMasteries = [{ subjectName: '数学', masteredCount: 32, totalCount: 40, masteredPercent: 80 }, { subjectName: '语文', masteredCount: 24, totalCount: 28, masteredPercent: 86 }, { subjectName: '英语', masteredCount: 16, totalCount: 30, masteredPercent: 53 }, { subjectName: '科学', masteredCount: 12, totalCount: 22, masteredPercent: 55 }]
|
||||||
|
const studentTwoMonthMasteries = [{ subjectName: '数学', masteredCount: 92, totalCount: 120, masteredPercent: 77 }, { subjectName: '语文', masteredCount: 76, totalCount: 92, masteredPercent: 83 }, { subjectName: '英语', masteredCount: 58, totalCount: 90, masteredPercent: 64 }, { subjectName: '科学', masteredCount: 38, totalCount: 68, masteredPercent: 56 }]
|
||||||
|
data = isSecondStudent ? {
|
||||||
|
studentId: 'test-student-002', studentName: '张一鸣', studentGrade: '五年级', studentClass: '二班', updatedAtText: '10月13日 08:30', subjects,
|
||||||
|
week: {
|
||||||
|
rangeText: '10月7日 - 10月13日', statusTitle: '本周学习状态良好', statusDescription: '本周学习完成度稳定,建议保持每天的阅读与口算练习。',
|
||||||
|
newMasteredPoints: 2, consecutiveDays: 8, averageAccuracy: 86, scaleLabels: ['120个', '116个', '112个', '108个'],
|
||||||
|
trends: [
|
||||||
|
{ subjectId: 'all', points: [{ label: '周一', value: 109, average: 110, valueHeight: 24, averageHeight: 30 }, { label: '周二', value: 110, average: 111, valueHeight: 30, averageHeight: 35 }, { label: '周三', value: 111, average: 111, valueHeight: 36, averageHeight: 35 }, { label: '周四', value: 112, average: 112, valueHeight: 42, averageHeight: 42 }, { label: '周五', value: 112, average: 113, valueHeight: 42, averageHeight: 48 }, { label: '周六', value: 114, average: 113, valueHeight: 54, averageHeight: 48 }, { label: '周日', value: 115, average: 114, valueHeight: 60, averageHeight: 54 }] },
|
||||||
|
{ subjectId: 'math', points: [{ label: '周一', value: 108, average: 109, valueHeight: 18, averageHeight: 24 }, { label: '周二', value: 109, average: 110, valueHeight: 24, averageHeight: 30 }, { label: '周三', value: 110, average: 110, valueHeight: 30, averageHeight: 30 }, { label: '周四', value: 111, average: 111, valueHeight: 36, averageHeight: 36 }, { label: '周五', value: 112, average: 112, valueHeight: 42, averageHeight: 42 }, { label: '周六', value: 113, average: 112, valueHeight: 48, averageHeight: 42 }, { label: '周日', value: 114, average: 113, valueHeight: 54, averageHeight: 48 }] }
|
||||||
|
], activity: studentTwoWeekActivity, subjectMasteries: studentTwoWeekMasteries
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
rangeText: '9月14日 - 10月13日', statusTitle: '本月学习状态稳步提升', statusDescription: '本月坚持学习,多个学科的知识点掌握情况有所提升。',
|
||||||
|
newMasteredPoints: 9, consecutiveDays: 8, averageAccuracy: 87, scaleLabels: ['120个', '116个', '112个', '108个'],
|
||||||
|
trends: [{ subjectId: 'all', points: [{ label: '第1周', value: 107, average: 108, valueHeight: 14, averageHeight: 18 }, { label: '第2周', value: 110, average: 109, valueHeight: 30, averageHeight: 24 }, { label: '第3周', value: 113, average: 111, valueHeight: 46, averageHeight: 36 }, { label: '第4周', value: 116, average: 113, valueHeight: 62, averageHeight: 48 }] }], activity: studentTwoMonthActivity, subjectMasteries: studentTwoMonthMasteries
|
||||||
|
}
|
||||||
|
} : {
|
||||||
|
studentId: 'test-student-001', studentName: '张一诺', studentGrade: '初二', studentClass: '3班', updatedAtText: '10月13日 08:30', subjects,
|
||||||
|
week: {
|
||||||
|
rangeText: '10月7日 - 10月13日', statusTitle: '本周学习状态良好', statusDescription: '本周状态良好,数学与英语逐渐稳定,物理薄弱题型进步明显。',
|
||||||
|
newMasteredPoints: 3, consecutiveDays: 12, averageAccuracy: 90, scaleLabels: ['120个', '116个', '112个', '108个'],
|
||||||
|
trends: [
|
||||||
|
{ subjectId: 'all', points: [{ label: '周一', value: 112, average: 111, valueHeight: 32, averageHeight: 26 }, { label: '周二', value: 112, average: 113, valueHeight: 32, averageHeight: 38 }, { label: '周三', value: 113, average: 113, valueHeight: 40, averageHeight: 38 }, { label: '周四', value: 114, average: 114, valueHeight: 48, averageHeight: 48 }, { label: '周五', value: 114, average: 114, valueHeight: 48, averageHeight: 48 }, { label: '周六', value: 116, average: 114, valueHeight: 64, averageHeight: 48 }, { label: '周日', value: 116, average: 115, valueHeight: 64, averageHeight: 56 }] },
|
||||||
|
{ subjectId: 'math', points: [{ label: '周一', value: 110, average: 110, valueHeight: 20, averageHeight: 20 }, { label: '周二', value: 111, average: 111, valueHeight: 28, averageHeight: 28 }, { label: '周三', value: 113, average: 112, valueHeight: 42, averageHeight: 35 }, { label: '周四', value: 114, average: 112, valueHeight: 49, averageHeight: 35 }, { label: '周五', value: 115, average: 113, valueHeight: 56, averageHeight: 42 }, { label: '周六', value: 116, average: 113, valueHeight: 63, averageHeight: 42 }, { label: '周日', value: 117, average: 114, valueHeight: 70, averageHeight: 49 }] }
|
||||||
|
], activity: studentOneWeekActivity, subjectMasteries: studentOneWeekMasteries
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
rangeText: '9月14日 - 10月13日', statusTitle: '本月学习状态持续向好', statusDescription: '本月学习计划完成度高,学科掌握情况保持上升趋势。',
|
||||||
|
newMasteredPoints: 14, consecutiveDays: 12, averageAccuracy: 89, scaleLabels: ['120个', '116个', '112个', '108个'],
|
||||||
|
trends: [{ subjectId: 'all', points: [{ label: '第1周', value: 108, average: 109, valueHeight: 16, averageHeight: 23 }, { label: '第2周', value: 111, average: 111, valueHeight: 38, averageHeight: 38 }, { label: '第3周', value: 114, average: 112, valueHeight: 53, averageHeight: 45 }, { label: '第4周', value: 116, average: 114, valueHeight: 68, averageHeight: 53 }] }], activity: studentOneMonthActivity, subjectMasteries: studentOneMonthMasteries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (config.url.includes('/api/parent/alerts')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
const alerts = isSecondStudent ? [
|
||||||
|
{ id: 'alert-201', level: 'medium', title: '作业未完成', content: '英语《单元词汇练习》还有 2 题未完成', occurredAt: '2026-10-28T09:20:00+08:00', isRead: false },
|
||||||
|
{ id: 'alert-202', level: 'low', title: '学习时长提醒', content: '今日学习时长低于本周日均时长', occurredAt: '2026-10-28T08:40:00+08:00', isRead: true }
|
||||||
|
] : [
|
||||||
|
{ id: 'alert-101', level: 'medium', title: '作业未完成', content: '物理《声现象章节测试》今晚 20:30 截止,尚未提交', occurredAt: '2026-10-28T09:30:00+08:00', isRead: false },
|
||||||
|
{ id: 'alert-102', level: 'high', title: '正确率下降', content: '今日数学正确率 78%,低于近 7 日均值 12%', occurredAt: '2026-10-28T09:10:00+08:00', isRead: false },
|
||||||
|
{ id: 'alert-103', level: 'high', title: '攻读失败', content: '数学《几何证明》已用满 2 次攻读机会,仍未完成', occurredAt: '2026-10-28T08:50:00+08:00', isRead: true },
|
||||||
|
{ id: 'alert-104', level: 'low', title: '上线提醒', content: '今日首次上线时间较平日延后 30 分钟', occurredAt: '2026-10-28T08:10:00+08:00', isRead: true }
|
||||||
|
]
|
||||||
|
const list = config.url.includes('limit=3') ? alerts.slice(0, 3) : alerts
|
||||||
|
data = { list, pagination: { page: 1, pageSize: list.length, total: alerts.length, hasMore: false } }
|
||||||
|
} else if (config.url.includes('/api/parent/home-profile')) {
|
||||||
|
const isSecondStudent = config.url.includes('studentId=test-student-002')
|
||||||
|
data = isSecondStudent ? {
|
||||||
|
id: 'test-student-002', name: '张一鸣', account: '20241002', grade: '五年级', school: '实验小学', avatar: '', comprehensiveLevel: 'good'
|
||||||
|
} : {
|
||||||
|
id: 'test-student-001', name: '张一诺', account: '20241001', grade: '初二', school: '实验中学', avatar: '', comprehensiveLevel: 'excellent'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'success',
|
||||||
|
data: data as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用请求方法
|
||||||
|
*/
|
||||||
export function request<T>(config: RequestConfig): Promise<ApiResponse<T> | null> {
|
export function request<T>(config: RequestConfig): Promise<ApiResponse<T> | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const finalConfig = requestInterceptor(config)
|
const finalConfig = requestInterceptor(config)
|
||||||
|
|
||||||
|
if (USE_MOCK_API && !finalConfig.url.includes('/api/v1/Parents/') && !finalConfig.url.includes('/api/v1/ParentStudentToday/') && !finalConfig.url.includes('/api/v1/ParentLearningWarning/') && !finalConfig.url.includes('/api/v1/ParentStudyPack/') && !finalConfig.url.includes('/api/v1/ParentLearningReport/')) {
|
||||||
|
const delay = Math.floor(Math.random() * (MOCK_RESPONSE_MAX_DELAY + 1))
|
||||||
|
setTimeout(() => {
|
||||||
|
if (finalConfig.showLoading !== false) {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
resolve(createMockResponse<T>(finalConfig))
|
||||||
|
}, delay)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
uni.request({
|
uni.request({
|
||||||
url: finalConfig.url,
|
url: finalConfig.url,
|
||||||
method: finalConfig.method ?? 'GET',
|
method: finalConfig.method ?? 'GET',
|
||||||
|
|||||||
Reference in New Issue
Block a user