feat: 电子药箱小程序 - 4Tab药品管理

- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡
- 药品百科: 搜索 + 分类筛选 + 20种药品静态数据
- 惠教中心: 4条药品使用指南课程
- 我的: 家庭成员信息管理
- 自定义TabBar + navigation-bar组件
- SVG药品分类插图
This commit is contained in:
陈誉午
2026-07-29 11:20:52 +08:00
commit 994b267f5c
683 changed files with 43849 additions and 0 deletions
@@ -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);
}
+262
View File
@@ -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>