首页、学情、任务绑定接口

This commit is contained in:
hexi
2026-11-06 16:43:41 +08:00
parent af73453035
commit 4a19192d73
16 changed files with 965 additions and 236 deletions
+59 -16
View File
@@ -61,8 +61,8 @@
<text class="metric-value metric-blue">{{ metrics.learningDaysThisWeek }}/{{ metrics.shouldStudyDaysThisWeek }} 天</text>
</view>
<view class="metric-item">
<text class="metric-label">待完成作业</text>
<text class="metric-value metric-orange">{{ metrics.pendingHomeworkCount }} 个</text>
<text class="metric-label">待完成学习包</text>
<text class="metric-value metric-orange">{{ metrics.pendingPackCount }} 个</text>
</view>
</view>
</view>
@@ -100,10 +100,10 @@
<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 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.school }} {{ item.className }}</text></view><text class="student-picker-detail">账号:{{ item.account }}</text></view>
<view v-if="selectedStudentId === item.id" class="student-picker-check"><text class="student-picker-check-text">已选</text></view>
<view v-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-role">{{ studentPickerRole(index) }}</text><text class="student-picker-status">{{ studentPickerStatus(index) }}</text></view><text class="student-picker-detail">{{ item.school }} · {{ item.className }}</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>
@@ -116,7 +116,7 @@
import MainTabBar from '../../components/main-tab-bar.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 { 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'
const loadingStudent = ref<StudentHomeProfile>({
@@ -134,10 +134,11 @@
const isStudentsLoading = ref(false)
const boundStudents = ref<BoundStudentSummary[]>([])
const selectedStudentId = ref('')
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
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, shouldStudyDaysThisWeek: 0, pendingHomeworkCount: 0, updatedAt: ''
accuracyToday: null, learningDaysThisWeek: 0, shouldStudyDaysThisWeek: 0, pendingPackCount: 0, updatedAt: ''
})
const alerts = computed((): StudentAlert[] => homeStudentAlerts.value ?? [])
const parentName = computed((): string => {
@@ -174,6 +175,24 @@
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) {
const res = await apiGetStudentHomeMetrics(studentId, showLoading)
if (res && res.data) {
@@ -182,7 +201,7 @@
}
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) {
setHomeStudentAlerts(res.data.list)
}
@@ -227,7 +246,6 @@
async function openStudentPicker() {
showStudentPicker.value = true
selectedStudentId.value = student.value.id
if (boundStudents.value.length > 0) return
isStudentsLoading.value = true
const res = await apiGetBoundStudents()
if (res && res.data) {
@@ -243,6 +261,26 @@
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 {
return level === 'low' ? 'reminder-blue' : level === 'medium' ? 'reminder-yellow' : 'reminder-red'
}
@@ -279,11 +317,9 @@
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)
switchBoundStudent(selectedStudent)
observedStudentVersion.value = selectedStudentChangeVersion.value
await loadHomeData(selectedStudent.id)
}
}
</script>
@@ -412,18 +448,25 @@
.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: 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-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-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-check { padding: 4px 7px; border-radius: 9px; background-color: #554be9; }
.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-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; }