Files
App.k12.Parents/pages/study/index.uvue
T
2026-10-29 10:08:39 +08:00

156 lines
13 KiB
Plaintext

<template>
<view class="page-container">
<view class="header-panel">
<view class="header-copy"><text class="header-title">学情报告</text><text class="header-subtitle">{{ report.studentName }} · {{ report.studentGrade }} · {{ report.studentClass }}</text></view>
<view class="header-action"><UniIcons type="person-filled" class="header-action-icon"></UniIcons></view>
</view>
<view class="report-meta"><text class="report-meta-text">{{ activeReport.rangeText }} · 数据更新到 {{ report.updatedAtText }}</text></view>
<scroll-view class="content-scroll" direction="vertical" scroll-y="true" refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="refreshReport">
<view v-if="isLoading" class="status-panel"><text class="status-text">学情报告加载中</text></view>
<view v-else class="content-panel">
<view class="filters-card">
<view class="period-tabs"><view class="period-tab" :class="activePeriod === 'week' ? 'period-tab-active' : ''" @tap="setPeriod('week')"><text class="period-tab-text" :class="activePeriod === 'week' ? 'period-tab-text-active' : ''">周报</text></view><view class="period-tab" :class="activePeriod === 'month' ? 'period-tab-active' : ''" @tap="setPeriod('month')"><text class="period-tab-text" :class="activePeriod === 'month' ? 'period-tab-text-active' : ''">月报</text></view></view>
<scroll-view class="subject-scroll" direction="horizontal" scroll-x="true"><view class="subject-tabs"><view v-for="subject in report.subjects" :key="subject.id" class="subject-tab" :class="activeSubjectId === subject.id ? 'subject-tab-active' : ''" @tap="setSubject(subject.id)"><text class="subject-tab-text" :class="activeSubjectId === subject.id ? 'subject-tab-text-active' : ''">{{ subject.name }}</text></view></view></scroll-view>
</view>
<view 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="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>
</scroll-view>
<MainTabBar active-tab="study"></MainTabBar>
</view>
</template>
<script setup lang="ts">
import MainTabBar from '../../components/main-tab-bar.uvue'
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'
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 isLoading = ref(true)
const isRefreshing = ref(false)
const activePeriod = ref('week')
const activeSubjectId = ref('all')
const activeReport = computed((): StudyReportPeriod => activePeriod.value === 'week' ? report.value.week : report.value.month)
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]
if (trend.subjectId === activeSubjectId.value) return trend.points
}
return activeReport.value.trends.length > 0 ? activeReport.value.trends[0].points : []
})
onLoad(() => {
loadReport()
})
async function loadReport() {
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 (!studentId) { isLoading.value = false; return }
const res = await apiGetStudentStudyReport(studentId)
if (res && res.data) report.value = res.data
isLoading.value = false
}
async function refreshReport() {
isRefreshing.value = true
await loadReport()
isRefreshing.value = false
}
function setPeriod(period: string) {
activePeriod.value = period
}
function setSubject(subjectId: string) {
activeSubjectId.value = subjectId
}
</script>
<style>
.page-container { display: flex; flex-direction: column; flex: 1; background-color: #f5f7fb; }
.header-panel { display: flex; flex-direction: row; align-items: center; padding: 12px 18px; background-color: #ffffff; }
.header-back { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 18px; background-color: #f1f4f8; }
.header-back-icon { font-size: 18px; color: #66758d; }
.header-copy { display: flex; flex-direction: column; flex: 1; margin-left: 12px; }
.header-title { font-size: 17px; font-weight: 700; color: #22283a; }
.header-subtitle { margin-top: 3px; font-size: 12px; 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: #6558e9; }
.report-meta { padding: 8px 18px; background-color: #ffffff; border-top: 1px solid #edf0f5; }
.report-meta-text { font-size: 11px; color: #8992a5; }
.content-scroll { flex: 1; }
.status-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 260px; }
.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; }
.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; }
.period-tab-text { font-size: 12px; color: #64748b; }
.period-tab-text-active { color: #ffffff; }
.subject-scroll { margin-top: 10px; }
.subject-tabs { display: flex; flex-direction: row; }
.subject-tab { flex-shrink: 0; padding: 7px 14px; margin-right: 8px; border-radius: 16px; background-color: #f1f4f8; }
.subject-tab-active { background-color: #554be9; }
.subject-tab-text { font-size: 12px; color: #64748b; }
.subject-tab-text-active { color: #ffffff; }
.summary-card { display: flex; flex-direction: column; margin-top: 16px; padding: 16px; border-radius: 14px; background-color: #6545e8; }
.summary-title { font-size: 15px; font-weight: 700; color: #ffffff; }
.summary-description { margin-top: 8px; font-size: 12px; line-height: 19px; color: #efeaff; }
.summary-stats { display: flex; flex-direction: row; margin-top: 14px; padding: 10px 0; border-radius: 8px; background-color: #7659ee; }
.summary-stat { display: flex; flex-direction: column; align-items: center; flex: 1; }
.summary-stat-middle { border-left: 1px solid #927cf3; border-right: 1px solid #927cf3; }
.summary-value { font-size: 15px; font-weight: 700; color: #ffffff; }
.summary-label { margin-top: 4px; font-size: 10px; color: #e9e2ff; }
.trend-card { display: flex; flex-direction: column; margin-top: 16px; padding: 16px; border-radius: 8px; background-color: #ffffff; }
.section-title { font-size: 16px; font-weight: 700; color: #242b3d; }
.trend-legend { display: flex; flex-direction: row; justify-content: flex-end; margin-top: 12px; }
.legend-item { display: flex; flex-direction: row; align-items: center; margin-left: 12px; }
.legend-mark { width: 11px; height: 5px; margin-right: 5px; border-radius: 2px; }
.legend-mark-student { background-color: #6558e9; }
.legend-mark-average { background-color: #9aa8bc; }
.legend-mark-plan { background-color: #d3dce8; }
.legend-mark-mastery { width: 7px; height: 7px; border-radius: 4px; background-color: #ffab26; }
.legend-text { font-size: 10px; color: #596275; }
.activity-card { display: flex; flex-direction: column; margin-top: 16px; padding: 16px; border-radius: 8px; background-color: #ffffff; }
.activity-summary { margin-top: 8px; font-size: 12px; color: #596275; }
.activity-total { margin-top: 4px; font-size: 12px; color: #8992a5; }
.activity-legend { display: flex; flex-direction: row; flex-wrap: wrap; margin-top: 6px; }
.activity-legend-item { margin-left: 0; margin-right: 12px; margin-bottom: 4px; }
.mastery-card { display: flex; flex-direction: column; margin-top: 16px; padding: 16px; border-radius: 8px; background-color: #ffffff; }
.mastery-legend { display: flex; flex-direction: row; margin-top: 16px; }
.mastery-legend-item { display: flex; flex-direction: row; align-items: center; margin-right: 16px; }
.mastery-legend-color { width: 8px; height: 8px; margin-right: 5px; border-radius: 2px; }
.mastery-legend-mastered { background-color: #16bd88; }
.mastery-legend-unmastered { background-color: #f34d54; }
.mastery-legend-text { font-size: 11px; color: #8992a5; }
.mastery-item { display: flex; flex-direction: column; margin-top: 14px; }
.mastery-heading { display: flex; flex-direction: row; justify-content: space-between; }
.mastery-subject { font-size: 13px; color: #4a5568; }
.mastery-value { font-size: 13px; font-weight: 700; color: #242b3d; }
.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; }
</style>