update:运动馆教师端

This commit is contained in:
ltx 2026-04-29 14:56:33 +08:00
parent b2be523c5c
commit 1f17d4736d
12 changed files with 1561 additions and 200 deletions

View File

@ -5,7 +5,7 @@ import {
getReport,
getPhysicalFitnessReport
} from '../../../utils/serve/parent'
import { getClassRoomRecord } from '../../../utils/serve/teacher';
import { getClassRoomRecord, getStudentClassRoomReport } from '../../../utils/serve/teacher';
import { getWeekdayWithValidation } from '../../../utils/utils';
var uChartsInstance = {};
@ -59,34 +59,8 @@ Page({
{ value: 3, label: '第三季度', months: '7-9月' },
{ value: 4, label: '第四季度', months: '10-12月' },
],
mockMonthData: {
heartRange: '80-116次/分',
avgHeart: 80,
highDuration: '80%',
highRatio: '60%',
jumpTotal: 706,
jumpDuration: 3,
jumpAvgSpeed: 235,
jumpRecords: [
{ type: '智能跳绳', duration: '60s', count: 248 },
{ type: '智能跳绳', duration: '60s', count: 248 },
{ type: '智能跳绳', duration: '60s', count: 248 },
],
},
mockQuarterData: {
heartRange: '75-122次/分',
avgHeart: 85,
highDuration: '72%',
highRatio: '52%',
jumpTotal: 2048,
jumpDuration: 9,
jumpAvgSpeed: 228,
jumpRecords: [
{ type: '智能跳绳', duration: '60s', count: 248 },
{ type: '智能跳绳', duration: '60s', count: 700 },
{ type: '智能跳绳', duration: '60s', count: 600 },
],
},
mockMonthData: {},
mockQuarterData: {},
},
/**
@ -156,7 +130,10 @@ Page({
switchClassroomSubTab(e) {
const tab = Number(e.currentTarget.dataset.tab)
if (tab === this.data.classroomSubTab) return
this.setData({ classroomSubTab: tab })
this.setData({ classroomSubTab: tab }, () => {
if (tab === 1) this.fetchMonthReport()
else if (tab === 2) this.fetchQuarterReport()
})
},
// 月份选择器
@ -170,7 +147,9 @@ Page({
const month = Number(e.currentTarget.dataset.month)
const { pickerYear, nowYear, nowMonth } = this.data
if (pickerYear > nowYear || (pickerYear === nowYear && month > nowMonth)) return
this.setData({ selectedMonth: month, selectedYear: pickerYear, showMonthPicker: false })
this.setData({ selectedMonth: month, selectedYear: pickerYear, showMonthPicker: false }, () => {
this.fetchMonthReport()
})
},
// 季度选择器
@ -184,7 +163,9 @@ Page({
const quarter = Number(e.currentTarget.dataset.quarter)
const { pickerYear, nowYear, nowQuarter } = this.data
if (pickerYear > nowYear || (pickerYear === nowYear && quarter > nowQuarter)) return
this.setData({ selectedQuarter: quarter, selectedYear: pickerYear, showQuarterPicker: false })
this.setData({ selectedQuarter: quarter, selectedYear: pickerYear, showQuarterPicker: false }, () => {
this.fetchQuarterReport()
})
},
// 选择器年份切换
@ -197,6 +178,68 @@ Page({
},
// 构建时间范围:月
_getMonthRange(year, month) {
const startTime = `${year}-${String(month).padStart(2, '0')}-01`
let endYear = year, endMonth = month + 1
if (endMonth > 12) { endYear++; endMonth = 1 }
const endTime = `${endYear}-${String(endMonth).padStart(2, '0')}-01`
return { startTime, endTime }
},
// 构建时间范围:季度
_getQuarterRange(year, quarter) {
const startMonth = (quarter - 1) * 3 + 1
let endYear = year, endMonth = quarter * 3 + 1
if (endMonth > 12) { endYear++; endMonth = 1 }
const startTime = `${year}-${String(startMonth).padStart(2, '0')}-01`
const endTime = `${endYear}-${String(endMonth).padStart(2, '0')}-01`
return { startTime, endTime }
},
// 将接口数据转换为页面所需格式
_buildPeriodData(apiData) {
const d = apiData || {}
return {
heartRange: `${d.minHR || 0}-${d.maxHR || 0}次/分`,
avgHeart: d.avgHR || 0,
highDuration: `${d.highIntensity || 0}分钟`,
highRatio: `${d.highIntensityDurationRatio || 0}%`,
jumpTotal: d.totalCount || 0,
jumpDuration: d.totalDuration || 0,
jumpAvgSpeed: d.avgSpeed || 0,
jumpRecords: (d.jumpRecordList || []).map(item => ({
type: item.jumpType,
duration: `${item.totalDuration}s`,
count: item.totalCount,
})),
}
},
// 获取月报告
async fetchMonthReport() {
const { selectedYear, selectedMonth } = this.data
const { startTime, endTime } = this._getMonthRange(selectedYear, selectedMonth)
try {
const res = await getStudentClassRoomReport({ type: 2, startTime, endTime })
this.setData({ mockMonthData: this._buildPeriodData(res.data) })
} catch (e) {
this.setData({ mockMonthData: {} })
}
},
// 获取季度报告
async fetchQuarterReport() {
const { selectedYear, selectedQuarter } = this.data
const { startTime, endTime } = this._getQuarterRange(selectedYear, selectedQuarter)
try {
const res = await getStudentClassRoomReport({ type: 2, startTime, endTime })
this.setData({ mockQuarterData: this._buildPeriodData(res.data) })
} catch (e) {
this.setData({ mockQuarterData: {} })
}
},
async fetchFitnessReport() {
try {
const res = await getPhysicalFitnessReport()
@ -402,28 +445,40 @@ Page({
* 页面相关事件处理函数--监听用户下拉动作
*/
async onPullDownRefresh() {
if(this.data.isTrainee){
this.setData({
pageIndex:1,
hasMore:true
},async ()=>{
this.getTrainClass(true)
if (this.data.isTrainee) {
const { activeTab, classroomSubTab } = this.data
if (activeTab === 0) {
// 课堂报告 Tab
if (classroomSubTab === 0) {
// 课堂列表
this.setData({ pageIndex: 1, hasMore: true }, async () => {
await this.getTrainClass(true)
wx.stopPullDownRefresh()
wx.showToast({ title: '刷新成功', icon: 'none' })
})
} else if (classroomSubTab === 1) {
// 月报告
await this.fetchMonthReport()
wx.stopPullDownRefresh()
wx.showToast({ title: '刷新成功', icon: 'none' })
} else if (classroomSubTab === 2) {
// 季度报告
await this.fetchQuarterReport()
wx.stopPullDownRefresh()
wx.showToast({ title: '刷新成功', icon: 'none' })
}
} else if (activeTab === 1) {
// 体测报告 Tab
await this.fetchFitnessReport()
this.drawRadarChart(this)
wx.stopPullDownRefresh()
wx.showToast({
title: '刷新成功',
icon: 'none'
})
})
}else{
wx.showToast({ title: '刷新成功', icon: 'none' })
}
} else {
await this.getStudentReport()
wx.stopPullDownRefresh()
wx.showToast({
title: '刷新成功!',
icon: 'none'
})
wx.showToast({ title: '刷新成功!', icon: 'none' })
}
},
/**

View File

@ -194,6 +194,9 @@
<view class="jump-col">{{mockMonthData.jumpDuration}}</view>
<view class="jump-col">{{mockMonthData.jumpAvgSpeed}}</view>
</view>
</view>
<view class="jump-summary-wrap" style="margin-top:30rpx">
<view class="jump-record-item" wx:for="{{mockMonthData.jumpRecords}}" wx:key="index">
<view class="jump-record-icon-wrap">
<image src="/static/student/train.png" class="jump-record-icon" mode="aspectFit" />
@ -203,7 +206,7 @@
<view class="jump-record-detail">{{item.duration}} {{item.count}}个</view>
</view>
</view>
</view>
</view>
</block>
<!-- 季度报告 -->
<block wx:elif="{{classroomSubTab === 2}}">

View File

@ -577,11 +577,13 @@
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 32rpx;
justify-content: space-evenly;
margin-top: 42rpx;
margin-top: 0;
}
.stat-card {
width: calc(50% - 8rpx);
width: calc(50% - 20rpx);
border-radius: 28rpx;
padding: 30rpx 26rpx 28rpx;
box-sizing: border-box;
@ -745,6 +747,7 @@
padding: 0 0 60rpx;
z-index: 201;
transform: translateY(100%);
padding-bottom: 220rpx;
transition: transform 0.3s ease;
box-shadow: 0 -8rpx 40rpx rgba(73,165,252,0.18);
}

View File

@ -44,6 +44,7 @@ Page({
nowFitnessClass: {}, // 当前体测班级
fitnessStudentList: [], // 体测学生列表
fitnessLoading: false,
courseLoading: false,
},
@ -66,7 +67,12 @@ Page({
// 切换 Tab
switchTab(e) {
const tab = e.currentTarget.dataset.tab
if(tab === this.data.activeTab) return
console.log('切换tab', tab)
this.setData({ activeTab: tab })
if(tab == 0 && !!this.data.nowCourse.id){
this.drawCharts(this)
}
},
// 获取体测班级列表
@ -105,6 +111,20 @@ Page({
})
},
// 计算课堂详情展示字段
_buildCourseDetail(data) {
const d = data || {}
const age = d.age || 0
const hrMax = 220 - age
const moderateHR = Math.round(hrMax * 0.6)
const highHR = Math.round(hrMax * 0.75)
const heartRange = `${d.minHR || 0}-${d.maxHR || 0}次/分`
const avgHeart = d.avgHR || 0
const highDuration = `${d.highIntensity || 0}分钟`
const highRatio = `${d.highIntensityDurationRatio || 0}%`
return { ...d, heartRange, avgHeart, highDuration, highRatio, _moderateHR: moderateHR, _highHR: highHR }
},
clickCouse(e) {
this.setData({
clickCouse: e.currentTarget.dataset.item
@ -114,19 +134,17 @@ Page({
async sureClickCouse() {
const that = this
if (!this.data.clickCouse.id) return app.showToast('请选择课程后再确认哦~')
wx.showLoading({
title: '加载中',
})
this.setData({ showDialog: false, courseLoading: true })
const res = await getClassRoomReportDetails(this.data.clickCouse.id)
const detail = this._buildCourseDetail(res.data)
that.setData({
nowCourse: this.data.clickCouse,
clickCouse: {},
showDialog: false,
nowCourseDetail: res.data
nowCourseDetail: detail,
courseLoading: false,
},()=>{
this.drawCharts(this)
})
wx.hideLoading()
},
showDialog() {
@ -160,17 +178,21 @@ Page({
console.log(isRefresh)
console.log('aaaaaaaaaaa',refresh)
if (!this.data.hasMore && !refresh) return //非刷新且没有
if (refresh) this.setData({ courseLoading: true })
const res = await getClassRoomRecord({
pageIndex: this.data.page,
PageSize: 20,
})
if((!!this.data.nowCourse && !this.data.nowCourse.id) && res.data.datas.length > 0){
const detail = await getClassRoomReportDetails(res.data.datas[0].id)
if(res.data.datas.length > 0){
const detail = await getClassRoomReportDetails(this.data.nowCourse.id ? this.data.nowCourse.id : res.data.datas[0].id)
this.setData({
nowCourseDetail:this.data.nowCourse.id ? this.data.nowCourseDetail: detail.data
nowCourseDetail: this.data.nowCourse.id ? this.data.nowCourseDetail: this._buildCourseDetail(detail.data),
courseLoading: false,
},()=>{
that.drawCharts(that)
})
} else {
if (refresh) this.setData({ courseLoading: false })
}
this.setData({
courseList: refresh ? res.data.datas : [...this.data.courseList, ...res.data.datas],
@ -256,8 +278,9 @@ Page({
animation: true,
background: "#FFFFFF",
color: that.data.chartType == 1 ? ["#FFA30C"] : that.data.chartType == 2 ? ["#6CB7FB"] : ["#FF4949"],
padding: [15, 15, 10, 0],
padding: [15, 15, 10, 15],
enableScroll: true,
enableMarkLine: true,
// touchMoveLimit: 24,
legend: {
show: false,
@ -284,6 +307,34 @@ Page({
// }]
},
extra: {
markLine: {
type: 'dash',
dashLength: 6,
data: [
{
value: that.data.nowCourseDetail._moderateHR,
lineColor: '#FFA30C',
showLabel: true,
labelText: '中强度',
labelFontColor: '#FFA30C',
labelBgColor: '#FFF8EC',
labelBgOpacity: 0.9,
labelAlign: 'left',
labelFontSize: 11
},
{
value: that.data.nowCourseDetail._highHR,
lineColor: '#FF4949',
showLabel: true,
labelText: '高强度',
labelFontColor: '#FF4949',
labelBgColor: '#FFF0F0',
labelBgOpacity: 0.9,
labelAlign: 'left',
labelFontSize: 11
}
]
},
line: {
type: "curve",
width: 2,
@ -372,24 +423,19 @@ Page({
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.setData({
page:1,
hasMore:true
},()=>{
if(this.data.isTrainer){
if(this.data.activeTab === 0){
this.getTrainClass(true)
}else if(this.data.activeTab === 1){
this.getFitnessStudents()
}
}else{
this.getClass()
async onPullDownRefresh() {
this.setData({ page: 1, hasMore: true })
if (this.data.isTrainer) {
if (this.data.activeTab === 0) {
await this.getTrainClass(true)
} else if (this.data.activeTab === 1) {
await this.getFitnessStudents()
}
wx.stopPullDownRefresh()
app.showToast('刷新成功')
})
} else {
await this.getClass()
}
wx.stopPullDownRefresh()
app.showToast('刷新成功')
},
/**

View File

@ -75,7 +75,29 @@
<image src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/downn-b.png" mode="" />
</view>
<!-- 骨架屏 -->
<block wx:if="{{courseLoading}}">
<view class="course-skeleton">
<view class="cs-chart-wrap">
<view class="cs-chart-label"></view>
<view class="cs-chart"></view>
</view>
<view class="cs-grid">
<view class="cs-card"></view>
<view class="cs-card"></view>
<view class="cs-card"></view>
<view class="cs-card"></view>
</view>
<view class="cs-list-title"></view>
<view class="cs-row"></view>
<view class="cs-row"></view>
<view class="cs-row"></view>
<view class="cs-row"></view>
</view>
</block>
<!-- 正式内容 -->
<block wx:else>
<view class="title">
<view class="title-before"></view>心率统计
</view>
@ -85,22 +107,40 @@
<canvas canvas-id="mycanvas" type="2d" id="mycanvas" style="width: {{cWidth}}px;height: {{cHeight}}px;" bindtouchstart="touchstart" bindtouchmove="touchmove" bindtouchend="touchend" />
</view>
<!-- <view class="hr-legend">
<view class="hr-legend-item">
<view class="hr-legend-line hr-legend-moderate"></view>
<text>中等强度 {{nowCourseDetail._moderateHR}}次/分</text>
</view>
<view class="hr-legend-item">
<view class="hr-legend-line hr-legend-high"></view>
<text>高强度 {{nowCourseDetail._highHR}}次/分</text>
</view>
</view> -->
</view>
<view class="heart-box">
<view class="heart-item ">
<view>最大心率</view>
<view class="heart-text3">{{nowCourseDetail.maxHR || '- -'}}</view>
<view class="stat-grid">
<view class="stat-card stat-card-1">
<view class="stat-card-bar"></view>
<text class="stat-card-label">心率范围</text>
<text class="stat-card-value">{{nowCourseDetail.heartRange}}</text>
</view>
<view class="stat-card stat-card-2">
<view class="stat-card-bar"></view>
<text class="stat-card-label">平均心率</text>
<text class="stat-card-value">{{nowCourseDetail.avgHeart}}</text>
</view>
<view class="stat-card stat-card-3">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高强度时长</text>
<text class="stat-card-value">{{nowCourseDetail.highDuration}}</text>
</view>
<view class="stat-card stat-card-4">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高时间占比</text>
<text class="stat-card-value">{{nowCourseDetail.highRatio}}</text>
</view>
</view>
<view class="heart-item heart-min">
<view>最小心率</view>
<view class="heart-text2">{{nowCourseDetail.minHR || '- -'}}</view>
</view>
<view class="heart-item heart-max">
<view>平均心率</view>
<view class="heart-text1">{{nowCourseDetail.avgHR || '- -'}}</view>
</view>
</view>
<view class="title">
<view class="title-before"></view>学生列表
@ -114,6 +154,7 @@
</view>
<mp-icon icon="arrow" size="{{13}}" color="#8c8c8c"></mp-icon>
</view>
</block>
<mp-half-screen-dialog bindbuttontap="buttontap" show="{{showDialog}}" title="课堂记录">
<view slot="desc">

View File

@ -29,6 +29,74 @@
box-shadow: 0 4rpx 16rpx rgba(38, 165, 246, 0.35);
}
/* ===== 心率图例 ===== */
.hr-legend {
display: flex;
justify-content: flex-end;
gap: 30rpx;
margin-top: 16rpx;
}
.hr-legend-item {
display: flex;
align-items: center;
font-size: 22rpx;
color: #666;
}
.hr-legend-line {
width: 36rpx;
height: 3rpx;
margin-right: 10rpx;
border-radius: 2rpx;
}
.hr-legend-moderate { background-color: #FFA30C; }
.hr-legend-high { background-color: #FF4949; }
/* ===== 心率统计卡片 ===== */
.stat-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
justify-content: space-evenly;
margin: 24rpx 30rpx;
}
.stat-card {
width: calc(50% - 20rpx);
border-radius: 28rpx;
padding: 30rpx 26rpx 28rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12rpx;
position: relative;
overflow: hidden;
box-shadow: 0 8rpx 28rpx rgba(0,0,0,0.13);
}
.stat-card-1 { background: linear-gradient(135deg, #ff5f6d 0%, #ffc371 100%); }
.stat-card-2 { background: linear-gradient(135deg, #0fd850 0%, #00c3a9 100%); }
.stat-card-3 { background: linear-gradient(135deg, #2980b9 0%, #6dd5fa 100%); }
.stat-card-4 { background: linear-gradient(135deg, #7f7fd5 0%, #86a8e7 100%); }
.stat-card-bar {
position: absolute;
width: 180rpx;
height: 180rpx;
border-radius: 50%;
background: rgba(255,255,255,0.13);
bottom: -50rpx;
right: -40rpx;
}
.stat-card-label {
font-size: 24rpx;
color: rgba(255,255,255,0.88);
}
.stat-card-value {
font-size: 38rpx;
font-weight: bold;
color: #fff;
word-break: break-all;
line-height: 1.2;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.12);
}
/* ===== 体测报告 header ===== */
.fitness-header {
display: flex;
@ -150,6 +218,79 @@
margin-left: 12rpx;
}
/* ===== 课堂报告骨架屏 ===== */
@keyframes cs-shimmer {
0% { background-position: -750rpx 0; }
100% { background-position: 750rpx 0; }
}
.course-skeleton {
padding: 0 30rpx 40rpx;
margin-top: 45rpx;
}
.cs-chart-wrap {
background: #fff;
border-radius: 24rpx;
padding: 24rpx 20rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
}
.cs-chart-label {
width: 80rpx;
height: 28rpx;
border-radius: 8rpx;
margin-bottom: 20rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cs-shimmer 1.4s infinite linear;
}
.cs-chart {
height: 280rpx;
border-radius: 16rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cs-shimmer 1.4s infinite linear;
}
.cs-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
justify-content: space-evenly;
margin-bottom: 32rpx;
}
.cs-card {
width: calc(50% - 20rpx);
height: 140rpx;
border-radius: 28rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cs-shimmer 1.4s infinite linear;
}
.cs-list-title {
width: 160rpx;
height: 32rpx;
border-radius: 8rpx;
margin-bottom: 24rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cs-shimmer 1.4s infinite linear;
}
.cs-row {
height: 100rpx;
border-radius: 16rpx;
margin-bottom: 16rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cs-shimmer 1.4s 0.1s infinite linear;
}
/* ===== 加载中 ===== */
.fitness-loading {
display: flex;

View File

@ -38,8 +38,16 @@ Page({
const hrMax = 220 - age
const moderateHR = Math.round(hrMax * 0.6) // 中等强度下限
const highHR = Math.round(hrMax * 0.75) // 高强度下限
const d = res.data || {}
const heartRange = `${d.minHR || 0}-${d.maxHR || 0}次/分`
const avgHeart = d.avgHR || 0
const highDuration = `${d.highIntensity || 0}分钟`
const highRatio = `${d.highIntensityDurationRatio || 0}%`
const jumpTotal = d.totalCount || 0
const jumpDuration = d.totalDuration || 0
const jumpAvgSpeed = d.avgSpeed || 0
this.setData({
reportDetail: res.data,
reportDetail: { ...d, heartRange, avgHeart, highDuration, highRatio, jumpTotal, jumpDuration, jumpAvgSpeed },
moderateHR,
highHR,
loading: false
@ -86,7 +94,7 @@ Page({
enableMarkLine: true,
background: "#FFFFFF",
color: that.data.chartType == 1 ? ["#FFA30C"] : that.data.chartType == 2 ? ["#6CB7FB"] : ["#FF4949"],
padding: [15, 15, 10, 0],
padding: [15, 15, 10, 15],
enableScroll: true,
// touchMoveLimit: 24,
legend: {

View File

@ -1,7 +1,6 @@
<!--subpackage/teacher/report-detail/report-detail.wxml-->
<!-- subpackage/teacher/report-detail/report-detail.wxml -->
<view class="page-content">
<mp-bar background="linear-gradient(190deg, #49a5fc 0%, #f7f7f7 800rpx, #f7f7f7 100%)" title="统计" color="#fff"></mp-bar>
<!-- 骨架屏 -->
<view wx:if="{{loading}}" class="skeleton">
<view class="skeleton-title">
@ -38,31 +37,29 @@
</view>
</view>
</view>
<!-- 正式内容 -->
<block wx:if="{{!loading}}">
<view class="title">
<view class="title-before"></view>心率统计
</view>
<view class="card canvasCard">
<view>次/分</view>
<view class="charts" style="width: {{cWidth}}px;height: {{cHeight}}px;">
<canvas canvas-id="mycanvas" type="2d" id="mycanvas" style="width: {{cWidth}}px;height: {{cHeight}}px;" bindtouchstart="touchstart" bindtouchmove="touchmove" bindtouchend="touchend" />
<view class="title">
<view class="title-before"></view>
心率统计
</view>
<view class="hr-legend">
<view class="hr-legend-item">
<view class="hr-legend-line hr-legend-moderate"></view>
<text>中等强度 {{moderateHR}}次/分</text>
<view class="card canvasCard">
<view>次/分</view>
<view class="charts" style="width: {{cWidth}}px;height: {{cHeight}}px;">
<canvas canvas-id="mycanvas" type="2d" id="mycanvas" style="width: {{cWidth}}px;height: {{cHeight}}px;" bindtouchstart="touchstart" bindtouchmove="touchmove" bindtouchend="touchend" />
</view>
<view class="hr-legend-item">
<view class="hr-legend-line hr-legend-high"></view>
<text>高强度 {{highHR}}次/分</text>
<view class="hr-legend">
<view class="hr-legend-item">
<view class="hr-legend-line hr-legend-moderate"></view>
<text>中等强度 {{moderateHR}}次/分</text>
</view>
<view class="hr-legend-item">
<view class="hr-legend-line hr-legend-high"></view>
<text>高强度 {{highHR}}次/分</text>
</view>
</view>
</view>
</view>
<!-- <view class="tp">
<!-- <view class="tp">
<view class="table-t">
<view class="no-b no-r">最大心率</view>
<view class="no-b no-r">最小心率</view>
@ -74,43 +71,66 @@
<view >{{reportDetail.avgHR || '- -'}}</view>
</view>
</view> -->
<view class="heart-box">
<view class="heart-item ">
<view>最大心率</view>
<view class="heart-text3">{{reportDetail.maxHR || '- -'}}</view>
<view class="stat-grid">
<view class="stat-card stat-card-1">
<view class="stat-card-bar"></view>
<text class="stat-card-label">心率范围</text>
<text class="stat-card-value">{{reportDetail.heartRange}}</text>
</view>
<view class="stat-card stat-card-2">
<view class="stat-card-bar"></view>
<text class="stat-card-label">平均心率</text>
<text class="stat-card-value">{{reportDetail.avgHeart}}</text>
</view>
<view class="stat-card stat-card-3">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高强度时长</text>
<text class="stat-card-value">{{reportDetail.highDuration}}</text>
</view>
<view class="stat-card stat-card-4">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高时间占比</text>
<text class="stat-card-value">{{reportDetail.highRatio}}</text>
</view>
</view>
<view class="heart-item heart-min">
<view>最小心率</view>
<view class="heart-text2">{{reportDetail.minHR || '- -'}}</view>
<view class="title">
<view class="title-before"></view>
跳绳记录
</view>
<view class="heart-item heart-max">
<view>平均心率</view>
<view class="heart-text1">{{reportDetail.avgHR || '- -'}}</view>
<view class="jump-summary-wrap">
<view class="jump-summary-header">
<view class="jump-col">个数</view>
<view class="jump-col">
时长
<text class="jump-unit">(分钟)</text>
</view>
<view class="jump-col">
平均速度
<text class="jump-unit">(个/分钟)</text>
</view>
</view>
<view class="jump-summary-data">
<view class="jump-col">{{reportDetail.jumpTotal}}</view>
<view class="jump-col">{{reportDetail.jumpDuration}}</view>
<view class="jump-col">{{reportDetail.jumpAvgSpeed}}</view>
</view>
</view>
</view>
<view class="title">
<view class="title-before"></view>跳绳记录
</view>
<view class="homework-item" wx:for="{{reportDetail.jumpRecordList}}" wx:key="index">
<view class="top">
<view class="left">
<view class="img-box">
<image class="jump-img" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/icon/Rope-Skipping.png" mode="" />
</view>
<view class="desc">
<view class="statu">
{{item.jumpType}}
<view class="homework-item" wx:for="{{reportDetail.jumpRecordList}}" wx:key="index">
<view class="top">
<view class="left">
<view class="img-box">
<image class="jump-img" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/icon/Rope-Skipping.png" mode="" />
</view>
<view class="desc">
<view class="statu">{{item.jumpType}}</view>
<view>
{{item.totalDuration}}s
<text style="margin-left: 30rpx;">{{item.totalCount}}个</text>
</view>
</view>
<view>{{item.totalDuration}}s <text style="margin-left: 30rpx;">{{item.totalCount}}个</text></view>
</view>
</view>
</view>
</view>
<view wx:if="{{reportDetail.jumpRecordList.length <= 0}}" class="no">暂无记录~</view>
<view wx:if="{{reportDetail.jumpRecordList.length <= 0}}" class="no">暂无记录~</view>
</block>
</view>

View File

@ -539,4 +539,166 @@
.skeleton-list-line-short {
width: 50%;
height: 26rpx;
}
/* ===== 统计指标网格(2列)===== */
.stat-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin: 0 30rpx;
margin-bottom: 32rpx;
justify-content: space-evenly;
margin-top: 42rpx;
}
.stat-card {
width: calc(50% - 20rpx);
border-radius: 28rpx;
padding: 30rpx 26rpx 28rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12rpx;
position: relative;
overflow: hidden;
box-shadow: 0 8rpx 28rpx rgba(0,0,0,0.13);
}
.stat-card-1 { background: linear-gradient(135deg, #ff5f6d 0%, #ffc371 100%); }
.stat-card-2 { background: linear-gradient(135deg, #0fd850 0%, #00c3a9 100%); }
.stat-card-3 { background: linear-gradient(135deg, #2980b9 0%, #6dd5fa 100%); }
.stat-card-4 { background: linear-gradient(135deg, #7f7fd5 0%, #86a8e7 100%); }
/* 装饰圆 */
.stat-card-bar {
position: absolute;
width: 180rpx;
height: 180rpx;
border-radius: 50%;
background: rgba(255,255,255,0.13);
bottom: -50rpx;
right: -40rpx;
flex-shrink: 0;
}
.stat-card-label {
font-size: 24rpx;
color: rgba(255,255,255,0.88);
font-weight: 400;
padding: 0;
}
.stat-card-value {
font-size: 38rpx;
font-weight: bold;
color: #fff;
padding: 0;
word-break: break-all;
line-height: 1.2;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.12);
}
.stat-card-1 .stat-card-value,
.stat-card-2 .stat-card-value,
.stat-card-3 .stat-card-value,
.stat-card-4 .stat-card-value { color: #fff; }
/* ===== 跳绳统计 ===== */
.jump-tip {
font-size: 22rpx;
color: #999;
margin-bottom: 16rpx;
}
.jump-summary-wrap {
background: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(73,165,252,0.1);
overflow: hidden;
margin:20rpx 30rpx;
margin-bottom: 10rpx;
}
.jump-summary-header {
display: flex;
background: #1d78e8;
padding: 18rpx 0;
}
.jump-summary-data {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f4f8;
}
.jump-col {
flex: 1;
text-align: center;
font-size: 24rpx;
color: #fff;
}
.jump-summary-data .jump-col {
color: #18394D;
font-size: 36rpx;
font-weight: bold;
}
.jump-unit {
font-size: 20rpx;
color: rgba(255,255,255,0.85);
}
.jump-record-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f4f8;
}
.jump-record-item:last-child {
border-bottom: none;
}
.jump-record-icon-wrap {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(135deg, #5ab4ff, #1d78e8);
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
overflow: hidden;
}
.jump-record-icon {
width: 54rpx;
height: 54rpx;
filter: brightness(10);
}
.jump-record-main {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.jump-record-tag {
display: inline-block;
background: #1d78e8;
color: #fff;
font-size: 22rpx;
padding: 4rpx 20rpx;
border-radius: 30rpx;
width: fit-content;
}
.jump-record-detail {
font-size: 26rpx;
color: #18394D;
padding-left: 4rpx;
}

View File

@ -1,6 +1,7 @@
// subpackage/teacher/student-fitness-detail/student-fitness-detail.js
import uCharts from '../../../utils/chart/u-charts';
import { getReport } from '../../../utils/serve/parent';
import { getStudentClassRoomReport } from '../../../utils/serve/teacher';
var uChartsInstance = {};
@ -24,17 +25,59 @@ Page({
selectedFitnessItem: null,
studentNo: '',
readyFired: false,
// Tab
activeTab: 0, // 0=课堂报告 1=体测报告
classroomSubTab: 0, // 0=月 1=季度
// 时间选择
selectedYear: 2026,
selectedMonth: 1,
selectedQuarter: 1,
showMonthPicker: false,
showQuarterPicker: false,
pickerYear: 2026,
nowYear: 0,
nowMonth: 0,
nowQuarter: 0,
monthList: [1,2,3,4,5,6,7,8,9,10,11,12],
quarterList: [
{ value: 1, label: '第一季度', months: '1-3月' },
{ value: 2, label: '第二季度', months: '4-6月' },
{ value: 3, label: '第三季度', months: '7-9月' },
{ value: 4, label: '第四季度', months: '10-12月' },
],
monthData: {},
quarterData: {},
classroomLoading: false,
},
async onLoad(options) {
const now = new Date()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth() + 1
const nowQuarter = Math.ceil(nowMonth / 3)
this.setData({
studentNo: options.studentNo || '',
nowYear, nowMonth, nowQuarter,
selectedYear: nowYear,
selectedMonth: nowMonth,
selectedQuarter: nowQuarter,
pickerYear: nowYear,
})
wx.showLoading({ title: '加载中' });
this.setData({ studentNo: options.studentNo || '' });
await Promise.all([
this._loadFitnessReport(options.studentNo),
this.fetchMonthReport(),
])
wx.hideLoading();
},
async _loadFitnessReport(studentNo) {
try {
const res = await getReport({ studentNo: options.studentNo });
const res = await getReport({ studentNo: studentNo || '' });
const rawList = (res.data && res.data.categoryScoreList) || [];
const list = rawList.filter(item => !(item.name && item.name.includes('米') && item.name.includes('跑')));
// 雷达图根据 gradeId 过滤不适用的项目
const gradeId = list.length > 0 ? list[0].gradeId : null;
let radarList = list;
if (gradeId !== null && gradeId !== undefined) {
@ -51,19 +94,14 @@ Page({
...item,
unit: UNIT_MAP[item.name] || '',
}));
this.setData({
fitnessReport: radarList,
fitnessItems,
}, () => {
// 如果 onReady 已经触发,数据到位后立即绘图
if (this.data.readyFired) {
this.setData({ fitnessReport: radarList, fitnessItems }, () => {
if (this.data.readyFired && this.data.activeTab === 1) {
this.drawRadarChart(this);
}
});
} catch (e) {
this.setData({ fitnessItems: [] });
}
wx.hideLoading();
},
onReady() {
@ -73,16 +111,137 @@ Page({
const radarCWidth = Math.floor(info.windowWidth - 2 * 30 * rpxRatio);
const radarCHeight = Math.floor(radarCWidth * 1.05);
this.setData({ radarCWidth, radarCHeight, pixelRatio, readyFired: true }, () => {
// 如果数据已经加载完,直接绘图;否则等 onLoad 回调里触发
if (this.data.fitnessReport && this.data.fitnessReport.length > 0) {
if (this.data.activeTab === 1 && this.data.fitnessReport && this.data.fitnessReport.length > 0) {
this.drawRadarChart(this);
}
});
},
// 切换主 Tab
switchTab(e) {
const tab = Number(e.currentTarget.dataset.tab)
if (tab === this.data.activeTab) return
this.setData({ activeTab: tab }, () => {
if (tab === 1 && this.data.readyFired && this.data.fitnessReport.length > 0) {
this.drawRadarChart(this)
}
})
},
// 切换课堂子Tab (0=月 1=季度)
switchClassroomSubTab(e) {
const tab = Number(e.currentTarget.dataset.tab)
if (tab === this.data.classroomSubTab) return
this.setData({ classroomSubTab: tab }, () => {
if (tab === 0) this.fetchMonthReport()
else if (tab === 1) this.fetchQuarterReport()
})
},
// 月份选择器
openMonthPicker() {
this.setData({ showMonthPicker: true, pickerYear: this.data.selectedYear })
},
closeMonthPicker() {
this.setData({ showMonthPicker: false })
},
selectMonth(e) {
const month = Number(e.currentTarget.dataset.month)
const { pickerYear, nowYear, nowMonth } = this.data
if (pickerYear > nowYear || (pickerYear === nowYear && month > nowMonth)) return
this.setData({ selectedMonth: month, selectedYear: pickerYear, showMonthPicker: false }, () => {
this.fetchMonthReport()
})
},
// 季度选择器
openQuarterPicker() {
this.setData({ showQuarterPicker: true, pickerYear: this.data.selectedYear })
},
closeQuarterPicker() {
this.setData({ showQuarterPicker: false })
},
selectQuarter(e) {
const quarter = Number(e.currentTarget.dataset.quarter)
const { pickerYear, nowYear, nowQuarter } = this.data
if (pickerYear > nowYear || (pickerYear === nowYear && quarter > nowQuarter)) return
this.setData({ selectedQuarter: quarter, selectedYear: pickerYear, showQuarterPicker: false }, () => {
this.fetchQuarterReport()
})
},
// 年份切换(月/季度共用)
prevPickerYear() {
this.setData({ pickerYear: this.data.pickerYear - 1 })
},
nextPickerYear() {
if (this.data.pickerYear >= this.data.nowYear) return
this.setData({ pickerYear: this.data.pickerYear + 1 })
},
_getMonthRange(year, month) {
const startTime = `${year}-${String(month).padStart(2, '0')}-01`
let endYear = year, endMonth = month + 1
if (endMonth > 12) { endYear++; endMonth = 1 }
const endTime = `${endYear}-${String(endMonth).padStart(2, '0')}-01`
return { startTime, endTime }
},
_getQuarterRange(year, quarter) {
const startMonth = (quarter - 1) * 3 + 1
let endYear = year, endMonth = quarter * 3 + 1
if (endMonth > 12) { endYear++; endMonth = 1 }
const startTime = `${year}-${String(startMonth).padStart(2, '0')}-01`
const endTime = `${endYear}-${String(endMonth).padStart(2, '0')}-01`
return { startTime, endTime }
},
_buildPeriodData(apiData) {
const d = apiData || {}
return {
heartRange: `${d.minHR || 0}-${d.maxHR || 0}次/分`,
avgHeart: d.avgHR || 0,
highDuration: `${d.highIntensity || 0}分钟`,
highRatio: `${d.highIntensityDurationRatio || 0}%`,
jumpTotal: d.totalCount || 0,
jumpDuration: d.totalDuration || 0,
jumpAvgSpeed: d.avgSpeed || 0,
jumpRecords: (d.jumpRecordList || []).map(item => ({
type: item.jumpType,
duration: `${item.totalDuration}s`,
count: item.totalCount,
})),
}
},
async fetchMonthReport() {
const { selectedYear, selectedMonth, studentNo } = this.data
const { startTime, endTime } = this._getMonthRange(selectedYear, selectedMonth)
this.setData({ classroomLoading: true })
try {
const res = await getStudentClassRoomReport({ type: 2, startTime, endTime, studentNo })
this.setData({ monthData: this._buildPeriodData(res.data) })
} catch (e) {
this.setData({ monthData: {} })
}
this.setData({ classroomLoading: false })
},
async fetchQuarterReport() {
const { selectedYear, selectedQuarter, studentNo } = this.data
const { startTime, endTime } = this._getQuarterRange(selectedYear, selectedQuarter)
this.setData({ classroomLoading: true })
try {
const res = await getStudentClassRoomReport({ type: 2, startTime, endTime, studentNo })
this.setData({ quarterData: this._buildPeriodData(res.data) })
} catch (e) {
this.setData({ quarterData: {} })
}
this.setData({ classroomLoading: false })
},
drawRadarChart(that) {
const list = that.data.fitnessReport || [];
// 长文本自动换行(超过5字在中间插入换行)
const categories = list.map(i => {
const name = i.name || '';
if (name.length > 5) {
@ -93,7 +252,6 @@ Page({
});
const prevData = list.map(i => i.prevScore || 0);
const currentData = list.map(i => i.lastScore || 0);
// 本次成绩数据标签:显示分数 + 等级
const currentDataLabels = list.map(i => {
const score = i.lastScore != null ? i.lastScore : '';
const rank = i.rank != null ? i.rank : '';
@ -174,7 +332,193 @@ Page({
onShow() {},
onHide() {},
onUnload() {},
onPullDownRefresh() {},
async onPullDownRefresh() {
if (this.data.activeTab === 0) {
if (this.data.classroomSubTab === 0) await this.fetchMonthReport()
else await this.fetchQuarterReport()
} else {
await this._loadFitnessReport(this.data.studentNo)
}
wx.stopPullDownRefresh()
wx.showToast({ title: '刷新成功', icon: 'none' })
},
onReachBottom() {},
onShareAppMessage() {},
})
// var uChartsInstance = {};
// const UNIT_MAP = {
// 'BMI': 'kg/m²',
// '肺活量': 'ml',
// '坐位体前屈': 'cm',
// '立定跳远': 'cm',
// '一分钟跳绳': '个',
// '一分钟仰卧起坐': '个',
// };
// Page({
// data: {
// fitnessItems: [],
// fitnessReport: [],
// radarCWidth: 300,
// radarCHeight: 300,
// pixelRatio: 2,
// showFitnessPopup: false,
// selectedFitnessItem: null,
// studentNo: '',
// readyFired: false,
// },
// async onLoad(options) {
// wx.showLoading({ title: '加载中' });
// this.setData({ studentNo: options.studentNo || '' });
// try {
// const res = await getReport({ studentNo: options.studentNo });
// const rawList = (res.data && res.data.categoryScoreList) || [];
// const list = rawList.filter(item => !(item.name && item.name.includes('米') && item.name.includes('跑')));
// // 雷达图根据 gradeId 过滤不适用的项目
// const gradeId = list.length > 0 ? list[0].gradeId : null;
// let radarList = list;
// if (gradeId !== null && gradeId !== undefined) {
// if (gradeId <= 2) {
// radarList = list.filter(item => item.name !== '一分钟仰卧起坐' && item.name !== '立定跳远');
// } else if (gradeId > 2 && gradeId <= 6) {
// radarList = list.filter(item => item.name !== '立定跳远');
// } else if (gradeId > 6) {
// radarList = list.filter(item => item.name !== '一分钟跳绳');
// }
// }
// const fitnessItems = list.map(item => ({
// ...item,
// unit: UNIT_MAP[item.name] || '',
// }));
// this.setData({
// fitnessReport: radarList,
// fitnessItems,
// }, () => {
// // 如果 onReady 已经触发,数据到位后立即绘图
// if (this.data.readyFired) {
// this.drawRadarChart(this);
// }
// });
// } catch (e) {
// this.setData({ fitnessItems: [] });
// }
// wx.hideLoading();
// },
// onReady() {
// const info = wx.getWindowInfo();
// const pixelRatio = info.pixelRatio;
// const rpxRatio = info.windowWidth / 750;
// const radarCWidth = Math.floor(info.windowWidth - 2 * 30 * rpxRatio);
// const radarCHeight = Math.floor(radarCWidth * 1.05);
// this.setData({ radarCWidth, radarCHeight, pixelRatio, readyFired: true }, () => {
// // 如果数据已经加载完,直接绘图;否则等 onLoad 回调里触发
// if (this.data.fitnessReport && this.data.fitnessReport.length > 0) {
// this.drawRadarChart(this);
// }
// });
// },
// drawRadarChart(that) {
// const list = that.data.fitnessReport || [];
// // 长文本自动换行(超过5字在中间插入换行)
// const categories = list.map(i => {
// const name = i.name || '';
// if (name.length > 5) {
// const mid = Math.ceil(name.length / 2);
// return name.slice(0, mid) + '\n' + name.slice(mid);
// }
// return name;
// });
// const prevData = list.map(i => i.prevScore || 0);
// const currentData = list.map(i => i.lastScore || 0);
// // 本次成绩数据标签:显示分数 + 等级
// const currentDataLabels = list.map(i => {
// const score = i.lastScore != null ? i.lastScore : '';
// const rank = i.rank != null ? i.rank : '';
// return rank ? score + '\n' + rank : String(score);
// });
// const query = wx.createSelectorQuery().in(that);
// query.select('#radarCanvas').fields({ node: true, size: true }).exec(res => {
// if (!res[0]) return;
// const canvas = res[0].node;
// const ctx = canvas.getContext('2d');
// canvas.width = (res[0].width + 20) * that.data.pixelRatio;
// canvas.height = (res[0].height + 20) * that.data.pixelRatio;
// uChartsInstance['radarCanvas'] = new uCharts({
// type: 'radar',
// context: ctx,
// width: that.data.radarCWidth * that.data.pixelRatio,
// height: that.data.radarCHeight * that.data.pixelRatio,
// categories,
// series: [
// { name: '上次成绩', data: prevData, color: '#4A90E2', dataLabel: false },
// { name: '本次成绩', data: currentData, color: '#E74C3C', dataLabels: currentDataLabels, textColor: '#E74C3C', textSize: 11 },
// ],
// pixelRatio: that.data.pixelRatio,
// animation: true,
// tapLegend: true,
// background: 'rgba(0,0,0,0)',
// color: ['#4A90E2', '#E74C3C'],
// padding: [0, 0, 0, 15],
// legend: {
// show: true,
// position: 'bottom',
// float: 'center',
// fontSize: 12,
// lineHeight: 11,
// margin: 0,
// itemGap: 20,
// },
// extra: {
// radar: {
// gridType: that.data.fitnessReport.length === 4 ? 'circle' : 'radar',
// gridColor: '#CCCCCC',
// gridCount: 4,
// opacity: 0.15,
// gridEvenShow: true,
// gridOddShow: true,
// labelShow: true,
// labelColor: '#333',
// fontSize: 13,
// labelMargin: 30,
// labelPointShow: false,
// max: 100,
// },
// },
// });
// });
// },
// showFitnessScore(e) {
// const item = e.currentTarget.dataset.item;
// this.setData({ selectedFitnessItem: item, showFitnessPopup: true });
// },
// closeFitnessPopup() {
// this.setData({ showFitnessPopup: false, selectedFitnessItem: null });
// },
// goFitnessDetail() {
// const item = this.data.selectedFitnessItem;
// if (!item) return;
// this.setData({ showFitnessPopup: false });
// wx.setStorageSync('fitnessDetailItem', item);
// wx.navigateTo({
// url: '/subpackage/parent/fitness-detail/fitness-detail',
// });
// },
// onShow() {},
// onHide() {},
// onUnload() {},
// onPullDownRefresh() {},
// onReachBottom() {},
// onShareAppMessage() {},
// })

View File

@ -1,41 +1,156 @@
<!--subpackage/teacher/student-fitness-detail/student-fitness-detail.wxml-->
<view class="page-wrap" style="padding: 0 30rpx; padding-bottom: 300rpx; box-sizing: border-box;">
<mp-bar background="linear-gradient(190deg, #49a5fc 0%, #f5f8ff 700rpx, #f5f8ff 100%)" title="体测报告" color="#fff" back="{{true}}"></mp-bar>
<mp-bar background="linear-gradient(190deg, #49a5fc 0%, #f5f8ff 700rpx, #f5f8ff 100%)" title="学生报告" color="#fff" back="{{true}}"></mp-bar>
<!-- 体测报告主体 -->
<view class="fitness-card">
<view class="fitness-chart-title">体测成绩对比</view>
<view class="radar-wrap" style="width:{{radarCWidth}}px;height:{{radarCHeight}}px;">
<canvas
type="2d"
id="radarCanvas"
canvas-id="radarCanvas"
style="width:{{radarCWidth}}px;height:{{radarCHeight}}px;"
/>
</view>
<!-- 主 Tab 切换 -->
<view class="tab-switcher">
<view class="tab-btn {{activeTab === 0 ? 'tab-btn-active' : ''}}" data-tab="0" bind:tap="switchTab">课堂报告</view>
<view class="tab-btn {{activeTab === 1 ? 'tab-btn-active' : ''}}" data-tab="1" bind:tap="switchTab">体测报告</view>
</view>
<!-- 六项指标卡片 -->
<view class="fitness-grid">
<view
class="fitness-item"
wx:for="{{fitnessItems}}"
wx:key="categoryValue"
data-item="{{item}}"
bind:tap="showFitnessScore"
>
<image src="{{item.imgUrl}}" class="fitness-icon" mode="aspectFit" />
<view class="fitness-name">{{item.name}}</view>
<view class="fitness-score">{{item.lastScore}}<text class="fitness-unit"> {{item.unit}}</text></view>
<!-- 课堂报告 -->
<block wx:if="{{activeTab === 0}}">
<!-- 子Tab: 月 / 季度 -->
<view class="sub-tab-switcher">
<view class="sub-tab-btn {{classroomSubTab === 0 ? 'sub-tab-active' : ''}}" data-tab="0" bind:tap="switchClassroomSubTab">月</view>
<view class="sub-tab-btn {{classroomSubTab === 1 ? 'sub-tab-active' : ''}}" data-tab="1" bind:tap="switchClassroomSubTab">季度</view>
</view>
</view>
<!-- 空状态 -->
<view wx:if="{{fitnessItems.length === 0}}" class="empty-tip">暂无体测数据</view>
<!-- 月报告 -->
<block wx:if="{{classroomSubTab === 0}}">
<view class="period-selector" bind:tap="openMonthPicker">
<text class="period-text">{{selectedYear}}年{{selectedMonth}}月</text>
<text class="period-caret">▾</text>
</view>
<view class="stat-section-header-wrap"><view class="stat-section-header">心率统计</view></view>
<view class="stat-grid">
<view class="stat-card stat-card-1">
<view class="stat-card-bar"></view>
<text class="stat-card-label">心率范围</text>
<text class="stat-card-value">{{monthData.heartRange}}</text>
</view>
<view class="stat-card stat-card-2">
<view class="stat-card-bar"></view>
<text class="stat-card-label">平均心率</text>
<text class="stat-card-value">{{monthData.avgHeart}}</text>
</view>
<view class="stat-card stat-card-3">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高强度时长</text>
<text class="stat-card-value">{{monthData.highDuration}}</text>
</view>
<view class="stat-card stat-card-4">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高时间占比</text>
<text class="stat-card-value">{{monthData.highRatio}}</text>
</view>
</view>
<view class="stat-section-header-wrap"><view class="stat-section-header">跳绳统计</view></view>
<view class="jump-tip">训练记录统计:仅智能跳绳,不含普通跳绳</view>
<view class="jump-summary-wrap">
<view class="jump-summary-header">
<view class="jump-col">个数</view>
<view class="jump-col">时长<text class="jump-unit">(分钟)</text></view>
<view class="jump-col">平均速度<text class="jump-unit">(个/分钟)</text></view>
</view>
<view class="jump-summary-data">
<view class="jump-col">{{monthData.jumpTotal}}</view>
<view class="jump-col">{{monthData.jumpDuration}}</view>
<view class="jump-col">{{monthData.jumpAvgSpeed}}</view>
</view>
</view>
<view class="jump-summary-wrap" style="margin-top:30rpx">
<view class="jump-record-item" wx:for="{{monthData.jumpRecords}}" wx:key="index">
<view class="jump-record-icon-wrap">
<image src="/static/student/train.png" class="jump-record-icon" mode="aspectFit" />
</view>
<view class="jump-record-main">
<view class="jump-record-tag">{{item.type}}</view>
<view class="jump-record-detail">{{item.duration}} {{item.count}}个</view>
</view>
</view>
</view>
</block>
<!-- 季度报告 -->
<block wx:elif="{{classroomSubTab === 1}}">
<view class="period-selector" bind:tap="openQuarterPicker">
<text class="period-text">{{selectedYear}}年 第{{selectedQuarter}}季度</text>
<text class="period-caret">▾</text>
</view>
<view class="stat-section-header-wrap"><view class="stat-section-header">心率统计</view></view>
<view class="stat-grid">
<view class="stat-card stat-card-1">
<view class="stat-card-bar"></view>
<text class="stat-card-label">心率范围</text>
<text class="stat-card-value">{{quarterData.heartRange}}</text>
</view>
<view class="stat-card stat-card-2">
<view class="stat-card-bar"></view>
<text class="stat-card-label">平均心率</text>
<text class="stat-card-value">{{quarterData.avgHeart}}</text>
</view>
<view class="stat-card stat-card-3">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高强度时长</text>
<text class="stat-card-value">{{quarterData.highDuration}}</text>
</view>
<view class="stat-card stat-card-4">
<view class="stat-card-bar"></view>
<text class="stat-card-label">中高时间占比</text>
<text class="stat-card-value">{{quarterData.highRatio}}</text>
</view>
</view>
<view class="stat-section-header-wrap"><view class="stat-section-header">跳绳统计</view></view>
<view class="jump-tip">训练记录统计:仅智能跳绳,不含普通跳绳</view>
<view class="jump-summary-wrap">
<view class="jump-summary-header">
<view class="jump-col">个数</view>
<view class="jump-col">时长<text class="jump-unit">(分钟)</text></view>
<view class="jump-col">平均速度<text class="jump-unit">(个/分钟)</text></view>
</view>
<view class="jump-summary-data">
<view class="jump-col">{{quarterData.jumpTotal}}</view>
<view class="jump-col">{{quarterData.jumpDuration}}</view>
<view class="jump-col">{{quarterData.jumpAvgSpeed}}</view>
</view>
</view>
<view class="jump-summary-wrap" style="margin-top:30rpx">
<view class="jump-record-item" wx:for="{{quarterData.jumpRecords}}" wx:key="index">
<view class="jump-record-icon-wrap">
<image src="/static/student/train.png" class="jump-record-icon" mode="aspectFit" />
</view>
<view class="jump-record-main">
<view class="jump-record-tag">{{item.type}}</view>
<view class="jump-record-detail">{{item.duration}} {{item.count}}个</view>
</view>
</view>
</view>
</block>
</block>
<!-- 体测报告 -->
<block wx:elif="{{activeTab === 1}}">
<view class="fitness-card">
<view class="fitness-chart-title">体测成绩对比</view>
<view class="radar-wrap" style="width:{{radarCWidth}}px;height:{{radarCHeight}}px;">
<canvas type="2d" id="radarCanvas" canvas-id="radarCanvas" style="width:{{radarCWidth}}px;height:{{radarCHeight}}px;" />
</view>
</view>
<view class="fitness-grid">
<view class="fitness-item" wx:for="{{fitnessItems}}" wx:key="categoryValue" data-item="{{item}}" bind:tap="showFitnessScore">
<image src="{{item.imgUrl}}" class="fitness-icon" mode="aspectFit" />
<view class="fitness-name">{{item.name}}</view>
<view class="fitness-score">{{item.lastScore}}<text class="fitness-unit"> {{item.unit}}</text></view>
</view>
</view>
<view wx:if="{{fitnessItems.length === 0}}" class="empty-tip">暂无体测数据</view>
</block>
<!-- 成绩弹窗遮罩 -->
<view class="popup-mask" wx:if="{{showFitnessPopup}}" bind:tap="closeFitnessPopup"></view>
<!-- 成绩弹窗 -->
<view class="score-popup {{showFitnessPopup ? 'popup-show' : ''}}">
<view class="popup-header">
@ -43,7 +158,6 @@
<text class="popup-title">{{selectedFitnessItem.name}}</text>
<view class="popup-close" bind:tap="closeFitnessPopup">✕</view>
</view>
<view class="score-rows">
<view class="score-row">
<view class="score-label">本次成绩</view>
@ -53,17 +167,50 @@
<view class="score-label">上次成绩</view>
<view class="score-value">{{selectedFitnessItem.prevScore}}<text class="score-unit"> {{selectedFitnessItem.unit}}</text></view>
</view>
<!-- <view class="score-divider"></view>
<view class="score-row">
<view class="score-label">班级平均</view>
<view class="score-value">{{selectedFitnessItem.classAvgScore}}<text class="score-unit"> {{selectedFitnessItem.unit}}</text></view>
</view>
<view class="score-row">
<view class="score-label">机构平均</view>
<view class="score-value">{{selectedFitnessItem.schoolAvgScore}}<text class="score-unit"> {{selectedFitnessItem.unit}}</text></view>
</view> -->
</view>
<view class="popup-btn" bind:tap="goFitnessDetail">查看详细趋势</view>
</view>
</view>
<!-- 月份选择弹窗 -->
<view class="picker-mask" wx:if="{{showMonthPicker}}" bind:tap="closeMonthPicker"></view>
<view class="picker-sheet {{showMonthPicker ? 'picker-sheet-show' : ''}}">
<view class="picker-header">
<text class="picker-cancel" bind:tap="closeMonthPicker">取消</text>
<text class="picker-title">选择月份</text>
<text class="picker-confirm" style="visibility:hidden">确定</text>
</view>
<view class="picker-year-row">
<view class="picker-year-arrow" bind:tap="prevPickerYear">‹</view>
<text class="picker-year-text">{{pickerYear}}年</text>
<view class="picker-year-arrow {{pickerYear >= nowYear ? 'picker-year-arrow-disabled' : ''}}" bind:tap="nextPickerYear">›</view>
</view>
<view class="picker-month-grid">
<view class="picker-month-item {{(item === selectedMonth && pickerYear === selectedYear) ? 'picker-item-active' : ''}} {{(pickerYear === nowYear && item > nowMonth) ? 'picker-item-disabled' : ''}}"
wx:for="{{monthList}}" wx:key="*this" data-month="{{item}}" bind:tap="selectMonth">
{{item}}月
</view>
</view>
</view>
<!-- 季度选择弹窗 -->
<view class="picker-mask" wx:if="{{showQuarterPicker}}" bind:tap="closeQuarterPicker"></view>
<view class="picker-sheet {{showQuarterPicker ? 'picker-sheet-show' : ''}}">
<view class="picker-header">
<text class="picker-cancel" bind:tap="closeQuarterPicker">取消</text>
<text class="picker-title">选择季度</text>
<text class="picker-confirm" style="visibility:hidden">确定</text>
</view>
<view class="picker-year-row">
<view class="picker-year-arrow" bind:tap="prevPickerYear">‹</view>
<text class="picker-year-text">{{pickerYear}}年</text>
<view class="picker-year-arrow {{pickerYear >= nowYear ? 'picker-year-arrow-disabled' : ''}}" bind:tap="nextPickerYear">›</view>
</view>
<view class="picker-quarter-list">
<view class="picker-quarter-item {{(item.value === selectedQuarter && pickerYear === selectedYear) ? 'picker-item-active' : ''}} {{(pickerYear === nowYear && item.value > nowQuarter) ? 'picker-item-disabled' : ''}}"
wx:for="{{quarterList}}" wx:key="value" data-quarter="{{item.value}}" bind:tap="selectQuarter">
<text class="picker-quarter-label">{{item.label}}</text>
<text class="picker-quarter-months">{{item.months}}</text>
</view>
</view>
</view>
</view>

View File

@ -375,3 +375,394 @@
border-radius: 12rpx;
margin-bottom: 16rpx;
}
/* ===== 主 Tab 切换 ===== */
.tab-switcher {
display: flex;
background: #e8f0fe;
border-radius: 60rpx;
padding: 6rpx;
margin: 24rpx 0 0;
}
.tab-btn {
flex: 1;
text-align: center;
padding: 20rpx 0;
border-radius: 54rpx;
font-size: 28rpx;
color: #26A5F6;
transition: all 0.25s;
}
.tab-btn-active {
background: linear-gradient(135deg, #26A5F6 0%, #1478D4 100%);
color: #fff;
font-weight: bold;
box-shadow: 0 4rpx 16rpx rgba(38,165,246,0.35);
}
/* ===== 课堂报告子Tab ===== */
.sub-tab-switcher {
display: flex;
background: #e8f0fe;
border-radius: 20rpx;
padding: 1rpx;
margin: 30rpx 200rpx 32rpx;
}
.sub-tab-btn {
flex: 1;
text-align: center;
padding: 20rpx 0;
border-radius: 16rpx;
font-size: 27rpx;
color: #6b7280;
}
.sub-tab-active {
background: linear-gradient(135deg, #1d78e8, #49a5fc);
color: #fff;
font-weight: bold;
box-shadow: 0 4rpx 16rpx rgba(29,120,232,0.35);
}
/* ===== 周期选择器入口 ===== */
.period-selector {
display: flex;
align-items: center;
justify-content: center;
background: #f0f7ff;
border-radius: 50rpx;
padding: 14rpx 40rpx;
margin: 0 auto 28rpx;
border: 1rpx solid #c8dff8;
width: fit-content;
}
.period-text {
font-size: 28rpx;
color: #1d78e8;
font-weight: 600;
}
.period-caret {
font-size: 24rpx;
color: #1d78e8;
margin-left: 8rpx;
}
/* ===== 统计区块标题 ===== */
.stat-section-header-wrap {
display: flex;
align-items: center;
margin: 16rpx 0 20rpx;
gap: 16rpx;
}
.stat-section-header-wrap::before,
.stat-section-header-wrap::after {
content: '';
flex: 1;
height: 1rpx;
background: linear-gradient(90deg, transparent, #cce0f8);
}
.stat-section-header-wrap::after {
background: linear-gradient(90deg, #cce0f8, transparent);
}
.stat-section-header {
color: #1d78e8;
font-size: 26rpx;
font-weight: bold;
white-space: nowrap;
}
/* ===== 统计指标网格 ===== */
.stat-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 32rpx;
justify-content: space-evenly;
}
.stat-card {
width: calc(50% - 20rpx);
border-radius: 28rpx;
padding: 30rpx 26rpx 28rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12rpx;
position: relative;
overflow: hidden;
box-shadow: 0 8rpx 28rpx rgba(0,0,0,0.13);
}
.stat-card-1 { background: linear-gradient(135deg, #ff5f6d 0%, #ffc371 100%); }
.stat-card-2 { background: linear-gradient(135deg, #0fd850 0%, #00c3a9 100%); }
.stat-card-3 { background: linear-gradient(135deg, #2980b9 0%, #6dd5fa 100%); }
.stat-card-4 { background: linear-gradient(135deg, #7f7fd5 0%, #86a8e7 100%); }
.stat-card-bar {
position: absolute;
width: 180rpx;
height: 180rpx;
border-radius: 50%;
background: rgba(255,255,255,0.13);
bottom: -50rpx;
right: -40rpx;
}
.stat-card-label {
font-size: 24rpx;
color: rgba(255,255,255,0.88);
}
.stat-card-value {
font-size: 38rpx;
font-weight: bold;
color: #fff;
word-break: break-all;
line-height: 1.2;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.12);
}
/* ===== 跳绳统计 ===== */
.jump-tip {
font-size: 22rpx;
color: #999;
margin-bottom: 16rpx;
}
.jump-summary-wrap {
background: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(73,165,252,0.1);
overflow: hidden;
margin-bottom: 10rpx;
}
.jump-summary-header {
display: flex;
background: #1d78e8;
padding: 18rpx 0;
}
.jump-summary-data {
display: flex;
padding: 20rpx 0;
}
.jump-col {
flex: 1;
text-align: center;
font-size: 24rpx;
color: #fff;
}
.jump-summary-data .jump-col {
color: #18394D;
font-size: 36rpx;
font-weight: bold;
}
.jump-unit {
font-size: 20rpx;
color: rgba(255,255,255,0.85);
}
.jump-record-item {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f0f4f8;
}
.jump-record-item:last-child { border-bottom: none; }
.jump-record-icon-wrap {
width: 72rpx;
height: 72rpx;
background: linear-gradient(135deg, #49a5fc, #1d78e8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
overflow: hidden;
}
.jump-record-icon {
width: 54rpx;
height: 54rpx;
filter: brightness(10);
}
.jump-record-main {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.jump-record-tag {
display: inline-block;
background: #1d78e8;
color: #fff;
font-size: 22rpx;
padding: 4rpx 20rpx;
border-radius: 30rpx;
width: fit-content;
}
.jump-record-detail {
font-size: 26rpx;
color: #18394D;
padding-left: 4rpx;
}
/* ===== 月份/季度选择弹窗 ===== */
.picker-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.45);
z-index: 200;
}
.picker-sheet {
position: fixed;
left: 0; right: 0; bottom: 0;
background: #fff;
border-radius: 36rpx 36rpx 0 0;
padding-bottom: 220rpx;
z-index: 201;
transform: translateY(100%);
transition: transform 0.3s ease;
box-shadow: 0 -8rpx 40rpx rgba(73,165,252,0.18);
}
.picker-sheet-show { transform: translateY(0); }
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 40rpx 20rpx;
border-bottom: 1rpx solid #f0f4f8;
}
.picker-title {
font-size: 32rpx;
font-weight: bold;
color: #18394D;
}
.picker-cancel {
font-size: 28rpx;
color: #999;
}
.picker-confirm {
font-size: 28rpx;
color: #1d78e8;
font-weight: bold;
}
.picker-year-row {
display: flex;
align-items: center;
justify-content: center;
padding: 28rpx 0 20rpx;
gap: 48rpx;
}
.picker-year-text {
font-size: 34rpx;
font-weight: bold;
color: #18394D;
min-width: 160rpx;
text-align: center;
}
.picker-year-arrow {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #f0f6ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 44rpx;
color: #1d78e8;
line-height: 1;
text-align: center;
}
.picker-month-grid {
display: flex;
flex-wrap: wrap;
padding: 8rpx 30rpx 20rpx;
gap: 20rpx;
}
.picker-month-item {
width: calc(25% - 16rpx);
text-align: center;
padding: 20rpx 0;
border-radius: 16rpx;
font-size: 28rpx;
color: #333;
background: #f5f7fa;
box-sizing: border-box;
}
.picker-item-active {
background: linear-gradient(135deg, #49a5fc, #1d78e8);
color: #fff;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(73,165,252,0.4);
}
.picker-quarter-list {
padding: 16rpx 40rpx 20rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.picker-quarter-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 26rpx 36rpx;
border-radius: 20rpx;
background: #f5f7fa;
}
.picker-quarter-item.picker-item-active {
background: linear-gradient(135deg, #49a5fc, #1d78e8);
}
.picker-quarter-label {
font-size: 30rpx;
font-weight: bold;
color: #18394D;
}
.picker-quarter-item.picker-item-active .picker-quarter-label,
.picker-quarter-item.picker-item-active .picker-quarter-months { color: #fff; }
.picker-quarter-months {
font-size: 24rpx;
color: #999;
}
.picker-item-disabled { opacity: 0.3; }
.picker-year-arrow-disabled { opacity: 0.3; color: #ccc; }
/* ===== 课堂报告骨架屏 ===== */
@keyframes cr-shimmer {
0% { background-position: -750rpx 0; }
100% { background-position: 750rpx 0; }
}
.cr-skeleton {
padding: 0 30rpx 40rpx;
}
.cr-sk-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 32rpx;
}
.cr-sk-card {
width: calc(50% - 10rpx);
height: 140rpx;
border-radius: 16rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cr-shimmer 1.4s infinite linear;
}
.cr-sk-divider {
height: 2rpx;
background: #f0f0f0;
margin-bottom: 32rpx;
}
.cr-sk-table {
height: 90rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cr-shimmer 1.4s infinite linear;
}
.cr-sk-row {
height: 72rpx;
border-radius: 12rpx;
margin-bottom: 16rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 750rpx 100%;
animation: cr-shimmer 1.4s 0.1s infinite linear;
}