update:学生端课堂报告查看

This commit is contained in:
ltx 2026-08-04 09:59:35 +08:00
parent 3efa7b0ad8
commit 844cde0fb6
8 changed files with 505 additions and 303 deletions

View File

@ -61,6 +61,7 @@ Page({
],
mockMonthData: {},
mockQuarterData: {},
studentReportLoading: false,
},
/**
@ -71,62 +72,95 @@ Page({
const nowYear = now.getFullYear()
const nowMonth = now.getMonth() + 1
const nowQuarter = Math.ceil(nowMonth / 3)
const isTrainee = wx.getStorageSync('roleId') && wx.getStorageSync('roleId') == 5
this.setData({
nowYear, nowMonth, nowQuarter,
selectedYear: nowYear,
selectedMonth: nowMonth,
selectedQuarter: nowQuarter,
pickerYear: nowYear,
isTrainee,
activeTab: isTrainee ? 0 : 1,
})
if(wx.getStorageSync('roleId') && wx.getStorageSync('roleId') == 5 ){
this.setData({
isTrainee: true
}, () => {
this.getTrainClass(true)
this.fetchFitnessReport()
})
await this.getTrainClass(true)
if (isTrainee) {
await 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
})
_toScoreValue(value) {
const score = Number(value || 0)
return Number.isFinite(score) ? score : 0
},
_formatScore(value) {
return this._toScoreValue(value).toFixed(0)
},
_getScoreGrade(score) {
const value = this._toScoreValue(score)
return value >= 90 ? 'grate' : value >= 80 ? 'good' : value >= 60 ? 'normal' : ''
},
_getScoreWidth(score) {
const value = Math.max(0, Math.min(100, this._toScoreValue(score)))
if (value >= 60 && value <= 80) return (((value - 60) / 4) * 5 + 25) + '%'
return Math.max(2, (value - 60) / 0.4 + 2) + '%'
},
_hasReportRecord(item) {
const scoreTrendChart = (item && item.scoreTrendChart) || {}
const axisY = Array.isArray(scoreTrendChart.axisY) ? scoreTrendChart.axisY : []
return axisY.length > 0
},
_getAverageScore(items) {
const valid = (items || []).filter(item => item && item.hasRecord)
if (!valid.length) return 0
return valid.reduce((sum, item) => sum + this._toScoreValue(item.lastScore), 0) / valid.length
},
_buildStudentReport(rawData) {
const sourceList = (rawData && rawData.categoryScoreList) || []
const list = sourceList.map(item => {
const score = this._toScoreValue(item.lastScore)
const scoreTrendChart = item.scoreTrendChart || { axisX: [], axisY: [] }
const reportItem = { ...item, scoreTrendChart }
const hasRecord = this._hasReportRecord(reportItem)
return { ...reportItem, hasRecord, score: this._formatScore(score), width: this._getScoreWidth(score), grade: this._getScoreGrade(score) }
}).filter(item => item.hasRecord)
const shapeList = list.filter(item => item.categoryValue === 1)
const functionList = list.filter(item => item.categoryValue === 2)
const qualityList = list.filter(item => item.categoryValue !== 1 && item.categoryValue !== 2)
const totalScore = this._getAverageScore(list)
return {
report: { totalScore: this._formatScore(totalScore), bodyShape: this._formatScore(this._getAverageScore(shapeList)), bodyFunction: this._formatScore(this._getAverageScore(functionList)), bodyQuality: this._formatScore(this._getAverageScore(qualityList)), width: this._getScoreWidth(totalScore), grade: this._getScoreGrade(totalScore) },
list,
}
},
async getStudentReport() {
this.setData({ studentReportLoading: true })
try {
const res = await getReport()
const viewData = this._buildStudentReport(res.data || {})
this.setData({ ...viewData, fitnessItems: viewData.list.map(item => ({ ...item, unit: UNIT_MAP[item.name] || '' })), studentReportLoading: false })
} catch (e) {
this.setData({ report: {}, list: [], fitnessItems: [], studentReportLoading: false })
}
},
// 切换体测/课堂 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)
}
if (tab === 0 && this.data.roomList.length === 0) this.getTrainClass(true)
if (this.data.isTrainee && tab === 1) this.drawRadarChart(this)
})
},
// 切换课堂报告子Tab (课堂/月/季度)
switchClassroomSubTab(e) {
const tab = Number(e.currentTarget.dataset.tab)
if (tab === this.data.classroomSubTab) return
@ -409,7 +443,7 @@ Page({
})
this.setData({
roomList: refresh ? newList : [...this.data.roomList, ...newList],
page: this.data.page + 1,
pageIndex: this.data.pageIndex + 1,
hasMore: res.data.total > (this.data.pageIndex) * 20,
})
@ -447,16 +481,10 @@ Page({
*/
onShow() {
tabService.updateIndex(this, 2)
if(wx.getStorageSync('roleId') && wx.getStorageSync('roleId') == 5){ //学员
this.setData({
isTrainee:true
})
}
const isTrainee = wx.getStorageSync('roleId') && wx.getStorageSync('roleId') == 5
this.setData({ isTrainee })
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
@ -472,53 +500,40 @@ Page({
* 页面相关事件处理函数--监听用户下拉动作
*/
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()
const { activeTab, classroomSubTab, isTrainee } = this.data
if (activeTab === 0) {
if (classroomSubTab === 0) {
this.setData({ pageIndex: 1, hasMore: true }, async () => {
await this.getTrainClass(true)
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)
})
} 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 (isTrainee && activeTab === 1) {
await this.fetchFitnessReport()
this.drawRadarChart(this)
wx.stopPullDownRefresh()
wx.showToast({ title: '刷新成功', icon: 'none' })
} else {
await this.getStudentReport()
wx.stopPullDownRefresh()
wx.showToast({ title: '刷新成功!', icon: 'none' })
wx.showToast({ title: '刷新成功', icon: 'none' })
}
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
this.getTrainClass()
if (this.data.activeTab === 0 && this.data.classroomSubTab === 0) this.getTrainClass()
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
})

View File

@ -1,8 +1,17 @@
<!-- pages/parent/report/report.wxml -->
<view class="page-content" style="padding: 0 30rpx; padding-bottom: 300rpx;box-sizing: border-box;">
<mp-bar background="linear-gradient(190deg, #49a5fc 0%, #f7f7f7 800rpx, #f7f7f7 100%)" title="报告" color="#fff" back="{{false}}"></mp-bar>
<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">
{{isTrainee ? '体测报告' : '学生报告'}}
</view>
</view>
<!-- 非学员:原有学生报告 -->
<view class="top" wx:if="{{!isTrainee}}">
<view class="top" wx:if="{{!isTrainee && activeTab === 1 && list && list.length > 0}}">
<view class="tit">
<image src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/heart.png" class="heart" />
健康身体 坚持运动
@ -102,17 +111,12 @@
</view>
</view>
</view>
<!-- 学员:双Tab报告 -->
<view wx:else class="trainee-wrap">
<!-- 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 class="report-empty" wx:if="{{!isTrainee && activeTab === 1 && !studentReportLoading && (!list || list.length === 0)}}">
<view class="report-empty-icon">暂无</view>
<text>暂无学生报告数据</text>
</view>
<!-- 课堂/体测报告 -->
<view wx:if="{{isTrainee || activeTab === 0}}" class="trainee-wrap">
<!-- 课堂报告 -->
<block wx:if="{{activeTab === 0}}">
<!-- 子Tab: 课堂 / 月 / 季度 -->
@ -136,8 +140,12 @@
</view>
<mp-icon icon="arrow" size="{{13}}" color="#ccc"></mp-icon>
</view>
<view class="more-tip" wx:if="{{hasMore}}">加载中...</view>
<view class="more-tip" wx:else>没有更多了哦~</view>
<view class="more-tip" wx:if="{{roomList.length > 0 && hasMore}}">加载中...</view>
<view class="more-tip" wx:elif="{{roomList.length > 0 && !hasMore}}">没有更多了哦~</view>
<view class="report-empty" wx:if="{{!roomList || roomList.length === 0}}">
<view class="report-empty-icon">暂无</view>
<text>暂无课堂报告数据</text>
</view>
</block>
<!-- 月报告 -->
<block wx:elif="{{classroomSubTab === 1}}">
@ -278,7 +286,7 @@
</block>
</block>
<!-- 体测报告 -->
<block wx:elif="{{activeTab === 1}}">
<block wx:if="{{isTrainee && activeTab === 1}}">
<view class="fitness-card">
<view class="fitness-chart-title">体测成绩对比</view>
<!-- 雷达图 Canvas -->
@ -381,4 +389,4 @@
</view>
</view>
</view>
</view>
</view>

View File

@ -882,4 +882,32 @@
.picker-year-arrow-disabled {
opacity: 0.3;
color: #ccc;
}
}
.report-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 260rpx;
margin: 30rpx 0;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(73,165,252,0.1);
color: #9aa7b2;
font-size: 26rpx;
}
.report-empty-icon {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 18rpx;
background: #eef8ff;
color: #26A5F6;
font-size: 24rpx;
font-weight: 600;
}

View File

@ -1,147 +1,164 @@
const utils = require("../../../utils/utils");
// subpackage/teacher/resource-detail/resource-detail.js
Page({
/**
* 页面的初始数据
*/
data: {
title: '口令',
value: '00:00',
duration: '00:00',
aduio:null,
data:{},
isPlay:false,
percent:0,
isReload:false,//是否重复播放
isShow:false,//是否展示弹窗
title: "音乐",
data: {},
playlist: [],
currentIndex: 0,
value: "00:00",
duration: "00:00",
percent: 0,
isPlay: false,
isSingleLoop: false,
isShow: false,
isClosing: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log(JSON.parse(options.data))
let current = {};
try {
current = JSON.parse(decodeURIComponent(options.data || "{}"));
} catch (error) {
console.error("资源参数解析失败", error);
}
this.setData({
data: JSON.parse(options.data),
title: JSON.parse(options.data).id == 1 ? '口令' : JSON.parse(options.data).id == 2 ? '音乐' : '视频'
})
this.loadAudio(JSON.parse(options.data).resourceUrl)
data: current,
playlist: current.resourceUrl ? [current] : [],
title: current.id == 1 ? "口令" : "音乐"
});
this.createAudioContext();
if (current.resourceUrl) {
this.loadTrack(current, false);
}
const eventChannel = this.getOpenerEventChannel && this.getOpenerEventChannel();
if (eventChannel && eventChannel.on) {
eventChannel.on("setPlaylist", ({ playlist = [], currentIndex = 0 }) => {
if (!playlist.length) return;
const safeIndex = Math.min(Math.max(Number(currentIndex) || 0, 0), playlist.length - 1);
this.setData({ playlist, currentIndex: safeIndex, data: playlist[safeIndex] });
});
}
},
loadAudio(url) {
createAudioContext() {
this.audioCtx = wx.createInnerAudioContext();
this.audioCtx.src = url
this.audioCtx.onCanplay(() => {
this.audioCtx.duration
this.audioCtx.currentTime
console.log(this.audioCtx.currentTime)
console.log(this.audioCtx.duration)
console.log(this.audioCtx.currentTime)
this.setData({
value: utils.formatTimeStr(this.audioCtx.currentTime),
duration: utils.formatTimeStr(this.audioCtx.duration),
})
})
this.audioCtx.obeyMuteSwitch = false;
this.audioCtx.onTimeUpdate(() => {
console.log('在播放咯')
console.log(this.data.percent)
console.log('在播放咯')
this.audioCtx.duration
this.audioCtx.currentTime
this.setData({
value: utils.formatTimeStr(this.audioCtx.currentTime),
duration: utils.formatTimeStr(this.audioCtx.duration),
percent:((this.audioCtx.currentTime/this.audioCtx.duration)*100).toFixed(0)
})
})
this.audioCtx.onCanplay(() => this.updateProgress());
this.audioCtx.onTimeUpdate(() => this.updateProgress());
this.audioCtx.onPlay(() => this.setData({ isPlay: true }));
this.audioCtx.onPause(() => this.setData({ isPlay: false }));
this.audioCtx.onStop(() => this.setData({ isPlay: false }));
this.audioCtx.onEnded(() => {
if (!this.data.isSingleLoop) this.changeTrack(1, true);
});
this.audioCtx.onError((error) => {
console.error("音频播放失败", error);
this.setData({ isPlay: false });
wx.showToast({ title: "音频加载失败,请稍后重试", icon: "none" });
});
},
btnCilck(){
console.log(this.audioCtx.src)
if(this.data.isPlay){
this.audioCtx.pause()
this.setData({
isPlay:false
})
}else{
this.audioCtx.play()
this.setData({
isPlay:true
})
loadTrack(track, autoplay = true) {
if (!track || !track.resourceUrl || !this.audioCtx) return;
this.audioCtx.stop();
this.audioCtx.loop = this.data.isSingleLoop;
this.audioCtx.src = track.resourceUrl;
this.setData({
data: track,
value: "00:00",
duration: "00:00",
percent: 0,
isPlay: autoplay
});
if (autoplay) this.audioCtx.play();
},
updateProgress() {
if (!this.audioCtx) return;
const currentTime = Number(this.audioCtx.currentTime) || 0;
const duration = Number(this.audioCtx.duration) || 0;
this.setData({
value: utils.formatTimeStr(currentTime),
duration: utils.formatTimeStr(duration),
percent: duration ? Math.min((currentTime / duration) * 100, 100) : 0
});
},
btnCilck() {
if (!this.audioCtx || !this.data.data.resourceUrl) return;
this.data.isPlay ? this.audioCtx.pause() : this.audioCtx.play();
},
playPrevious() {
this.changeTrack(-1, true);
},
playNext() {
this.changeTrack(1, true);
},
changeTrack(step, autoplay) {
const { playlist, currentIndex } = this.data;
if (!playlist.length) return;
const nextIndex = (currentIndex + step + playlist.length) % playlist.length;
this.setData({ currentIndex: nextIndex }, () => this.loadTrack(playlist[nextIndex], autoplay));
},
selectTrack(e) {
const index = Number(e.currentTarget.dataset.index);
if (!Number.isInteger(index) || !this.data.playlist[index]) return;
this.setData({ currentIndex: index }, () => {
this.loadTrack(this.data.playlist[index], true);
this.hideList();
});
},
toggleLoop() {
const isSingleLoop = !this.data.isSingleLoop;
if (this.audioCtx) this.audioCtx.loop = isSingleLoop;
this.setData({ isSingleLoop });
wx.showToast({ title: isSingleLoop ? "单曲循环" : "列表循环", icon: "none" });
},
seekAudio(e) {
if (!this.audioCtx || !this.audioCtx.duration) return;
wx.createSelectorQuery().in(this).select("#audio-progress").boundingClientRect((rect) => {
if (!rect || !rect.width) return;
const ratio = Math.min(Math.max((e.detail.x - rect.left) / rect.width, 0), 1);
this.audioCtx.seek(this.audioCtx.duration * ratio);
}).exec();
},
showList() {
if (this.closeTimer) {
clearTimeout(this.closeTimer);
this.closeTimer = null;
}
this.setData({ isShow: true, isClosing: false });
},
showList(){
this.setData({
isShow:true
})
hideList() {
if (!this.data.isShow || this.data.isClosing) return;
this.setData({ isClosing: true });
this.closeTimer = setTimeout(() => {
this.setData({ isShow: false, isClosing: false });
this.closeTimer = null;
}, 240);
},
reload(){
wx.showToast({
title: this.data.isReload ?'列表循环':'单曲循环',
icon:'none'
})
this.audioCtx.loop = true
this.setData({
isReload:!this.data.isReload
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
preventClose() {},
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
if(this.audioCtx){
if (this.closeTimer) clearTimeout(this.closeTimer);
if (this.audioCtx) {
this.audioCtx.stop();
if(this.audioCtx.destory){
this.audioCtx.destory();
}
if (this.audioCtx.destroy) this.audioCtx.destroy();
this.audioCtx = null;
}
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
});

View File

@ -1,10 +1,9 @@
{
"usingComponents": {
"mp-bar": "weui-miniprogram/navigation-bar/navigation-bar",
"mp-icon": "weui-miniprogram/icon/icon",
"mp-half-screen-dialog": "weui-miniprogram/half-screen-dialog/half-screen-dialog"
"mp-icon": "weui-miniprogram/icon/icon"
},
"backgroundColorContent": "#f7f7f7",
"backgroundColor": "#f7f7f7",
"navigationStyle": "custom"
}
}

View File

@ -1,32 +1,73 @@
<!--subpackage/teacher/resource-detail/resource-detail.wxml-->
<view class="page-content pad">
<mp-bar background="linear-gradient(190deg, #49a5fc 0%, #f7f7f7 800rpx, #f7f7f7 100%)" title="{{title}}" color="rgba(255,255,255,0.8)"></mp-bar>
<view class="page-content player-page">
<mp-bar background="transparent" title="{{title}}" color="#ffffff"></mp-bar>
<view class="card">
<view class="title">{{data.resourceName}}</view>
<view class="time">
{{value}}
<view>{{duration}}</view>
<view class="player-card">
<view class="record-wrap {{isPlay ? 'record-playing' : ''}}">
<view class="record">
<view class="record-line line-one"></view>
<view class="record-line line-two"></view>
<view class="record-core">
<text class="record-note">♫</text>
</view>
</view>
</view>
<view class="progress">
<view class="active" style="width:{{percent+'%'}}"></view>
<view class="song-name">{{data.resourceName || '暂无音频'}}</view>
<view class="song-count" wx:if="{{playlist.length}}">{{currentIndex + 1}} / {{playlist.length}}</view>
<view class="progress-area">
<view id="audio-progress" class="progress" bind:tap="seekAudio">
<view class="active" style="width: {{percent}}%;">
<view class="progress-dot"></view>
</view>
</view>
<view class="time">
<text>{{value}}</text>
<text>{{duration}}</text>
</view>
</view>
<view class="btnlist">
<image bind:tap="reload" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/{{isReload?'reload': 'repeat'}}.png" class="img" />
<image src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/play-previous.png" class="img" />
<view class="btn" bind:tap="btnCilck">
<mp-icon icon="{{!isPlay?'play':'pause'}}" type="field" size="{{50}}" color="#fff"></mp-icon>
<view class="tool-button" bind:tap="toggleLoop">
<image src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/{{isSingleLoop ? 'reload' : 'repeat'}}.png" class="tool-icon" />
<text>{{isSingleLoop ? '单曲' : '列表'}}</text>
</view>
<image bind:tap="playPrevious" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/play-previous.png" class="switch-icon" />
<view class="play-button" bind:tap="btnCilck">
<mp-icon icon="{{!isPlay ? 'play' : 'pause'}}" type="field" size="52" color="#ffffff"></mp-icon>
</view>
<image bind:tap="playNext" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/play-next.png" class="switch-icon" />
<view class="tool-button" bind:tap="showList">
<image src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/play-list.png" class="tool-icon" />
<text>列表</text>
</view>
<image src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/play-next.png" class="img" />
<image bind:tap="showList" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/teacher/play-list.png" class="img" />
</view>
</view>
<mp-half-screen-dialog bindbuttontap="buttontap" show="{{isShow}}">
<view slot="footer" class="none">1</view>
<view slot="title" class="none"></view>
</mp-half-screen-dialog>
</view>
<view wx:if="{{isShow}}" class="playlist-mask {{isClosing ? 'playlist-mask-leave' : 'playlist-mask-enter'}}" bind:tap="hideList">
<view class="playlist-panel {{isClosing ? 'playlist-panel-leave' : 'playlist-panel-enter'}}" catch:tap="preventClose">
<view class="playlist-header">
<view>
<view class="playlist-title">播放列表</view>
<view class="playlist-subtitle">共 {{playlist.length}} 首</view>
</view>
<view class="close-button" bind:tap="hideList">×</view>
</view>
<scroll-view scroll-y class="playlist-scroll" scroll-into-view="song-{{currentIndex}}">
<view wx:if="{{!playlist.length}}" class="empty-list">暂无可播放歌曲</view>
<view
wx:for="{{playlist}}"
wx:key="resourceUrl"
id="song-{{index}}"
class="playlist-item {{index === currentIndex ? 'playlist-active' : ''}}"
data-index="{{index}}"
bind:tap="selectTrack"
>
<view class="song-index">{{index === currentIndex && isPlay ? '♪' : index + 1}}</view>
<view class="playlist-name">{{item.resourceName}}</view>
<view wx:if="{{index === currentIndex}}" class="playing-label">{{isPlay ? '播放中' : '当前'}}</view>
</view>
</scroll-view>
</view>
</view>
</view>

View File

@ -1,78 +1,161 @@
/* subpackage/teacher/resource-detail/resource-detail.wxss */
.pad{
padding: 0 30rpx;
.player-page {
min-height: 100vh;
box-sizing: border-box;
padding: 0 30rpx 60rpx;
overflow: hidden;
background: linear-gradient(160deg, #42a4fc 0%, #7fc8ff 28%, #f4f8fc 64%, #f7f7f7 100%);
}
.card{
padding: 60rpx 30rpx;
box-sizing: border-box;
box-shadow: 0 10rpx 10rpx rgba(226, 222, 222,.4);
background-color: #fff;
border-radius: 20rpx;
margin-top: 30rpx;
.player-card {
margin-top: 38rpx;
padding: 66rpx 34rpx 54rpx;
border: 1rpx solid rgba(255, 255, 255, .75);
border-radius: 36rpx;
background: rgba(255, 255, 255, .94);
box-shadow: 0 24rpx 60rpx rgba(35, 116, 180, .18);
text-align: center;
}
.title{
font-weight: bold;
font-size: 48rpx;
}
.time{
display: flex;
align-items: center;
justify-content: space-between;
color: #636363;
font-size: 32rpx;
margin-top: 100rpx;
margin-bottom: 20rpx;
}
.progress{
width: 100%;
height: 24rpx;
border-radius: 24rpx;
background-color: #e4e4e4;
}
.active{
background-color: #FBB716;
border-radius: 24rpx;
height: 24rpx;
width: 10%;
transition: all 0.2s ease;
}
.btn{
background-color: #FBB716;
.record-wrap {
width: 330rpx;
height: 330rpx;
margin: 0 auto 52rpx;
padding: 18rpx;
box-sizing: border-box;
border-radius: 50%;
width: 140rpx;
height: 140rpx;
margin: auto;
background: rgba(251, 183, 22, .16);
box-shadow: 0 18rpx 38rpx rgba(48, 66, 84, .2);
}
.record {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 50%;
background: repeating-radial-gradient(circle, #2d3741 0, #2d3741 10rpx, #202832 11rpx, #202832 17rpx);
}
.btnlist{
.record-playing .record { animation: recordRotate 7s linear infinite; }
.record-line { position: absolute; border: 2rpx solid rgba(255, 255, 255, .2); border-radius: 50%; }
.line-one { width: 210rpx; height: 210rpx; }
.line-two { width: 150rpx; height: 150rpx; }
.record-core {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 80rpx;
justify-content: center;
width: 108rpx;
height: 108rpx;
border: 12rpx solid #f7c143;
border-radius: 50%;
background: #fbb716;
}
.record-note { color: #fff; font-size: 52rpx; line-height: 1; }
@keyframes recordRotate { from { transform: rotate(0); } to { transform: rotate(360deg); } }
.song-name {
min-height: 58rpx;
overflow: hidden;
color: #1d2a36;
font-size: 42rpx;
font-weight: 600;
line-height: 58rpx;
text-overflow: ellipsis;
white-space: nowrap;
}
.img{
width: 64rpx;
height: 64rpx;
margin: 20rpx;
.song-count { margin-top: 10rpx; color: #96a0aa; font-size: 24rpx; }
.progress-area { margin-top: 70rpx; }
.progress {
width: 100%;
height: 10rpx;
border-radius: 10rpx;
background: #e5eaf0;
}
.active { position: relative; height: 10rpx; border-radius: 10rpx; background: linear-gradient(90deg, #ffd465, #fbb716); }
.progress-dot {
position: absolute;
top: -8rpx;
right: -13rpx;
width: 26rpx;
height: 26rpx;
border: 5rpx solid #fff;
border-radius: 50%;
background: #fbb716;
box-shadow: 0 3rpx 10rpx rgba(251, 183, 22, .55);
}
.time { display: flex; justify-content: space-between; margin-top: 18rpx; color: #87929d; font-size: 25rpx; }
.btnlist { display: flex; align-items: center; justify-content: space-between; margin-top: 58rpx; }
.play-button {
display: flex;
align-items: center;
justify-content: center;
width: 142rpx;
height: 142rpx;
border-radius: 50%;
background: linear-gradient(145deg, #ffd052, #f5a900);
box-shadow: 0 16rpx 30rpx rgba(251, 183, 22, .35);
}
.switch-icon { width: 62rpx; height: 62rpx; padding: 12rpx; }
.tool-button { display: flex; flex-direction: column; align-items: center; min-width: 72rpx; color: #7e8994; font-size: 21rpx; }
.tool-icon { width: 50rpx; height: 50rpx; margin-bottom: 5rpx; }
.playlist-mask {
position: fixed;
z-index: 99;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(13, 26, 39, .48);
}
.playlist-mask-enter { animation: maskFadeIn 240ms ease-out both; }
.playlist-mask-leave { animation: maskFadeOut 240ms ease-in both; }
.playlist-panel {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 38rpx 30rpx calc(24rpx + env(safe-area-inset-bottom));
border-radius: 34rpx 34rpx 0 0;
background: #fff;
box-shadow: 0 -16rpx 50rpx rgba(18, 43, 65, .15);
}
.playlist-panel-enter { animation: playlistSlideUp 300ms cubic-bezier(.22, .8, .3, 1) both; }
.playlist-panel-leave { animation: playlistSlideDown 240ms ease-in both; }
@keyframes maskFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.none{
padding: 0;
margin: 0;
height: 0;
@keyframes maskFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.weui-half-screen-dialog__ft{
padding: 0;
}
@keyframes playlistSlideUp {
from { transform: translate3d(0, 100%, 0); }
to { transform: translate3d(0, 0, 0); }
}
@keyframes playlistSlideDown {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(0, 100%, 0); }
}
.playlist-header { display: flex; align-items: center; justify-content: space-between; padding: 0 10rpx 28rpx; }
.playlist-title { color: #172532; font-size: 36rpx; font-weight: 600; }
.playlist-subtitle { margin-top: 6rpx; color: #9aa4ad; font-size: 24rpx; }
.close-button { width: 64rpx; color: #8c969f; font-size: 48rpx; font-weight: 300; line-height: 64rpx; text-align: center; }
.playlist-scroll { max-height: 650rpx; }
.playlist-item { display: flex; align-items: center; min-height: 96rpx; padding: 0 22rpx; border-radius: 18rpx; color: #34424e; }
.playlist-active { color: #eaa400; background: #fff8e8; }
.song-index { width: 52rpx; flex-shrink: 0; color: #a0a9b2; font-size: 26rpx; text-align: left; }
.playlist-active .song-index { color: #f1ac00; }
.playlist-name { overflow: hidden; flex: 1; font-size: 29rpx; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
.playing-label { margin-left: 20rpx; color: #f1ac00; font-size: 22rpx; }
.empty-list { padding: 100rpx 0; color: #a0a9b2; font-size: 28rpx; text-align: center; }

View File

@ -59,10 +59,21 @@ Page({
}else if(this.data.id == 4){
return
}else {
const data = e.currentTarget.dataset.item
data.id = this.data.id
const data = Object.assign({}, e.currentTarget.dataset.item, { id: this.data.id })
const playlist = []
const collectResources = (nodes) => {
(nodes || []).forEach((node) => {
(node.resourceDetails || []).forEach((item) => playlist.push(Object.assign({}, item, { id: this.data.id })))
collectResources(node.children)
})
}
collectResources(this.data.list)
const currentIndex = Math.max(playlist.findIndex((item) => item.resourceUrl === data.resourceUrl), 0)
wx.navigateTo({
url: '/subpackage/teacher/resource-detail/resource-detail?data='+JSON.stringify(data),
url: '/subpackage/teacher/resource-detail/resource-detail?data='+encodeURIComponent(JSON.stringify(data)),
success(res) {
res.eventChannel.emit('setPlaylist', { playlist, currentIndex })
}
})
}
},
@ -115,4 +126,4 @@ Page({
onShareAppMessage() {
}
})
})