// 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() { } })