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