首页、学情、任务绑定接口
This commit is contained in:
+153
-26
@@ -1,68 +1,195 @@
|
||||
<template>
|
||||
<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">
|
||||
<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 v-for="alert in alerts" :key="alert.id" class="alert-card">
|
||||
<view class="alert-card-top"><view class="alert-level-dot" :class="levelDotClass(alert.level)"></view><text class="alert-title">{{ alert.title }}</text><text v-if="!alert.isRead" class="unread-mark">未读</text></view>
|
||||
<text class="alert-content">{{ alert.content }}</text>
|
||||
<text class="alert-time">{{ formatTime(alert.occurredAt) }}</text>
|
||||
<view v-else class="content-body">
|
||||
<view class="summary-card">
|
||||
<text class="summary-title">今日触发预警</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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { apiGetStudentAlerts, type StudentAlert, type StudentAlertLevel } from '../../api/student'
|
||||
import { homeStudentProfile } from '../../store/home'
|
||||
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
|
||||
import { apiGetBoundStudents, apiGetStudentLearningWarnings, type BoundStudentSummary, type StudentAlert } from '../../api/student'
|
||||
import { homeStudentProfile, selectedStudentChangeVersion, switchBoundStudent } from '../../store/home'
|
||||
|
||||
const alerts = ref<StudentAlert[]>([])
|
||||
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 triggeredCount = computed((): number => alerts.value.filter((item: StudentAlert) => item.isTriggered).length)
|
||||
|
||||
onLoad(() => {
|
||||
loadAlerts()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||
loadAlerts()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadAlerts() {
|
||||
isLoading.value = true
|
||||
const studentId = homeStudentProfile.value?.id ?? ''
|
||||
if (!studentId) {
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
const res = await apiGetStudentAlerts(studentId, 1, 20)
|
||||
const res = await apiGetStudentLearningWarnings(studentId)
|
||||
if (res && res.data) {
|
||||
alerts.value = res.data.list
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
function levelDotClass(level: StudentAlertLevel): string {
|
||||
return level === 'low' ? 'alert-level-low' : level === 'medium' ? 'alert-level-medium' : 'alert-level-high'
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function formatTime(value: string): string {
|
||||
return value.length > 0 ? value.substring(5, 16).replace('T', ' ') : ''
|
||||
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 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>
|
||||
|
||||
<style>
|
||||
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f5f7fb; }
|
||||
.header-panel { display: flex; flex-direction: column; padding: 18px 20px; background-color: #5d3de8; }
|
||||
.header-title { font-size: 21px; font-weight: 700; color: #ffffff; }
|
||||
.header-subtitle { margin-top: 6px; font-size: 13px; color: #e5ddff; }
|
||||
.content-scroll { flex: 1; padding: 16px 20px; }
|
||||
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f6f7fb; }
|
||||
.header-panel { display: flex; flex-direction: row; align-items: center; padding: 6px 4px 8px; background-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; }
|
||||
.back-icon { font-size: 30px; line-height: 32px; color: #415066; }
|
||||
.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-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-level-dot { width: 9px; height: 9px; margin-right: 8px; border-radius: 5px; }
|
||||
.alert-level-high { background-color: #e25a5a; }
|
||||
.alert-level-medium { background-color: #e8a129; }
|
||||
.alert-level-low { background-color: #4d87cf; }
|
||||
.alert-title { flex: 1; font-size: 15px; font-weight: 700; color: #30384a; }
|
||||
.unread-mark { padding: 3px 7px; border-radius: 8px; font-size: 10px; color: #554be9; background-color: #eeefff; }
|
||||
.alert-content { margin-top: 10px; font-size: 13px; line-height: 20px; color: #626c7e; }
|
||||
.alert-time { margin-top: 10px; font-size: 11px; color: #9aa2b1; }
|
||||
.alert-level-dot { width: 7px; height: 7px; margin-right: 11px; border-radius: 4px; }
|
||||
.alert-level-triggered { background-color: #ff3f6b; }
|
||||
.alert-level-normal { background-color: #d2dce8; }
|
||||
.alert-title { flex: 1; font-size: 14px; font-weight: 700; color: #1f2a3d; }
|
||||
.alert-status { padding: 4px 8px; border-radius: 12px; font-size: 10px; }
|
||||
.alert-status-triggered { color: #ff3f6b; background-color: #fff0f4; }
|
||||
.alert-status-normal { color: #00a86b; background-color: #eafaf3; }
|
||||
.alert-content { margin-top: 10px; margin-left: 18px; font-size: 12px; line-height: 19px; color: #536278; }
|
||||
.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>
|
||||
|
||||
+10
-1
@@ -8,14 +8,23 @@
|
||||
<script setup lang="ts">
|
||||
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
|
||||
import { apiGetStudentGrowth, apiGetStudentHomeProfile, 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 isLoading = ref(true)
|
||||
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||
|
||||
onLoad(() => { loadGrowth() })
|
||||
|
||||
onShow(() => {
|
||||
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||
loadGrowth()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadGrowth() {
|
||||
isLoading.value = true
|
||||
let studentId = homeStudentProfile.value?.id ?? ''
|
||||
if (!studentId) {
|
||||
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
||||
|
||||
+59
-16
@@ -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; }
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
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'
|
||||
import { switchBoundStudent } from '../../store/home'
|
||||
|
||||
type LoginForm = {
|
||||
phone: string
|
||||
@@ -144,8 +144,7 @@
|
||||
? res.data.students.find((item) => item.id === savedStudent.id)!
|
||||
: res.data.students[0]
|
||||
|
||||
saveSelectedStudent(selectedStudent)
|
||||
applyBoundStudent(selectedStudent)
|
||||
switchBoundStudent(selectedStudent)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<script setup lang="ts">
|
||||
import MainTabBar from '../../components/main-tab-bar.uvue'
|
||||
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 { clearUserInfo } from '../../store/user'
|
||||
|
||||
@@ -72,9 +72,17 @@
|
||||
const selectedStudentId = ref(homeStudentProfile.value?.id ?? '')
|
||||
const isLoading = ref(true)
|
||||
const listFailed = ref(false)
|
||||
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||
|
||||
onLoad(() => { loadStudents() })
|
||||
|
||||
onShow(() => {
|
||||
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||
selectedStudentId.value = homeStudentProfile.value?.id ?? selectedStudentId.value
|
||||
}
|
||||
})
|
||||
|
||||
async function loadStudents() {
|
||||
isLoading.value = true
|
||||
listFailed.value = false
|
||||
@@ -92,8 +100,8 @@
|
||||
selectedStudentId.value = studentId
|
||||
const student = students.value.find((item) => item.id === studentId)
|
||||
if (student) {
|
||||
saveSelectedStudent(student)
|
||||
applyBoundStudent(student)
|
||||
switchBoundStudent(student)
|
||||
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+124
-10
@@ -2,7 +2,7 @@
|
||||
<view class="page-container">
|
||||
<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-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 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>
|
||||
</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>
|
||||
</scroll-view>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -32,18 +54,24 @@
|
||||
import StudyActivityChart from '../../components/study-activity-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 { apiGetStudentHomeProfile, apiGetStudentStudyReport, type StudentStudyReport, type StudyReportActivity, type StudyReportPeriod, type StudyReportSubjectMastery, type StudyReportTrendPoint } from '../../api/student'
|
||||
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
|
||||
import { apiGetBoundStudents, apiGetStudentHomeProfile, apiGetStudentStudyReport, type BoundStudentSummary, type StudentStudyReport, type StudyReportActivity, type StudyReportPeriod, type StudyReportSubjectMastery, type StudyReportTrendPoint } from '../../api/student'
|
||||
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile, switchBoundStudent } from '../../store/home'
|
||||
|
||||
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 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 isRefreshing = ref(false)
|
||||
const isStudentsLoading = ref(false)
|
||||
const showStudentPicker = ref(false)
|
||||
const boundStudents = ref<BoundStudentSummary[]>([])
|
||||
const selectedStudentId = ref('')
|
||||
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 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[] => {
|
||||
for (let index = 0; index < activeReport.value.trends.length; index++) {
|
||||
const trend = activeReport.value.trends[index]
|
||||
@@ -56,7 +84,16 @@
|
||||
loadReport()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||
loadReport()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadReport() {
|
||||
isLoading.value = true
|
||||
activeSubjectId.value = '0'
|
||||
let studentId = homeStudentProfile.value?.id ?? ''
|
||||
if (!studentId) {
|
||||
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
||||
@@ -79,12 +116,57 @@
|
||||
|
||||
function setPeriod(period: string) {
|
||||
activePeriod.value = period
|
||||
activeSubjectId.value = '0'
|
||||
}
|
||||
|
||||
function setSubject(subjectId: string) {
|
||||
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>
|
||||
|
||||
<style>
|
||||
@@ -104,6 +186,10 @@
|
||||
.status-text { font-size: 13px; color: #8992a5; }
|
||||
.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; }
|
||||
.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-tab { padding: 7px 14px; margin-right: 8px; border-radius: 16px; background-color: #f1f4f8; }
|
||||
.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-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; }
|
||||
.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>
|
||||
|
||||
+222
-52
@@ -1,100 +1,270 @@
|
||||
<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>
|
||||
|
||||
<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 { apiGetParentStudyPacks, apiGetStudentHomeProfile, type LearningTaskItem, type ParentStudyPackStatus, type StudentLearningTasks } from '../../api/student'
|
||||
import { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
|
||||
import { apiGetBoundStudents, apiGetParentStudyPacks, apiGetStudentHomeProfile, type BoundStudentSummary, type LearningTaskItem, type ParentStudyPackStatus, type StudentLearningTasks } from '../../api/student'
|
||||
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile, switchBoundStudent } 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 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 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)
|
||||
const headerStudentName = computed((): string => homeStudentProfile.value?.name ?? tasks.value.studentName ?? '孩子')
|
||||
|
||||
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 ?? ''
|
||||
if (!studentId) {
|
||||
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
||||
if (profileRes && profileRes.data) { setHomeStudentProfile(profileRes.data); studentId = profileRes.data.id }
|
||||
if (profileRes && profileRes.data) {
|
||||
setHomeStudentProfile(profileRes.data)
|
||||
studentId = profileRes.data.id
|
||||
}
|
||||
}
|
||||
if (studentId) {
|
||||
const res = await 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}`
|
||||
}
|
||||
}
|
||||
const res = await apiGetParentStudyPacks(studentId, activeStatus.value, showLoading)
|
||||
if (res && res.data) tasks.value = res.data
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
isRefreshing.value = true
|
||||
await loadTasks()
|
||||
await loadTasks(false)
|
||||
isRefreshing.value = false
|
||||
}
|
||||
|
||||
async function setFilter(filter: string) {
|
||||
if (activeFilter.value === filter) return
|
||||
activeFilter.value = filter
|
||||
await loadTasks()
|
||||
await loadTasks(true)
|
||||
}
|
||||
|
||||
function matchesFilter(task: LearningTaskItem): boolean {
|
||||
if (activeFilter.value === 'all') return true
|
||||
if (activeFilter.value === 'pending') return task.status === 'not_started' || task.status === 'in_progress'
|
||||
if (activeFilter.value === 'completed') return task.status === 'success'
|
||||
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>
|
||||
|
||||
<style>
|
||||
.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-title { font-size: 18px; font-weight: 700; color: #242b3d; }
|
||||
.header-subtitle { margin-top: 4px; font-size: 11px; color: #8992a5; }
|
||||
.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-icon { font-size: 18px; color: #554be9; }
|
||||
.filter-panel { display: flex; flex-direction: row; padding: 12px 20px; 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-active { background-color: #554be9; }
|
||||
.filter-text { font-size: 11px; color: #64748b; }
|
||||
.header-title { font-size: 17px; font-weight: 800; color: #101827; }
|
||||
.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: #edf1ff; }
|
||||
.header-action-icon { font-size: 18px; color: #5947ea; }
|
||||
.filter-panel { display: flex; flex-direction: row; padding: 10px 8px 12px; background-color: #ffffff; }
|
||||
.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: #5a45e8; }
|
||||
.filter-text { font-size: 11px; color: #4e5b70; }
|
||||
.filter-text-active { color: #ffffff; }
|
||||
.content-scroll { flex: 1; }
|
||||
.content-panel { display: flex; flex-direction: column; padding: 14px 16px 24px; }
|
||||
.status-panel, .empty-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
|
||||
.status-text, .empty-text { font-size: 13px; color: #8992a5; }
|
||||
.group-card { display: flex; flex-direction: column; margin-bottom: 12px; padding: 16px; border-radius: 15px; background-color: #ffffff; }
|
||||
.content-panel { display: flex; flex-direction: column; padding: 14px 8px 24px; }
|
||||
.status-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
|
||||
.empty-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; }
|
||||
.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-tags { display: flex; flex-direction: row; align-items: center; flex: 1; }
|
||||
.subject-tag, .unit-tag { padding: 3px 7px; margin-right: 6px; border-radius: 8px; font-size: 10px; color: #554be9; background-color: #eeefff; }
|
||||
.unit-tag { color: #1598dc; background-color: #e6f5ff; }
|
||||
.lesson-title { font-size: 13px; font-weight: 700; color: #242b3d; }
|
||||
.knowledge-count { flex-shrink: 0; font-size: 10px; color: #8992a5; }
|
||||
.lesson-time { margin-top: 7px; font-size: 11px; color: #8ca0bd; }
|
||||
.group-note { display: flex; flex-direction: row; align-items: flex-start; margin-top: 12px; padding: 12px; border-radius: 12px; background-color: #f6f8fb; }
|
||||
.group-note-icon { width: 12px; height: 12px; margin-right: 7px; border-radius: 6px; text-align: center; font-size: 9px; color: #ffffff; background-color: #9aa8bc; }
|
||||
.group-note-text { flex: 1; font-size: 11px; line-height: 18px; color: #68778b; }
|
||||
.detail-task { display: flex; flex-direction: column; margin-top: 10px; padding: 12px; border: 1px solid #edf0f5; border-radius: 12px; background-color: #f9fbfd; }
|
||||
.detail-task-heading { display: flex; flex-direction: row; justify-content: space-between; }
|
||||
.detail-task-title { font-size: 13px; color: #242b3d; }
|
||||
.detail-task-status { padding: 3px 7px; border-radius: 8px; font-size: 10px; }
|
||||
.detail-task-status-success { color: #16a979; background-color: #e8fbf3; }
|
||||
.detail-task-status-in_progress { color: #5147e9; background-color: #eeefff; }
|
||||
.detail-task-status-failed { color: #f34d54; background-color: #fff0f1; }
|
||||
.detail-task-result { margin-top: 8px; font-size: 11px; }
|
||||
.detail-task-result-success { color: #16a979; }
|
||||
.detail-task-result-in_progress { color: #5147e9; }
|
||||
.detail-task-result-failed { color: #f34d54; }
|
||||
.detail-task-meta { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e4eaf1; font-size: 10px; color: #8992a5; }
|
||||
.group-title-row { display: flex; flex-direction: row; align-items: center; flex: 1; min-width: 0; }
|
||||
.subject-tag { padding: 3px 7px; margin-right: 6px; border-radius: 9px; font-size: 10px; font-weight: 700; color: #5947ea; background-color: #eeefff; }
|
||||
.unit-tag { padding: 3px 7px; margin-right: 8px; border-radius: 9px; font-size: 10px; font-weight: 700; color: #2f6bea; background-color: #edf5ff; }
|
||||
.lesson-title { flex: 1; font-size: 14px; font-weight: 800; color: #1f2937; }
|
||||
.knowledge-count { flex-shrink: 0; font-size: 11px; color: #8a99ae; }
|
||||
.lesson-time-row { display: flex; flex-direction: row; align-items: center; margin-top: 8px; }
|
||||
.time-dot { width: 8px; height: 8px; margin-right: 6px; border-radius: 4px; background-color: #9caec4; }
|
||||
.lesson-time { font-size: 11px; color: #71849d; }
|
||||
.task-empty { display: flex; flex-direction: column; align-items: center; padding: 18px 0; }
|
||||
.task-empty-text { font-size: 12px; color: #8992a5; }
|
||||
.task-card { display: flex; flex-direction: column; margin-top: 13px; padding: 12px; border-radius: 12px; background-color: #f8fafc; border: 1px solid #edf1f6; }
|
||||
.task-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||
.task-title { flex: 1; font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||
.task-status { flex-shrink: 0; padding: 4px 8px; border-radius: 11px; font-size: 10px; }
|
||||
.task-status-not_started { color: #516175; background-color: #edf2f7; }
|
||||
.task-status-in_progress { color: #5947ea; background-color: #eeefff; }
|
||||
.task-status-success { color: #04a86b; background-color: #e8fbf2; }
|
||||
.task-status-failed { color: #ef4d62; background-color: #fff0f3; }
|
||||
.task-result-row { display: flex; flex-direction: row; align-items: center; margin-top: 9px; }
|
||||
.task-result-dot { width: 9px; height: 9px; margin-right: 7px; border-radius: 5px; }
|
||||
.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>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
import MainTabBar from '../../components/main-tab-bar.uvue'
|
||||
import UniIcons from '../../uni_modules/uni-icons-x/components/uni-icons/uni-icons.uvue'
|
||||
import { apiGetTodayLearningDetail, type TodayLearningDetail } from '../../api/student'
|
||||
import { homeStudentProfile } from '../../store/home'
|
||||
import { homeStudentProfile, selectedStudentChangeVersion } from '../../store/home'
|
||||
|
||||
const isLoading = ref(true)
|
||||
const detail = ref<TodayLearningDetail>({
|
||||
@@ -64,12 +64,21 @@
|
||||
answerCount: 0, completedPackages: 0, totalPackages: 0, masteredPoints: 0, courses: [],
|
||||
score: { accuracy: 0, yesterdayAccuracy: 0, change: 0 }, pointsTotal: 0, pointDetails: []
|
||||
})
|
||||
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||
|
||||
onLoad(() => {
|
||||
loadTodayLearning()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||
loadTodayLearning()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadTodayLearning() {
|
||||
isLoading.value = true
|
||||
const studentId = homeStudentProfile.value?.id ?? ''
|
||||
if (!studentId) {
|
||||
isLoading.value = false
|
||||
|
||||
@@ -8,14 +8,23 @@
|
||||
<script setup lang="ts">
|
||||
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 { homeStudentProfile, setHomeStudentProfile } from '../../store/home'
|
||||
import { homeStudentProfile, selectedStudentChangeVersion, setHomeStudentProfile } from '../../store/home'
|
||||
|
||||
const vocabulary = ref<StudentVocabulary>({ studentId: '', studentName: '', studentGrade: '', studentClass: '', bookCount: 0, books: [] })
|
||||
const isLoading = ref(true)
|
||||
const observedStudentVersion = ref(selectedStudentChangeVersion.value)
|
||||
|
||||
onLoad(() => { loadVocabulary() })
|
||||
|
||||
onShow(() => {
|
||||
if (observedStudentVersion.value !== selectedStudentChangeVersion.value) {
|
||||
observedStudentVersion.value = selectedStudentChangeVersion.value
|
||||
loadVocabulary()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadVocabulary() {
|
||||
isLoading.value = true
|
||||
let studentId = homeStudentProfile.value?.id ?? ''
|
||||
if (!studentId) {
|
||||
const profileRes = await apiGetStudentHomeProfile(undefined, false)
|
||||
|
||||
Reference in New Issue
Block a user