420 lines
13 KiB
TypeScript
420 lines
13 KiB
TypeScript
// 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` });
|
||
}
|
||
}); |