Files
ruo-shan-cloud-pharmacy/miniprogram/pages/live/video/video.ts
T
2026-09-07 16:50:54 +08:00

420 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// pages/video/video.ts
import UrlParamsHandler from "../../../utils/store";
import { htmlToWxNodes } from "../../../utils/html-to-wx-nodes";
import { request } from "../../../utils/request";
import { AES_IV, AES_KEY, appId } from "../../../env";
import { receiveRedPacket } from "../../../utils/util";
import { aesEncrypt } from "../../../utils/crypto";
const isMobileEnvironment = (): boolean => {
const platform = wx.getSystemInfoSync().platform?.toLowerCase() || '';
return platform !== 'windows' && platform !== 'mac';
};
Page({
// 页面私有变量,避免触发无效渲染
_wasPlayingBeforeInterrupt: false,
_pauseVideoHandler: null as any,
_resumeVideoHandler: null as any,
_appHideHandler: null as any,
_appShowHandler: null as any,
_networkStatusChangeHandler: null as any,
_videoContext: null as any,
data: {
dataList: {} as any,
nodes: [] as any[],
eveId: null,
store: {},
fileid: "",
active: 0,
canSubmit: false,
safeBottom: 24,
resumeTime: 0,
playing: false,
currentTime: 0,
sendDurationTimer: 0,
isAppHidden: false,
},
async onLoad(options: any) {
const app = getApp<IAppOption>();
wx.hideShareMenu({ menus: ["shareAppMessage"] });
new UrlParamsHandler(options);
app.globalData.store = options;
// 适配底部安全区
let safeBottom = 24;
try {
//@ts-ignore
const { windowHeight, safeArea } = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync();
safeBottom = safeArea ? Math.abs(windowHeight - safeArea.bottom) + 10 : 24;
} catch (e) { }
this.setData({ safeBottom, store: options });
this.initSystemListeners();
if (!isMobileEnvironment()) {
return this.handleEnterError("当前环境不支持观看,请使用手机观看。", "环境不支持");
}
if (!(await app.login("video"))) return;
//@ts-ignore 开启防截屏
if (wx.setVisualEffectOnCapture) wx.setVisualEffectOnCapture({ visualEffect: 'hidden' });
},
onShow() {
this.search();
},
onHide() {
//@ts-ignore
if (wx.setVisualEffectOnCapture) wx.setVisualEffectOnCapture({ visualEffect: 'none' });
this.flushProgress();
},
onUnload() {
//@ts-ignore
if (wx.setVisualEffectOnCapture) wx.setVisualEffectOnCapture({ visualEffect: 'none' });
this.flushProgress();
try { this.pauseVideoBeforeNavigate(); } catch (err) { }
// 卸载事件与定时器
if (this._pauseVideoHandler) wx.offAudioInterruptionBegin(this._pauseVideoHandler);
if (this._resumeVideoHandler) wx.offAudioInterruptionEnd(this._resumeVideoHandler);
if (this._appHideHandler) wx.offAppHide(this._appHideHandler);
if (this._appShowHandler) wx.offAppShow(this._appShowHandler);
if (this._networkStatusChangeHandler) wx.offNetworkStatusChange(this._networkStatusChangeHandler);
this.clearSendProgressTimer();
},
onChange(event: any) {
this.setData({ active: event.detail.index });
},
initSystemListeners() {
this._networkStatusChangeHandler = (res: any) => {
if (!res.isConnected) {
wx.showToast({ title: '网络连接断开', icon: 'none' });
this.pauseVideoBeforeNavigate();
} else if (res.networkType !== 'wifi') {
wx.showToast({ title: '当前为非Wi-Fi环境,请注意流量消耗', icon: 'none' });
}
};
wx.onNetworkStatusChange(this._networkStatusChangeHandler);
this._pauseVideoHandler = () => {
this.flushProgress();
this._wasPlayingBeforeInterrupt = this.data.playing;
this.pauseVideoBeforeNavigate();
};
this._resumeVideoHandler = () => {
if (this._wasPlayingBeforeInterrupt && !this.data.isAppHidden) {
this.resumeVideoAfterInterrupt();
}
this._wasPlayingBeforeInterrupt = false;
};
this._appHideHandler = () => {
this.flushProgress();
this.setData({ isAppHidden: true });
this.pauseVideoBeforeNavigate();
};
this._appShowHandler = () => {
this.setData({ isAppHidden: false });
};
wx.onAudioInterruptionBegin(this._pauseVideoHandler);
wx.onAudioInterruptionEnd(this._resumeVideoHandler);
wx.onAppHide(this._appHideHandler);
wx.onAppShow(this._appShowHandler);
},
getVideoContext() {
if (!this._videoContext) {
this._videoContext = wx.createVideoContext("myPlayerId", this);
}
return this._videoContext;
},
pauseVideoBeforeNavigate() {
this.getVideoContext().pause();
},
resumeVideoAfterInterrupt() {
this.getVideoContext().play();
},
// 统一进度落盘(极简版,直接信任内存里的 currentTime)
flushProgress() {
const t = this.data.currentTime;
if (t > 0) {
wx.setStorageSync("interruptedTime", t);
wx.setStorageSync("currentTime", t);
}
},
getResumeTime(): number {
if (wx.getStorageSync("ended")) return 0;
const interrupted = Number(wx.getStorageSync("interruptedTime") || 0);
const current = Number(wx.getStorageSync("currentTime") || 0);
return Math.max(interrupted, current);
},
onPlayerPlay() {
this.setData({ playing: true });
// 异步拉取到 fileid 后,初次播放如果存在历史记录,则跳转
if (this.data.resumeTime > 0) {
this.getVideoContext().seek(this.data.resumeTime);
this.setData({ resumeTime: 0 }); // 跳转完立即清空,防止二次跳转
}
this.startSendProgress();
},
onPlayerPause() {
this.setData({ playing: false });
this.clearSendProgressTimer();
this.flushProgress();
},
onPlayerEnded() {
this.setData({ playing: false, resumeTime: 0, currentTime: 0 });
wx.setStorageSync("ended", true);
this.clearSendProgressTimer();
wx.removeStorageSync("interruptedTime");
wx.removeStorageSync("currentTime");
},
onPlayerError(e: any) {
const detail = e?.detail || e || {};
wx.showModal({
title: "播放出错",
content: String(detail.errMsg || detail.message || JSON.stringify(detail) + ',请联系客服'),
showCancel: false,
});
},
onPlayerTimeUpdate(e: any) {
let time = e.detail?.currentTime ?? e.detail?.detail?.currentTime;
if (typeof time === 'number' && time > 0) {
const currentTime = Math.floor(time);
if (currentTime === this.data.currentTime) return; // 避免重复操作
this.data.currentTime = currentTime;
// 逢 5 秒落盘一次
if (currentTime % 5 === 0) this.flushProgress();
}
},
startSendProgress() {
this.clearSendProgressTimer();
this.sendProgress(); // 启动时先发一次
const timer = setInterval(() => this.sendProgress(), 15000) as unknown as number;
this.setData({ sendDurationTimer: timer });
},
clearSendProgressTimer() {
if (this.data.sendDurationTimer) {
clearInterval(this.data.sendDurationTimer);
this.setData({ sendDurationTimer: 0 });
}
},
async sendProgress() {
if (!this.data.playing) return;
try {
const code = wx.getStorageSync("code");
const openId = wx.getStorageSync("openId");
const duration = this.data.currentTime + 1; // 时长兜底补偿
const params = code
? { openId, code, duration }
: { openId, state: wx.getStorageSync("state"), seeId: wx.getStorageSync("seeId"), duration };
await request({
options: {
url: code ? `/app/video-watch/inspect` : `/app/watch/inspect`,
data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV),
},
isLoading: false,
});
} catch (err) {
console.error("上报观看进度失败:", err);
}
},
async search() {
const app = getApp<IAppOption>();
if (!(await app.checkLoginState())) return;
const code = wx.getStorageSync("code");
const params = { openId: wx.getStorageSync("openId"), ...app.globalData.store };
const data = await request({
options: {
url: code ? `/app/video-watch/details` : `/app/watch/details`,
method: code ? "GET" : "POST",
data: params
},
});
const targetData = code ? data.watch : data.data;
if (!targetData?.extend?.fileid) {
const stateParam = code ? -1 : 5;
return wx.reLaunch({
url: `/pages/live/registration/registration?content=${targetData?.content || ''}&state=${stateParam}`,
});
}
this.setData({
dataList: targetData,
eveId: targetData.eveId,
nodes: htmlToWxNodes(targetData.content),
fileid: targetData.extend.fileid,
resumeTime: this.getResumeTime(), // 获取到真实视频时,一次性读取恢复点
});
this.selectComponent("#tabs")?.resize();
},
onAnswerChange() {
const quizComponent = this.selectComponent("#quizComponent");
if (quizComponent) {
this.setData({ canSubmit: quizComponent.isAllAnswered() });
}
},
handleSubmit() {
if (!wx.getStorageSync("ended")) {
return wx.showModal({ title: "提示", content: "请先观看完整视频,再参与答题", showCancel: false, confirmColor: "#FF0000" });
}
if (this.data.dataList?.userAnsNum >= this.data.dataList?.answerMax) {
return wx.showToast({ title: "答题次数已用完", icon: "none", duration: 3000 });
}
const quizComponent = this.selectComponent("#quizComponent");
if (!quizComponent) return wx.showToast({ title: "组件加载失败", icon: "none" });
if (!quizComponent.isAllAnswered()) {
const { answered, total } = quizComponent.getProgress();
return wx.showModal({
title: "答题未完成",
content: `您已完成 ${answered}/${total} 题,请完成所有题目后再提交。`,
showCancel: false,
});
}
const answers = quizComponent.getAllAnswers();
if (!answers?.length) return wx.showToast({ title: "请先完成答题", icon: "none" });
wx.showModal({
title: "确认提交",
content: `您已完成 ${answers.length} 道题目,确定要提交答案吗?`,
success: (res) => { if (res.confirm) this.submitAnswers(answers); },
});
},
async submitAnswers(answers: any[]) {
const code = wx.getStorageSync("code");
const params = {
answers,
openId: wx.getStorageSync("openId"),
eveId: this.data.eveId,
...this.data.store,
};
wx.showLoading({ title: "提交中..." });
const url = code ? "/app/video-watch/interactive" : "/app/watch/interactive";
const res = await request({ options: { url, data: params } });
wx.hideLoading();
if (res.code !== 200) {
return wx.showToast({ title: res.msg || "提交失败", icon: "none", duration: 2000 });
}
const { pass, redPackage } = res.data || {};
if (pass) {
wx.showModal({
content: "🎉 回答正确,领取红包!",
showCancel: false,
confirmText: "立即领取",
confirmColor: "#FF0000",
success: async (res) => {
if (!res.confirm) return;
if (redPackage?.packageInfo) {
this.transferRedPacket(redPackage);
} else if (redPackage?.redId) {
await this.handleReceive(redPackage.redId);
}
},
});
} else {
wx.showToast({ title: "回答错误,与红包擦肩而过", icon: "none", duration: 3000 });
}
},
async transferRedPacket(redPackage: any) {
wx.showLoading({ title: "领取中", mask: true });
if (!wx.canIUse?.("requestMerchantTransfer")) {
wx.hideLoading();
return wx.showModal({ content: "你的微信版本过低,请更新至最新版本。", showCancel: false });
}
try {
await new Promise<void>((resolve, reject) => {
//@ts-ignore
wx.requestMerchantTransfer({
mchId: redPackage.mchId,
appId,
package: redPackage.packageInfo,
success: () => resolve(),
fail: (err: any) => reject(new Error(err?.errMsg || "领取失败")),
});
});
wx.hideLoading();
wx.showToast({ title: "领取成功", icon: "success" });
} catch (err: any) {
wx.hideLoading();
wx.showToast({ title: err?.message || "领取失败", icon: "none" });
}
},
async handleReceive(id: any) {
try {
wx.showLoading({ title: "领取中" });
await receiveRedPacket(
{ appId, code: wx.getStorageSync("code"), openId: wx.getStorageSync("openId"), redId: id },
'/app/general/receive-red',
appId
);
wx.hideLoading();
} catch (err) {
wx.hideLoading();
}
},
handleEnterError(message: string, title = "无法进入") {
wx.showModal({ title, content: message, showCancel: false });
},
handleBack() {
wx.switchTab({ url: "/pages/tab-bar/course/course" });
},
handleCustomReport() {
this.pauseVideoBeforeNavigate();
wx.navigateTo({ url: `/pages/live/report/report` });
},
handleViewHitory() {
this.pauseVideoBeforeNavigate();
wx.navigateTo({ url: `/pages/live/red-history/red-history` });
}
});