教练端在学生报告页录入体质数据并生成报告,新增报告查看页;学员端从报告页查看最新评估结果、体测指标与运动建议。请求统一走现有 QA 后端并携带登录令牌。 教学资源改用后台音频管理器,支持进度拖动、曲目切换、时长探测及网络异常时下载恢复;修正跳绳和开合跳动作提示文案。 验证:相关 JS 语法、页面 JSON 解析和 git diff --cached --check 均通过。
393 lines
13 KiB
JavaScript
393 lines
13 KiB
JavaScript
const utils = require("../../../utils/utils");
|
|
|
|
Page({
|
|
data: {
|
|
title: "音乐",
|
|
data: {},
|
|
playlist: [],
|
|
currentIndex: 0,
|
|
value: "00:00",
|
|
duration: "00:00",
|
|
durationSeconds: 1,
|
|
sliderValue: 0,
|
|
percent: 0,
|
|
isPlay: false,
|
|
isSingleLoop: false,
|
|
isShow: false,
|
|
isClosing: false
|
|
},
|
|
|
|
onLoad(options) {
|
|
let current = {};
|
|
try {
|
|
current = JSON.parse(decodeURIComponent(options.data || "{}"));
|
|
} catch (error) {
|
|
console.error("资源参数解析失败", error);
|
|
}
|
|
|
|
this.setData({
|
|
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] }, () => {
|
|
this.probeAudioDuration(playlist[safeIndex]);
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
createAudioContext() {
|
|
this.audioCtx = wx.getBackgroundAudioManager();
|
|
this.audioCache = {};
|
|
this.audioDurationCache = {};
|
|
this.recoveryAttempted = {};
|
|
this.loadedTrackUrl = "";
|
|
this.audioHandlers = {
|
|
canplay: () => this.updateProgress(),
|
|
timeUpdate: () => {
|
|
this.clearWaitingTimer();
|
|
this.updateProgress();
|
|
},
|
|
waiting: () => this.handleAudioWaiting(),
|
|
play: () => this.setData({ isPlay: true }),
|
|
pause: () => this.setData({ isPlay: false }),
|
|
stop: () => this.setData({ isPlay: false }),
|
|
ended: () => {
|
|
if (this.data.isSingleLoop) {
|
|
this.audioCtx.seek(0);
|
|
this.audioCtx.play();
|
|
} else {
|
|
this.changeTrack(1, true);
|
|
}
|
|
},
|
|
error: (error) => this.handleAudioError(error),
|
|
prev: () => this.playPrevious(),
|
|
next: () => this.playNext()
|
|
};
|
|
|
|
this.audioCtx.onCanplay(this.audioHandlers.canplay);
|
|
this.audioCtx.onTimeUpdate(this.audioHandlers.timeUpdate);
|
|
this.audioCtx.onWaiting(this.audioHandlers.waiting);
|
|
this.audioCtx.onPlay(this.audioHandlers.play);
|
|
this.audioCtx.onPause(this.audioHandlers.pause);
|
|
this.audioCtx.onStop(this.audioHandlers.stop);
|
|
this.audioCtx.onEnded(this.audioHandlers.ended);
|
|
this.audioCtx.onError(this.audioHandlers.error);
|
|
this.audioCtx.onPrev(this.audioHandlers.prev);
|
|
this.audioCtx.onNext(this.audioHandlers.next);
|
|
},
|
|
|
|
loadTrack(track, autoplay = true) {
|
|
if (!track || !track.resourceUrl || !this.audioCtx) return;
|
|
this.cancelPendingDownload();
|
|
this.clearWaitingTimer();
|
|
const cachedDuration = Number(this.audioDurationCache[track.resourceUrl]) || 0;
|
|
this.setData({
|
|
data: track,
|
|
value: "00:00",
|
|
duration: cachedDuration ? utils.formatTimeStr(cachedDuration) : "00:00",
|
|
durationSeconds: cachedDuration || 1,
|
|
sliderValue: 0,
|
|
percent: 0,
|
|
isPlay: false
|
|
});
|
|
if (!cachedDuration) this.probeAudioDuration(track);
|
|
if (!autoplay) return;
|
|
|
|
const playableUrl = this.audioCache[track.resourceUrl] || track.resourceUrl;
|
|
this.playAudioSource(track, playableUrl, 0);
|
|
},
|
|
|
|
playAudioSource(track, playableUrl, startTime = 0) {
|
|
if (!this.audioCtx || !track || !playableUrl) return;
|
|
this.audioCtx.title = track.resourceName || this.data.title || "音频资源";
|
|
this.audioCtx.epname = this.data.title || "教学资源";
|
|
this.audioCtx.singer = "跃动";
|
|
this.audioCtx.startTime = Math.max(Number(startTime) || 0, 0);
|
|
this.loadedTrackUrl = track.resourceUrl;
|
|
this.audioCtx.src = playableUrl;
|
|
},
|
|
|
|
handleAudioWaiting() {
|
|
if (this.waitingTimer || this.downloadTask || !this.data.data.resourceUrl) return;
|
|
const trackUrl = this.data.data.resourceUrl;
|
|
const resumeTime = Number(this.audioCtx.currentTime) || 0;
|
|
this.waitingTimer = setTimeout(() => {
|
|
this.waitingTimer = null;
|
|
if (this.data.data.resourceUrl === trackUrl && !this.audioCache[trackUrl]) {
|
|
this.downloadAndPlay(this.data.data, Number(this.audioCtx.currentTime) || resumeTime);
|
|
}
|
|
}, 5000);
|
|
},
|
|
|
|
handleAudioError(error = {}) {
|
|
console.error("音频播放失败", {
|
|
errCode: error.errCode,
|
|
errMsg: error.errMsg,
|
|
url: this.data.data.resourceUrl
|
|
});
|
|
this.clearWaitingTimer();
|
|
this.setData({ isPlay: false });
|
|
|
|
const track = this.data.data;
|
|
const trackUrl = track && track.resourceUrl;
|
|
if (this.downloadTask) return;
|
|
if (trackUrl && !this.audioCache[trackUrl] && !this.recoveryAttempted[trackUrl]) {
|
|
this.downloadAndPlay(track, Number(this.audioCtx.currentTime) || 0);
|
|
return;
|
|
}
|
|
|
|
const message = Number(error.errCode) === 10003
|
|
? "音频文件响应异常,请检查文件配置"
|
|
: "音频加载失败,请稍后重试";
|
|
wx.showToast({ title: message, icon: "none" });
|
|
},
|
|
|
|
downloadAndPlay(track, resumeTime = 0) {
|
|
if (!track || !track.resourceUrl || this.downloadTask) return;
|
|
const originalUrl = track.resourceUrl;
|
|
this.recoveryAttempted[originalUrl] = true;
|
|
const downloadState = { canceled: false, url: originalUrl };
|
|
this.downloadState = downloadState;
|
|
wx.showLoading({ title: "音频加载中", mask: false });
|
|
|
|
this.downloadTask = wx.downloadFile({
|
|
url: originalUrl,
|
|
success: (result) => {
|
|
if (downloadState.canceled) return;
|
|
if (result.statusCode !== 200 || !result.tempFilePath) {
|
|
this.showDownloadError();
|
|
return;
|
|
}
|
|
this.audioCache[originalUrl] = result.tempFilePath;
|
|
if (this.data.data.resourceUrl === originalUrl) {
|
|
this.playAudioSource(track, result.tempFilePath, resumeTime);
|
|
}
|
|
},
|
|
fail: (error) => {
|
|
if (downloadState.canceled) return;
|
|
console.error("音频下载失败", error);
|
|
this.showDownloadError();
|
|
},
|
|
complete: () => {
|
|
if (this.downloadState === downloadState) {
|
|
this.downloadTask = null;
|
|
this.downloadState = null;
|
|
wx.hideLoading();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
showDownloadError() {
|
|
this.setData({ isPlay: false });
|
|
wx.showToast({ title: "长音频加载失败,请检查网络或文件配置", icon: "none" });
|
|
},
|
|
|
|
clearWaitingTimer() {
|
|
if (!this.waitingTimer) return;
|
|
clearTimeout(this.waitingTimer);
|
|
this.waitingTimer = null;
|
|
},
|
|
|
|
cancelPendingDownload() {
|
|
if (!this.downloadTask) return;
|
|
if (this.downloadState) {
|
|
this.downloadState.canceled = true;
|
|
delete this.recoveryAttempted[this.downloadState.url];
|
|
}
|
|
this.downloadTask.abort();
|
|
this.downloadTask = null;
|
|
this.downloadState = null;
|
|
wx.hideLoading();
|
|
},
|
|
|
|
updateProgress() {
|
|
if (!this.audioCtx || this.isSeeking) return;
|
|
const currentTime = Number(this.audioCtx.currentTime) || 0;
|
|
const audioDuration = Number(this.audioCtx.duration) || 0;
|
|
const knownDuration = this.data.durationSeconds > 1 ? this.data.durationSeconds : 0;
|
|
const duration = audioDuration || knownDuration;
|
|
this.setData({
|
|
value: utils.formatTimeStr(currentTime),
|
|
duration: duration ? utils.formatTimeStr(duration) : this.data.duration,
|
|
durationSeconds: duration || 1,
|
|
sliderValue: Math.min(currentTime, duration || 1),
|
|
percent: duration ? Math.min((currentTime / duration) * 100, 100) : 0
|
|
});
|
|
},
|
|
|
|
probeAudioDuration(track) {
|
|
if (!track || !track.resourceUrl || !wx.createInnerAudioContext) return;
|
|
this.destroyDurationProbe();
|
|
|
|
const trackUrl = track.resourceUrl;
|
|
const probe = wx.createInnerAudioContext();
|
|
this.durationProbe = probe;
|
|
probe.autoplay = false;
|
|
probe.obeyMuteSwitch = false;
|
|
|
|
const syncDuration = () => {
|
|
if (this.durationProbe !== probe || this.data.data.resourceUrl !== trackUrl) return false;
|
|
const duration = Number(probe.duration) || 0;
|
|
if (duration <= 0) return false;
|
|
this.audioDurationCache[trackUrl] = duration;
|
|
this.setData({
|
|
duration: utils.formatTimeStr(duration),
|
|
durationSeconds: duration
|
|
});
|
|
this.destroyDurationProbe();
|
|
return true;
|
|
};
|
|
|
|
probe.onCanplay(() => {
|
|
if (syncDuration()) return;
|
|
[200, 500, 1000, 2000].forEach((delay) => {
|
|
const timer = setTimeout(syncDuration, delay);
|
|
this.durationProbeTimers.push(timer);
|
|
});
|
|
});
|
|
probe.onError((error) => {
|
|
console.warn("音频时长读取失败", error);
|
|
this.destroyDurationProbe();
|
|
});
|
|
this.durationProbeTimers = [];
|
|
probe.src = trackUrl;
|
|
},
|
|
|
|
destroyDurationProbe() {
|
|
(this.durationProbeTimers || []).forEach((timer) => clearTimeout(timer));
|
|
this.durationProbeTimers = [];
|
|
if (!this.durationProbe) return;
|
|
this.durationProbe.destroy();
|
|
this.durationProbe = null;
|
|
},
|
|
|
|
btnCilck() {
|
|
if (!this.audioCtx || !this.data.data.resourceUrl) return;
|
|
if (this.data.isPlay) {
|
|
this.audioCtx.pause();
|
|
} else if (this.loadedTrackUrl !== this.data.data.resourceUrl) {
|
|
this.loadTrack(this.data.data, true);
|
|
} else {
|
|
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;
|
|
this.setData({ isSingleLoop });
|
|
wx.showToast({ title: isSingleLoop ? "单曲循环" : "列表循环", icon: "none" });
|
|
},
|
|
|
|
onSliderChanging(e) {
|
|
const seconds = Number(e.detail.value) || 0;
|
|
this.isSeeking = true;
|
|
this.setData({
|
|
sliderValue: seconds,
|
|
value: utils.formatTimeStr(seconds),
|
|
percent: this.data.durationSeconds ? (seconds / this.data.durationSeconds) * 100 : 0
|
|
});
|
|
},
|
|
|
|
onSliderChange(e) {
|
|
const seconds = Number(e.detail.value) || 0;
|
|
if (!this.audioCtx || !this.data.data.resourceUrl) return;
|
|
|
|
if (this.loadedTrackUrl !== this.data.data.resourceUrl) {
|
|
this.loadTrack(this.data.data, true);
|
|
}
|
|
this.audioCtx.seek(seconds);
|
|
this.setData({
|
|
sliderValue: seconds,
|
|
value: utils.formatTimeStr(seconds),
|
|
percent: this.data.durationSeconds ? (seconds / this.data.durationSeconds) * 100 : 0
|
|
});
|
|
|
|
if (this.seekTimer) clearTimeout(this.seekTimer);
|
|
this.seekTimer = setTimeout(() => {
|
|
this.isSeeking = false;
|
|
this.updateProgress();
|
|
this.seekTimer = null;
|
|
}, 350);
|
|
},
|
|
|
|
showList() {
|
|
if (this.closeTimer) {
|
|
clearTimeout(this.closeTimer);
|
|
this.closeTimer = null;
|
|
}
|
|
this.setData({ isShow: true, isClosing: false });
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
preventClose() {},
|
|
|
|
onUnload() {
|
|
if (this.closeTimer) clearTimeout(this.closeTimer);
|
|
if (this.seekTimer) clearTimeout(this.seekTimer);
|
|
this.clearWaitingTimer();
|
|
this.cancelPendingDownload();
|
|
this.destroyDurationProbe();
|
|
if (this.audioCtx) {
|
|
this.audioCtx.stop();
|
|
const handlers = this.audioHandlers || {};
|
|
if (this.audioCtx.offCanplay) this.audioCtx.offCanplay(handlers.canplay);
|
|
if (this.audioCtx.offTimeUpdate) this.audioCtx.offTimeUpdate(handlers.timeUpdate);
|
|
if (this.audioCtx.offWaiting) this.audioCtx.offWaiting(handlers.waiting);
|
|
if (this.audioCtx.offPlay) this.audioCtx.offPlay(handlers.play);
|
|
if (this.audioCtx.offPause) this.audioCtx.offPause(handlers.pause);
|
|
if (this.audioCtx.offStop) this.audioCtx.offStop(handlers.stop);
|
|
if (this.audioCtx.offEnded) this.audioCtx.offEnded(handlers.ended);
|
|
if (this.audioCtx.offError) this.audioCtx.offError(handlers.error);
|
|
if (this.audioCtx.offPrev) this.audioCtx.offPrev(handlers.prev);
|
|
if (this.audioCtx.offNext) this.audioCtx.offNext(handlers.next);
|
|
this.audioCtx = null;
|
|
}
|
|
}
|
|
});
|