diff --git a/app.json b/app.json index 778614a..3f406ee 100644 --- a/app.json +++ b/app.json @@ -71,7 +71,8 @@ "teacher/report-detail/report-detail", "user/apply-school/apply-school", "user/redeem-class/redeem-class", - "parent/fitness-detail/fitness-detail" + "parent/fitness-detail/fitness-detail", + "teacher/student-fitness-detail/student-fitness-detail" ] } ], diff --git a/pages/teacher/count/count.js b/pages/teacher/count/count.js index 74e5a61..2310b4a 100644 --- a/pages/teacher/count/count.js +++ b/pages/teacher/count/count.js @@ -38,6 +38,12 @@ Page({ cWidth: 330, cHeight: 280, pixelRatio: 2, + // 体测报告 + activeTab: 0, // 0-课堂报告 1-体测报告 + fitnessClassList: [], // 体测班级列表 + nowFitnessClass: {}, // 当前体测班级 + fitnessStudentList: [], // 体测学生列表 + fitnessLoading: false, }, @@ -50,12 +56,55 @@ Page({ }) if (wx.getStorageSync('roleId') && wx.getStorageSync('roleId') == 4) { await this.getTrainClass(true) + await this.getFitnessClasses() } else { await this.getClass() } wx.hideLoading() }, + // 切换 Tab + switchTab(e) { + const tab = e.currentTarget.dataset.tab + this.setData({ activeTab: tab }) + }, + + // 获取体测班级列表 + async getFitnessClasses() { + const res = await getClassList() + if (res && res.data && res.data.length > 0) { + this.setData({ + fitnessClassList: res.data, + nowFitnessClass: res.data[0] + }, () => { + this.getFitnessStudents() + }) + } + }, + + // 体测班级切换 + fitnessPicker(e) { + const nowFitnessClass = this.data.fitnessClassList[Number(e.detail.value)] + this.setData({ nowFitnessClass }, () => { + this.getFitnessStudents() + }) + }, + + // 获取体测学生列表 + async getFitnessStudents() { + if (!this.data.nowFitnessClass.classId) return + this.setData({ fitnessLoading: true }) + const res = await getStudentList({ + pageIndex: 1, + pageSize: 200, + classId: this.data.nowFitnessClass.classId + }) + this.setData({ + fitnessStudentList: (res && res.data && res.data.datas) ? res.data.datas : (res && res.data ? res.data : []), + fitnessLoading: false + }) + }, + clickCouse(e) { this.setData({ clickCouse: e.currentTarget.dataset.item @@ -115,7 +164,7 @@ Page({ pageIndex: this.data.page, PageSize: 20, }) - if(!this.data.nowCourse.id){ + if(!this.data.nowCourse.id && res.data.datas.length > 0){ const detail = await getClassRoomReportDetails(res.data.datas[0].id) this.setData({ nowCourseDetail:this.data.nowCourse.id ? this.data.nowCourseDetail: detail.data @@ -133,6 +182,7 @@ Page({ //获取项目列表 async getProject() { + if(!this.data.nowClass || !this.data.nowClass.gradeId) return const res = await getProjectList(this.data.nowClass.gradeId) this.setData({ projectList: res.data, @@ -142,6 +192,7 @@ Page({ //获取学生列表 刷新 加载更多 async getStudent(isRefresh) { + if (!this.data.nowClass || !this.data.nowClass.classId) return if (!this.data.hasMore && !isRefresh) return //非刷新且没有 const res = await getTongji({ ClassId: this.data.nowClass.classId, @@ -294,9 +345,14 @@ Page({ wx.navigateTo({ url: '/subpackage/teacher/report-detail/report-detail?id='+this.data.nowCourse.id +'&studentNo='+e.currentTarget.dataset.item.studentNo, }) - // wx.navigateTo({ - // url: '/subpackage/teacher/report-list/report-list?studentNo='+e.currentTarget.dataset.item.studentNo, - // }) + }, + + // 体测报告 - 跳转学生体测详情 + goFitnessDetail(e) { + const item = e.currentTarget.dataset.item + wx.navigateTo({ + url: '/subpackage/teacher/student-fitness-detail/student-fitness-detail?studentNo=' + item.studentNo + '&classId=' + this.data.nowFitnessClass.classId + }) }, /** diff --git a/pages/teacher/count/count.wxml b/pages/teacher/count/count.wxml index f84333c..f4b7e08 100644 --- a/pages/teacher/count/count.wxml +++ b/pages/teacher/count/count.wxml @@ -59,6 +59,18 @@ + + + + 课堂报告 + + + 体测报告 + + + + + {{nowCourse.startingEndingTime}} @@ -90,21 +102,6 @@ - - 学生列表 @@ -114,9 +111,7 @@ {{item.studentName}} - - @@ -131,7 +126,6 @@ - 加载中... 没有更多数据了 @@ -141,5 +135,58 @@ + + + + + + + + + + + {{nowFitnessClass.name || '请选择班级'}} + + + + + + 共 {{fitnessStudentList.length}} 人 + + + + + + + 加载中... + + + + + + + + + + + {{item.studentName || item.name}} + + + + 学号:{{item.studentNo}} + + + + + + + + + + + 📋 + 暂无学生数据 + + \ No newline at end of file diff --git a/pages/teacher/count/count.wxss b/pages/teacher/count/count.wxss index e8bcb7b..2681acb 100644 --- a/pages/teacher/count/count.wxss +++ b/pages/teacher/count/count.wxss @@ -1,4 +1,180 @@ /* pages/teacher/count/count.wxss */ + +/* ===== Tab 切换 ===== */ +.report-tabs { + display: flex; + margin: 24rpx 30rpx 0; + background-color: #EAF4FF; + border-radius: 60rpx; + padding: 6rpx; + box-sizing: border-box; +} + +.report-tab-item { + flex: 1; + height: 68rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 54rpx; + font-size: 28rpx; + color: #26A5F6; + transition: all 0.25s; +} + +.report-tab-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); +} + +/* ===== 体测报告 header ===== */ +.fitness-header { + display: flex; + align-items: center; + justify-content: space-between; + margin: 30rpx 30rpx 16rpx; +} + +.fitness-class-picker { + flex: 1; + margin-right: 20rpx; +} + +.fitness-picker-btn { + display: flex; + align-items: center; + justify-content: center; + height: 72rpx; + background: linear-gradient(135deg, #26A5F6 0%, #1478D4 100%); + border-radius: 50rpx; + padding: 0 24rpx; + box-shadow: 0 4rpx 16rpx rgba(38, 165, 246, 0.3); +} + +.fitness-picker-btn text { + color: #fff; + font-size: 26rpx; + font-weight: bold; + flex: 1; + text-align: center; +} + +.picker-icon { + display: none; +} + +.picker-arrow { + width: 24rpx; + height: 24rpx; + filter: brightness(10); +} + +.fitness-count-badge { + background-color: #EAF4FF; + border: 2rpx solid #26A5F6; + border-radius: 30rpx; + padding: 8rpx 24rpx; + white-space: nowrap; +} + +.fitness-count-badge text { + font-size: 24rpx; + color: #26A5F6; +} + +/* ===== 体测学生卡片 ===== */ +.fitness-student-card { + display: flex; + align-items: center; + background-color: #fff; + border-radius: 20rpx; + margin: 18rpx 30rpx; + padding: 24rpx 28rpx; + box-shadow: 0 4rpx 20rpx rgba(38, 165, 246, 0.08); + border-left: 6rpx solid #26A5F6; +} + +.fsc-avatar-wrap { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + background: linear-gradient(135deg, #74C7FF 0%, #26A5F6 100%); + padding: 4rpx; + box-sizing: border-box; + flex-shrink: 0; +} + +.fsc-avatar { + width: 100%; + height: 100%; + border-radius: 50%; + border: 3rpx solid #fff; + background-color: #e8f5ff; +} + +.fsc-info { + flex: 1; + margin-left: 24rpx; +} + +.fsc-name-row { + display: flex; + align-items: center; +} + +.fsc-name { + font-size: 30rpx; + font-weight: bold; + color: #222; +} + +.fsc-sex { + width: 36rpx; + height: 36rpx; + margin-left: 12rpx; +} + +.fsc-meta { + margin-top: 8rpx; +} + +.fsc-no { + font-size: 22rpx; + color: #999; +} + +.fsc-arrow { + flex-shrink: 0; + margin-left: 12rpx; +} + +/* ===== 加载中 ===== */ +.fitness-loading { + display: flex; + flex-direction: column; + align-items: center; + padding: 80rpx 0; + color: #26A5F6; + font-size: 26rpx; +} + +/* ===== 空状态 ===== */ +.fitness-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 120rpx 0 60rpx; + color: #bbb; + font-size: 26rpx; +} + +.fitness-empty-icon { + font-size: 80rpx; + margin-bottom: 20rpx; +} + .card{ background-color: #fff; box-shadow: 0 0 10rpx rgb(223, 222, 222); diff --git a/subpackage/teacher/student-fitness-detail/student-fitness-detail.js b/subpackage/teacher/student-fitness-detail/student-fitness-detail.js new file mode 100644 index 0000000..bccbb8f --- /dev/null +++ b/subpackage/teacher/student-fitness-detail/student-fitness-detail.js @@ -0,0 +1,148 @@ +// subpackage/teacher/student-fitness-detail/student-fitness-detail.js +import uCharts from '../../../utils/chart/u-charts'; +import { getReport } from '../../../utils/serve/parent'; + +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 list = (res.data && res.data.categoryScoreList) || []; + const fitnessItems = list.map(item => ({ + ...item, + unit: UNIT_MAP[item.name] || '', + })); + this.setData({ + fitnessReport: list, + 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 radarCWidth = info.windowWidth * 0.86; + const radarCHeight = info.windowWidth * 0.86; + 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 || []; + const categories = list.map(i => i.name); + const prevData = list.map(i => i.prevScore || 0); + const currentData = list.map(i => i.lastScore || 0); + + 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' }, + { name: '本次成绩', data: currentData, color: '#E74C3C' }, + ], + pixelRatio: that.data.pixelRatio, + animation: 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: 5, + itemGap: 20, + }, + extra: { + radar: { + gridType: 'radar', + gridColor: '#CCCCCC', + gridCount: 4, + opacity: 0.15, + gridEvenShow: true, + gridOddShow: true, + labelShow: true, + labelColor: '#333', + fontSize: 13, + 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() {}, +}) diff --git a/subpackage/teacher/student-fitness-detail/student-fitness-detail.json b/subpackage/teacher/student-fitness-detail/student-fitness-detail.json new file mode 100644 index 0000000..48e4106 --- /dev/null +++ b/subpackage/teacher/student-fitness-detail/student-fitness-detail.json @@ -0,0 +1,9 @@ +{ + "usingComponents": { + "mp-bar": "weui-miniprogram/navigation-bar/navigation-bar", + "mp-icon": "weui-miniprogram/icon/icon" + }, + "backgroundColorContent": "#f0f7ff", + "backgroundColor": "#f0f7ff", + "navigationStyle": "custom" +} diff --git a/subpackage/teacher/student-fitness-detail/student-fitness-detail.wxml b/subpackage/teacher/student-fitness-detail/student-fitness-detail.wxml new file mode 100644 index 0000000..2d4085d --- /dev/null +++ b/subpackage/teacher/student-fitness-detail/student-fitness-detail.wxml @@ -0,0 +1,69 @@ + + + + + + + 体测成绩对比 + + + + + + + + + + {{item.name}} + {{item.lastScore}} {{item.unit}} + + + + + 暂无体测数据 + + + + + + + + + {{selectedFitnessItem.name}} + ✕ + + + + + 本次成绩 + {{selectedFitnessItem.lastScore}} {{selectedFitnessItem.unit}} + + + 上次成绩 + {{selectedFitnessItem.prevScore}} {{selectedFitnessItem.unit}} + + + + 班级平均 + {{selectedFitnessItem.classAvgScore}} {{selectedFitnessItem.unit}} + + + 机构平均 + {{selectedFitnessItem.schoolAvgScore}} {{selectedFitnessItem.unit}} + + + + 查看详细趋势 + + diff --git a/subpackage/teacher/student-fitness-detail/student-fitness-detail.wxss b/subpackage/teacher/student-fitness-detail/student-fitness-detail.wxss new file mode 100644 index 0000000..8ba3821 --- /dev/null +++ b/subpackage/teacher/student-fitness-detail/student-fitness-detail.wxss @@ -0,0 +1,377 @@ +/* subpackage/teacher/student-fitness-detail/student-fitness-detail.wxss */ + +/* ===== 成绩弹窗 ===== */ +.popup-mask { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.45); + z-index: 999; +} + +.score-popup { + position: fixed; + left: 0; right: 0; bottom: 0; + background: #fff; + border-radius: 36rpx 36rpx 0 0; + padding: 36rpx 40rpx 60rpx; + z-index: 9999; + transform: translateY(100%); + transition: transform 0.3s ease; + box-shadow: 0 -8rpx 40rpx rgba(73,165,252,0.18); +} + +.popup-show { + transform: translateY(0); +} + +.popup-header { + display: flex; + align-items: center; + margin-bottom: 36rpx; +} + +.popup-icon { + width: 88rpx; + height: 88rpx; + margin-right: 20rpx; + border-radius: 50%; + /* background: linear-gradient(135deg, #8dcfff, #49a5fc); */ + /* padding: 14rpx; */ + box-sizing: border-box; +} + +.popup-title { + flex: 1; + font-size: 34rpx; + font-weight: bold; + color: #18394D; +} + +.popup-close { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: #f0f4f8; + display: flex; + align-items: center; + justify-content: center; + font-size: 26rpx; + color: #999; +} + +.score-rows { + background: #f7fbff; + border-radius: 20rpx; + padding: 8rpx 30rpx; + margin-bottom: 36rpx; +} + +.score-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx 0; + border-bottom: 1rpx solid #eef2f8; +} + +.score-row:last-child { + border-bottom: none; +} + +.score-divider { + height: 2rpx; + background: linear-gradient(90deg, transparent, #d0e8ff, transparent); + margin: 4rpx 0; +} + +.score-label { + font-size: 28rpx; + color: #666; +} + +.score-value { + font-size: 34rpx; + font-weight: bold; + color: #333; +} + +.score-highlight { + color: #1d78e8; + font-size: 40rpx; +} + +.score-unit { + font-size: 22rpx; + color: #999; + font-weight: normal; +} + +.popup-btn { + background: linear-gradient(135deg, #49a5fc, #1d78e8); + color: #fff; + text-align: center; + padding: 28rpx 0; + border-radius: 50rpx; + font-size: 30rpx; + font-weight: bold; + box-shadow: 0 6rpx 20rpx rgba(73,165,252,0.4); + letter-spacing: 2rpx; +} + +/* ===== 体测报告卡片 ===== */ +.fitness-card { + background: #fff; + border-radius: 24rpx; + padding: 30rpx 20rpx 10rpx; + box-shadow: 0 4rpx 20rpx rgba(73,165,252,0.12); + margin-bottom: 30rpx; + margin-top: 20rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.fitness-chart-title { + font-size: 30rpx; + font-weight: bold; + color: #18394D; + margin-bottom: 16rpx; + position: relative; + padding: 0 30rpx; +} + +.fitness-chart-title::before, +.fitness-chart-title::after { + content: ''; + display: inline-block; + width: 60rpx; + height: 4rpx; + background: linear-gradient(90deg, transparent, #49a5fc); + vertical-align: middle; + margin-right: 16rpx; + border-radius: 2rpx; +} + +.fitness-chart-title::after { + background: linear-gradient(90deg, #49a5fc, transparent); + margin-right: 0; + margin-left: 16rpx; +} + +.radar-wrap { + overflow: hidden; +} + +/* ===== 六项指标网格 ===== */ +.fitness-grid { + display: flex; + flex-wrap: wrap; + gap: 20rpx; + margin-bottom: 10rpx; +} + +.fitness-item { + width: calc(33.33% - 14rpx); + background: #fff; + border-radius: 20rpx; + box-shadow: 0 4rpx 16rpx rgba(73,165,252,0.12); + display: flex; + flex-direction: column; + align-items: center; + padding: 30rpx 10rpx 24rpx; + box-sizing: border-box; + active-opacity: 0.7; +} + +.fitness-item:active { + transform: scale(0.96); +} + +.fitness-icon { + width: 88rpx; + height: 88rpx; +} + +.fitness-name { + font-size: 24rpx; + color: #333; + font-weight: 500; + text-align: center; + margin-bottom: 8rpx; +} + +.fitness-score { + font-size: 30rpx; + font-weight: bold; + color: #1d78e8; +} + +.fitness-unit { + font-size: 20rpx; + color: #999; + font-weight: normal; +} + +/* ===== 空状态 ===== */ +.empty-tip { + text-align: center; + padding: 80rpx 0; + color: #bbb; + font-size: 26rpx; + + flex-wrap: wrap; + gap: 10rpx; +} + +.meta-tag { + background-color: rgba(255, 255, 255, 0.25); + border-radius: 20rpx; + padding: 4rpx 18rpx; + font-size: 22rpx; + color: #fff; +} + +.score-wrap { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 20rpx; + padding: 16rpx 28rpx; + flex-shrink: 0; +} + +.score-num { + font-size: 52rpx; + font-weight: bold; + color: #fff; + line-height: 1; +} + + +/* ===== 章节标题 ===== */ +.section-title { + display: flex; + align-items: center; + margin: 36rpx 30rpx 16rpx; + font-size: 28rpx; + font-weight: bold; + color: #222; +} + +.title-bar { + width: 6rpx; + height: 28rpx; + border-radius: 6rpx; + background: linear-gradient(180deg, #26A5F6, #1478D4); + margin-right: 16rpx; +} + +/* ===== 成绩表格 ===== */ +.table-card { + background-color: #fff; + border-radius: 24rpx; + margin: 0 30rpx 40rpx; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(38, 165, 246, 0.1); +} + +.table-row { + display: grid; + grid-template-columns: 2fr 1.5fr 1fr 1.2fr; + align-items: center; + padding: 0 24rpx; + min-height: 90rpx; +} + +.table-header { + background: linear-gradient(90deg, #EAF4FF, #f5faff); + font-size: 24rpx; + font-weight: bold; + color: #26A5F6; + min-height: 72rpx; + border-bottom: 2rpx solid #e8f3ff; +} + +.row-even { + background-color: #fff; + border-bottom: 2rpx solid #f5f5f5; +} + +.row-odd { + background-color: #f8fcff; + border-bottom: 2rpx solid #f5f5f5; +} + +.col-name { + font-size: 26rpx; + color: #333; + padding-right: 10rpx; + word-break: break-all; +} + +.col { + font-size: 25rpx; + color: #555; + text-align: center; +} + +.col-score { + font-weight: bold; + color: #26A5F6; +} + +.col-rank { + text-align: center; + border-radius: 20rpx; + font-size: 22rpx; + padding: 4rpx 0; +} + +.col-rank.rank-1 { + color: #1aad19; + background-color: #e8f9e8; +} + +.col-rank.rank-2 { + color: #26A5F6; + background-color: #eaf4ff; +} + +.col-rank.rank-3 { + color: #ff9900; + background-color: #fff7e8; +} + +.col-rank.rank-4 { + color: #e64340; + background-color: #fff0f0; +} + +/* ===== 空状态 ===== */ +.empty { + text-align: center; + padding: 80rpx 0; + color: #bbb; + font-size: 26rpx; +} + +/* ===== 加载骨架 ===== */ +.skeleton { + padding: 24rpx 30rpx; +} + +.sk-header { + height: 160rpx; + background: linear-gradient(90deg, #dbeeff 25%, #c8e5ff 50%, #dbeeff 75%); + border-radius: 24rpx; + margin-bottom: 40rpx; +} + +.sk-row { + height: 80rpx; + background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%); + border-radius: 12rpx; + margin-bottom: 16rpx; +}