feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/* components/my-video.wxss */
|
||||
.live-content {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.video {
|
||||
width: 100%;
|
||||
min-height: 400rpx; /* 200px -> 400rpx */
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.screen {
|
||||
.video {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.tools {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
color: white;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.video-poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 2;
|
||||
.play-button {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
.play-icon {
|
||||
font-size: 50rpx;
|
||||
color:white;
|
||||
margin-left: 6rpx;
|
||||
margin-bottom: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.poster-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.cover {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 998;
|
||||
height: 20%;
|
||||
width: 70%;
|
||||
left: 10%;
|
||||
background-color: rgba(255, 255, 255, 0.007);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// components/my-video/index.ts
|
||||
import { AES_KEY, AES_IV } from "../../env";
|
||||
import { request } from "../../utils/request";
|
||||
import { aesEncrypt } from "../../utils/crypto";
|
||||
|
||||
interface VideoData {
|
||||
screen: boolean;
|
||||
playing: boolean;
|
||||
currentTime: number;
|
||||
isReady: boolean;
|
||||
sendDurationTimer: number | null;
|
||||
lastUpdateTime: number;
|
||||
isConnected: boolean; // 新增网络连接状态字段
|
||||
}
|
||||
interface MyVideoComponent {
|
||||
data: VideoData;
|
||||
videoContext: WechatMiniprogram.VideoContext | null;
|
||||
pauseVideoHandler?: () => void;
|
||||
resumeVideoHandler?: () => void;
|
||||
setData: (d: Partial<VideoData>, cb?: () => void) => void;
|
||||
initNetworkListener: () => void;
|
||||
startSendProgress: () => void;
|
||||
clearSendProgressTimer: () => void;
|
||||
sendProgress: () => Promise<void>;
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
src: String,
|
||||
backgroundUrl: String,
|
||||
},
|
||||
|
||||
data: {
|
||||
screen: false,
|
||||
playing: false,
|
||||
currentTime: 0,
|
||||
isReady: false,
|
||||
isConnected: false,
|
||||
sendDurationTimer: 0,
|
||||
lastUpdateTime: 0,
|
||||
} as VideoData,
|
||||
|
||||
lifetimes: {
|
||||
attached(this: MyVideoComponent) {
|
||||
this.initNetworkListener();
|
||||
|
||||
// 获取 videoContext 并初始化
|
||||
this.videoContext = wx.createVideoContext("videoPlayer", this as any);
|
||||
|
||||
const pauseVideo = () => {
|
||||
if (this.data.playing) {
|
||||
wx.setStorageSync("interruptedTime", this.data.currentTime);
|
||||
this.videoContext?.pause();
|
||||
this.setData({ playing: false });
|
||||
this.clearSendProgressTimer();
|
||||
}
|
||||
};
|
||||
|
||||
const resumeVideo = () => {
|
||||
if (!this.videoContext) {
|
||||
this.videoContext = wx.createVideoContext("videoPlayer", this as any);
|
||||
}
|
||||
this.videoContext.pause();
|
||||
};
|
||||
|
||||
this.pauseVideoHandler = pauseVideo;
|
||||
this.resumeVideoHandler = resumeVideo;
|
||||
|
||||
wx.onAudioInterruptionBegin(this.pauseVideoHandler);
|
||||
wx.onAudioInterruptionEnd(this.resumeVideoHandler);
|
||||
wx.onAppHide(this.pauseVideoHandler);
|
||||
wx.onAppShow(this.resumeVideoHandler);
|
||||
},
|
||||
|
||||
detached(this: MyVideoComponent) {
|
||||
if (this.pauseVideoHandler) {
|
||||
wx.offAudioInterruptionBegin(this.pauseVideoHandler);
|
||||
wx.offAppHide(this.pauseVideoHandler);
|
||||
}
|
||||
if (this.resumeVideoHandler) {
|
||||
wx.offAudioInterruptionEnd(this.resumeVideoHandler);
|
||||
wx.offAppShow(this.resumeVideoHandler);
|
||||
}
|
||||
this.clearSendProgressTimer();
|
||||
this.videoContext = null;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
initNetworkListener(this: MyVideoComponent) {
|
||||
// 首先获取当前网络状态
|
||||
wx.getNetworkType({
|
||||
success: (res) => {
|
||||
this.setData({
|
||||
isConnected: res.networkType !== "none",
|
||||
});
|
||||
},
|
||||
});
|
||||
wx.onNetworkStatusChange(
|
||||
(res: WechatMiniprogram.OnNetworkStatusChangeCallbackResult) => {
|
||||
this.setData({
|
||||
isConnected: res.isConnected,
|
||||
});
|
||||
if (!res.isConnected && this.data.playing) {
|
||||
this.videoContext?.pause();
|
||||
this.setData({ playing: false });
|
||||
wx.showToast({
|
||||
title: "网络连接已断开",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
if (res.isConnected) {
|
||||
wx.showToast({
|
||||
title: "网络已连接",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
handleLoadedMetadata(this: MyVideoComponent) {
|
||||
this.setData({ isReady: true });
|
||||
},
|
||||
|
||||
handlePosterTap(this: MyVideoComponent) {
|
||||
if (!this.videoContext || !this.data.isReady) return;
|
||||
if (!this.data.isConnected) {
|
||||
wx.showToast({
|
||||
title: "网络连接异常,请检查网络设置",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newPlaying = !this.data.playing;
|
||||
const resumeTime =
|
||||
(wx.getStorageSync("interruptedTime") as number) ||
|
||||
this.data.currentTime ||
|
||||
(wx.getStorageSync("currentTime") as number) ||
|
||||
0;
|
||||
|
||||
if (newPlaying) {
|
||||
if (resumeTime > 0) {
|
||||
this.videoContext.seek(resumeTime);
|
||||
}
|
||||
this.videoContext.play();
|
||||
if (!this.data.sendDurationTimer) {
|
||||
this.startSendProgress();
|
||||
}
|
||||
} else {
|
||||
this.videoContext.pause();
|
||||
this.clearSendProgressTimer();
|
||||
}
|
||||
|
||||
this.setData({ playing: newPlaying });
|
||||
wx.removeStorageSync("interruptedTime");
|
||||
},
|
||||
|
||||
handlePause(this: MyVideoComponent) {
|
||||
wx.setStorageSync("interruptedTime", this.data.currentTime);
|
||||
this.setData({ playing: false });
|
||||
this.clearSendProgressTimer();
|
||||
},
|
||||
|
||||
handleTimeUpdate(
|
||||
this: MyVideoComponent,
|
||||
e: WechatMiniprogram.VideoTimeUpdate
|
||||
) {
|
||||
const now = Date.now();
|
||||
const currentTime = Math.floor(e.detail.currentTime ?? 0);
|
||||
if (now - this.data.lastUpdateTime >= 1000) {
|
||||
this.setData({ currentTime });
|
||||
(this.data as VideoData).lastUpdateTime = now;
|
||||
wx.setStorageSync("currentTime", currentTime);
|
||||
}
|
||||
},
|
||||
|
||||
handleEnded(this: MyVideoComponent) {
|
||||
this.sendProgress();
|
||||
wx.setStorageSync("ended", true);
|
||||
this.setData({ playing: false, currentTime: 0 });
|
||||
wx.removeStorageSync("currentTime");
|
||||
wx.removeStorageSync("interruptedTime");
|
||||
this.clearSendProgressTimer();
|
||||
},
|
||||
|
||||
handleFullscreenChange(this: MyVideoComponent) {
|
||||
const newScreen = !this.data.screen;
|
||||
if (newScreen) {
|
||||
this.videoContext?.requestFullScreen({ direction: 90 });
|
||||
} else {
|
||||
this.videoContext?.exitFullScreen();
|
||||
}
|
||||
this.setData({ screen: newScreen });
|
||||
},
|
||||
|
||||
handleError(this: MyVideoComponent, e: WechatMiniprogram.VideoError) {
|
||||
console.error("播放错误:", e.detail.errMsg);
|
||||
this.setData({ playing: false });
|
||||
},
|
||||
|
||||
startSendProgress(this: MyVideoComponent) {
|
||||
this.clearSendProgressTimer();
|
||||
const timer = setInterval(() => {
|
||||
this.sendProgress();
|
||||
}, 15000) as unknown as number;
|
||||
this.setData({ sendDurationTimer: timer });
|
||||
},
|
||||
|
||||
clearSendProgressTimer(this: MyVideoComponent) {
|
||||
const t = this.data.sendDurationTimer;
|
||||
if (t) {
|
||||
clearInterval(t);
|
||||
this.setData({ sendDurationTimer: 0 });
|
||||
}
|
||||
},
|
||||
|
||||
async sendProgress(this: MyVideoComponent) {
|
||||
try {
|
||||
const openId = wx.getStorageSync("openId");
|
||||
const code = wx.getStorageSync("code");
|
||||
const state = wx.getStorageSync("state");
|
||||
const seeId = wx.getStorageSync("seeId");
|
||||
const duration = this.data.currentTime + 1;
|
||||
if (code) {
|
||||
const params = {
|
||||
openId,
|
||||
code,
|
||||
duration
|
||||
};
|
||||
await request({
|
||||
options: {
|
||||
url: `/app/video-watch/inspect`,
|
||||
data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV),
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
} else {
|
||||
const params = {
|
||||
openId,
|
||||
state,
|
||||
seeId,
|
||||
duration
|
||||
};
|
||||
await request({
|
||||
options: {
|
||||
url: `/app/watch/inspect`,
|
||||
data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV),
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("上报观看进度失败:", err);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--components/my-video.wxml-->
|
||||
<view class="live-content">
|
||||
<video id="videoPlayer"
|
||||
class="video"
|
||||
danmu-list="{{danmuList}}"
|
||||
enable-danmu="{{false}}"
|
||||
show-fullscreen-btn
|
||||
show-center-play-btn='{{false}}'
|
||||
enable-progress-gesture="{{false}}"
|
||||
show-play-btn="{{true}}"
|
||||
controls="{{true}}"
|
||||
object-fit="contain"
|
||||
picture-in-picture-mode="{{[]}}"
|
||||
show-background-playback-button="{{false}}"
|
||||
src="{{src}}"
|
||||
bindended="handleEnded"
|
||||
binderror="handleError"
|
||||
bindpause="handlePause"
|
||||
bindtimeupdate="handleTimeUpdate"
|
||||
bindfullscreenchange="handleFullscreenChange"
|
||||
bindloadedmetadata="handleLoadedMetadata"
|
||||
>
|
||||
<cover-view class="cover"></cover-view>
|
||||
<image wx:if="{{!playing}}" src="{{backgroundUrl}}" mode="aspectFill" class="poster-image"></image>
|
||||
<view class="video-poster" wx:if="{{!playing}}" catchtap="handlePosterTap">
|
||||
<view class="play-button">
|
||||
<view class="play-icon">
|
||||
▶
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</video>
|
||||
</view>
|
||||
Reference in New Issue
Block a user