首页、学情、任务绑定接口
This commit is contained in:
+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>
|
||||
|
||||
Reference in New Issue
Block a user