// 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 = {}; 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, // Tab activeTab: 1, // 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: '加载中' }); await Promise.all([ this._loadFitnessReport(options.studentNo), this.fetchMonthReport(), ]) wx.hideLoading(); }, async _loadFitnessReport(studentNo) { try { 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('跑'))); 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 }, () => { if (this.data.readyFired && this.data.activeTab === 1) { this.drawRadarChart(this); } }); } catch (e) { this.setData({ fitnessItems: [] }); } }, 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 }, () => { 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 || []; 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, 10], 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() {}, 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() {}, // })