2026-05-06 09:16:21 +08:00

524 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// pages/parent/report/report.js
const tabService = require("../../../utils/tab-serve");
import uCharts from '../../../utils/chart/u-charts';
import {
getReport,
getPhysicalFitnessReport
} from '../../../utils/serve/parent'
import { getClassRoomRecord, getStudentClassRoomReport } from '../../../utils/serve/teacher';
import { getWeekdayWithValidation } from '../../../utils/utils';
var uChartsInstance = {};
// 各指标单位映射(接口不返回单位)
const UNIT_MAP = {
'BMI': 'kg/m²',
'肺活量': 'ml',
'坐位体前屈': 'cm',
'立定跳远': 'cm',
'一分钟跳绳': '个',
'一分钟仰卧起坐': '个',
};
Page({
/**
* 页面的初始数据
*/
data: {
report: {},
list: [],
isTrainee: false,
pageIndex: 1,
hasMore: true,
roomList: [],
// 体测报告
activeTab: 0, // 0=课堂报告 1=体测报告
fitnessItems: [], // 六个指标卡片
fitnessReport: null, // 接口返回的完整体测数据
radarCWidth: 300,
radarCHeight: 300,
pixelRatio: 2,
showFitnessPopup: false, // 成绩弹窗
selectedFitnessItem: null,
// 课堂报告子Tab (0=课堂 1=月 2=季度)
classroomSubTab: 0,
selectedYear: 2026,
selectedMonth: 3,
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月' },
],
mockMonthData: {},
mockQuarterData: {},
},
/**
* 生命周期函数--监听页面加载
*/
async onLoad(options) {
const now = new Date()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth() + 1
const nowQuarter = Math.ceil(nowMonth / 3)
this.setData({
nowYear, nowMonth, nowQuarter,
selectedYear: nowYear,
selectedMonth: nowMonth,
selectedQuarter: nowQuarter,
pickerYear: nowYear,
})
if(wx.getStorageSync('roleId') && wx.getStorageSync('roleId') == 5 ){
this.setData({
isTrainee: true
}, () => {
this.getTrainClass(true)
this.fetchFitnessReport()
})
} else {
await this.getStudentReport()
}
},
async getStudentReport() {
const res = await getReport()
let list = []
res.data.categoryScoreList.forEach(e => {
list.push({
...e,
width: e.score >= 60 && e.score <= 80 ? (((e.score - 60) / 4)*5 + 25) + '%' : ((e.score - 60) / 0.4 + 2) + '%',
grade: e.score >= 90 ? 'grate' : e.score >= 80 ? 'good' : e.score >= 60 ? 'normal' : ''
})
})
res.data.width = res.data.totalScore >= 60 && res.data.totalScore <= 80 ? (((res.data.totalScore - 60) / 4)*5 + 25) + '%' : ((res.data.totalScore - 60) / 0.4 + 2) + '%',
res.data.grade = res.data.totalScore >= 90 ? 'grate' : res.data.totalScore >= 80 ? 'good' : res.data.totalScore >= 60 ? 'normal' : ''
res.data.totalScore = res.data.totalScore.toFixed(0)
res.data.bodyFunction = res.data.bodyFunction.toFixed(0)
res.data.bodyQuality = res.data.bodyQuality.toFixed(0)
res.data.bodyShape = res.data.bodyShape.toFixed(0)
this.setData({
report: res.data,
list
})
},
// 切换体测/课堂 Tab
switchTab(e) {
const tab = Number(e.currentTarget.dataset.tab)
if (tab === this.data.activeTab) return
this.setData({ activeTab: tab }, () => {
if (tab === 1) {
// 切到体测报告时绘制雷达图
this.drawRadarChart(this)
}
})
},
// 切换课堂报告子Tab (课堂/月/季度)
switchClassroomSubTab(e) {
const tab = Number(e.currentTarget.dataset.tab)
if (tab === this.data.classroomSubTab) return
this.setData({ classroomSubTab: tab }, () => {
if (tab === 1) this.fetchMonthReport()
else if (tab === 2) 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 } = 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()
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,
})
} catch (e) {
this.setData({ fitnessItems: [] })
}
},
// 绘制六边形雷达图
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 ''
return rank ? score + '\n' + rank : String(score)
})
// 富文本 label:名称行 + 颜色区分的成绩行(上次/本次+等级)
const labelLines = list.map(i => {
const name = i.name || ''
const lines = []
// 名称行,超5字自动换行
if (name.length > 15) {
const mid = Math.ceil(name.length / 2)
lines.push(name.slice(0, mid))
lines.push(name.slice(mid))
} else {
lines.push(name)
}
// 成绩行:上次(蓝)/本次+等级(红)
const prevScore = i.prevScore != null ? String(i.prevScore) : '-'
const lastScore = i.lastScore != null ? String(i.lastScore) : '-'
const rank = i.rank ? i.rank : ''
const currentText = rank ? lastScore + rank : lastScore
lines.push([
{ text: prevScore, color: '#4A90E2' },
{ text: ' / ', color: '#999999' },
{ text: currentText, color: '#E74C3C' },
])
return lines
})
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', dataLabels: currentDataLabels },
{ 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, 20],
legend: {
show: true,
position: 'bottom',
float: 'center',
fontSize: 12,
lineHeight: 11,
margin: 0,
itemGap: 20,
},
extra: {
radar: {
gridType: this.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,
labelLines,
},
},
})
})
},
// 点击图标:展示成绩弹窗
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',
})
},
//获取教练课堂记录 刷新 加载更多
async getTrainClass(refresh) {
if (!this.data.hasMore && !refresh) return //非刷新且没有
const res = await getClassRoomRecord({
pageIndex: this.data.pageIndex,
PageSize: 20,
})
let newList = res.data.datas.map(item => {
return {
...item,
time: getWeekdayWithValidation(Number(item.startingEndingTime.slice(0, 4)), Number(item.startingEndingTime.slice(5, 7)), Number(item.startingEndingTime.slice(8, 10)))
}
})
this.setData({
roomList: refresh ? newList : [...this.data.roomList, ...newList],
page: this.data.page + 1,
hasMore: res.data.total > (this.data.pageIndex) * 20,
})
if(this.data.isTrainee && this.data.activeTab === 1){
this.drawRadarChart(this)
}
},
//查看报告详情
goReportDetail(e){
wx.navigateTo({
url: '/subpackage/teacher/report-detail/report-detail?id='+e.currentTarget.id ,
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
const windowInfo = wx.getWindowInfo()
const pixelRatio = windowInfo.pixelRatio
// 宽度 = 屏幕宽度 - 页面两侧 padding(30rpx×2),不计 card padding(由负 margin 抵消)
const rpxRatio = windowInfo.windowWidth / 750
const radarCWidth = Math.floor(windowInfo.windowWidth - 2 * 30 * rpxRatio)
const radarCHeight = Math.floor(radarCWidth * 1.05) // 稍高,给换行标签和 rank 留出空间
this.setData({ radarCWidth, radarCHeight, pixelRatio }, () => {
if (this.data.isTrainee && this.data.activeTab === 1) {
this.drawRadarChart(this)
}
})
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
tabService.updateIndex(this, 2)
if(wx.getStorageSync('roleId') && wx.getStorageSync('roleId') == 5){ //学员
this.setData({
isTrainee:true
})
}
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
async onPullDownRefresh() {
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 {
await this.getStudentReport()
wx.stopPullDownRefresh()
wx.showToast({ title: '刷新成功!', icon: 'none' })
}
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
this.getTrainClass()
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})