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 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744786271399" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M832 192H192c-35.2 0-64 28.8-64 64v512c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64V256c0-35.2-28.8-64-64-64z" fill="#F56C6C"/><path d="M192 192h640L512 448 192 192z" fill="#F78989"/><path d="M512 512m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" fill="#FFD700"/></svg>

After

Width:  |  Height:  |  Size: 545 B

@@ -0,0 +1,7 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar",
"van-tabs": "@vant/weapp/tabs/index",
"van-tab": "@vant/weapp/tab/index"
}
}
@@ -0,0 +1,184 @@
.page-wrap {
box-sizing: border-box;
background: #f5f5f5;
height: calc(100vh - 162rpx);
display: flex;
flex-direction: column;
}
/* ===== 汇总卡片 ===== */
.summary-section {
padding: 24rpx 24rpx 0;
}
.summary-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx;
border-radius: 16rpx;
}
.total-card {
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
margin-bottom: 16rpx;
padding: 28rpx 20rpx;
}
.total-card .card-label {
color: rgba(255, 255, 255, 0.85);
font-size: 26rpx;
}
.total-amount {
color: #fff !important;
font-size: 48rpx !important;
}
.summary-row {
display: flex;
gap: 16rpx;
}
.sub-card {
flex: 1;
background: #fff;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
padding: 24rpx 10rpx;
}
.card-label {
font-size: 22rpx;
color: #999;
margin-bottom: 10rpx;
}
.card-amount {
font-size: 30rpx;
font-weight: 700;
}
.card-amount.received {
color: #52c41a;
}
.card-amount.pending {
color: #fa8c16;
}
.card-amount.expired {
color: #999;
}
.tabs-wrap {
margin: 16rpx;
}
/* ===== 红包列表 ===== */
.red-history {
flex: 1;
background: #f5f5f5;
margin-top: 16rpx;
overflow: hidden;
}
.list-scroll {
height: 100%;
overflow: auto;
}
.record {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background: #fff;
border-bottom: 1px solid #eee;
margin: 0 24rpx;
border-radius: 12rpx;
margin-bottom: 12rpx;
}
.record:first-child {
margin-top: 12rpx;
}
.red-icon {
width: 70rpx;
height: 140rpx;
flex-shrink: 0;
}
.content {
flex: 1;
margin-left: 20rpx;
min-width: 0;
}
.name {
font-size: 28rpx;
color: #333;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time {
font-size: 24rpx;
color: #999;
display: block;
margin-top: 8rpx;
}
.right {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
}
.amount {
font-size: 32rpx;
color: #ff4d4f;
font-weight: 600;
}
.state-text {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.receive-btn {
margin-top: 12rpx;
padding: 8rpx 28rpx;
font-size: 24rpx;
color: #fff;
background: #ff4d4f;
border-radius: 24rpx;
line-height: 1.4;
}
.no-more {
text-align: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
}
.loading-tip {
text-align: center;
padding: 20rpx 0;
font-size: 24rpx;
color: #999;
}
.no-data {
display: flex;
justify-content: center;
align-items: center;
flex: 0.9;
font-size: 32rpx;
color: #999;
}
@@ -0,0 +1,196 @@
import { appId } from "../../../env";
import { request } from "../../../utils/request";
import { receiveRedPacket } from "../../../utils/util";
const app = getApp<IAppOption>();
const formatDate = (isoString: string): string => {
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const RED_STATE_MAP: Record<number, string> = {
10: "未发送",
11: "等待领取",
20: "正在发送",
21: "等待确认",
30: "发送失败",
31: "个数不够",
32: "账户余额不足",
33: "客户未实名",
34: "其它原因",
40: "已领取",
50: "已过期",
};
const getStateText = (state: number) => {
return RED_STATE_MAP[state] || "发放中";
};
const FAILED_STATES = [30, 31, 32, 33, 34];
const canReceive = (state: number) => {
return state === 11 || state === 21 || FAILED_STATES.includes(state);
};
const isRetry = (state: number) => {
return FAILED_STATES.includes(state);
};
Page({
data: {
list: [] as any[],
loading: false,
hasMore: true,
current: 1,
pageSize: 15,
activeTab: 0,
tabs: [
{ label: '待领取', value: 0, type: 1 },
{ label: '已领取', value: 1, type: 2 },
{ label: '已过期', value: 2, type: 3 },
],
summary: {
totalAmount: '0.00',
receivedAmount: '0.00',
pendingAmount: '0.00',
expiredAmount: '0.00',
},
store: {},
safeBottom: app.globalData.safeBottom,
safeTop: app.globalData.safeTop,
},
async onLoad() {
await this.fetchRecords(1, false, this.data.tabs[0].type);
},
onReachBottom() {
if (this.data.loading || !this.data.hasMore) return;
const nextPage = this.data.current + 1;
this.fetchRecords(nextPage, true, this.data.tabs[this.data.activeTab].type);
},
onTabChange(e: any) {
const index = e.detail.index;
const tab = this.data.tabs[index];
this.setData({ activeTab: index });
this.fetchRecords(1, false, tab.type);
},
async handleReceive(e: any) {
const { id } = e.currentTarget.dataset;
const { code } = this.data.store as Record<string, any>;
const params = {
appId,
code,
openId: wx.getStorageSync("openId"),
redId: id,
}
try {
await receiveRedPacket(
params,
'/app/general/receive-red',
appId
);
// 领取成功,刷新当前列表
this.fetchRecords(1, false, this.data.tabs[this.data.activeTab].type);
} catch (err) {
console.error('领取红包失败:', err);
}
},
async fetchRecords(page: number, append = false, type: number) {
const app = getApp<IAppOption>();
const openId = wx.getStorageSync("openId");
this.setData({
store: app.globalData.store,
});
if (!openId) {
wx.showToast({ title: "请先登录", icon: "none" });
return;
}
if (append) {
this.setData({ loading: true });
} else {
wx.showLoading({ title: "加载中" });
}
try {
let res: any;
const { state, seeId, code } = this.data.store as Record<string, any>;
const params: Record<string, any> = {};
params.openId = openId;
params.current = page;
params.pageSize = 15;
params.type = type;
if (code) params.code = code;
if (state) params.state = state;
if (seeId) params.seeId = seeId;
// if (code) {
res = await request({
options: {
url: "/app/video-watch/record",
data: params,
},
isLoading: !append,
// needLogin: false,
});
// } else {
// res = await request({
// options: {
// url: "/live/watch/record",
// data: params,
// },
// isLoading: !append,
// needLogin: false,
// });
// }
const records = res.data?.records || res.records || [];
const items = records.map((item: any) => ({
...item,
date: formatDate(item.createdTime),
stateText: getStateText(item.state),
amountText: `${(item.amount / 100).toFixed(2)}`,
canReceive: canReceive(item.state),
isRetry: isRetry(item.state),
}));
this.setData({
list: append ? [...this.data.list, ...items] : items,
current: page,
hasMore: records.length >= this.data.pageSize,
});
// 汇总数据在 res.data 根层级,单位是分,转为元
const d = res.data;
if (d && d.totalAmount !== undefined) {
this.setData({
summary: {
totalAmount: (d.totalAmount / 100).toFixed(2),
receivedAmount: (d.receivedAmount / 100).toFixed(2),
pendingAmount: (d.pendingAmount / 100).toFixed(2),
expiredAmount: (d.expiredAmount / 100).toFixed(2),
},
});
}
} catch (err) {
console.error("加载红包记录失败:", err);
} finally {
if (append) {
this.setData({ loading: false });
} else {
wx.hideLoading();
}
}
},
});
@@ -0,0 +1,57 @@
<navigation-bar title="红包记录" back="{{true}}" color="black" background="#FFF" />
<view class="page-wrap">
<view class="summary-section">
<view class="summary-card total-card">
<text class="card-label">总领取金额</text>
<text class="card-amount total-amount">¥{{summary.totalAmount}}</text>
</view>
<view class="summary-row">
<view class="summary-card sub-card">
<text class="card-label">待领取金额</text>
<text class="card-amount pending">¥{{summary.pendingAmount}}</text>
</view>
<view class="summary-card sub-card">
<text class="card-label">已领取金额</text>
<text class="card-amount received">¥{{summary.receivedAmount}}</text>
</view>
<view class="summary-card sub-card">
<text class="card-label">已过期金额</text>
<text class="card-amount expired">¥{{summary.expiredAmount}}</text>
</view>
</view>
</view>
<view class="tabs-wrap">
<van-tabs active="{{activeTab}}" bind:change="onTabChange" animated>
<van-tab wx:for="{{tabs}}" wx:key="value" title="{{item.label}}" />
</van-tabs>
</view>
<view wx:if="{{list.length}}" class="red-history">
<scroll-view
class="list-scroll"
scroll-y
bindscrolltolower="onReachBottom"
lower-threshold="200"
>
<view class="record" wx:for="{{list}}" wx:key="id">
<image class="red-icon" src="./assets/red.svg" mode="aspectFill"/>
<view class="content">
<text class="name">{{item.description}}</text>
<text class="time">{{item.date}}</text>
</view>
<view class="right">
<text class="amount">¥{{item.amountText}}</text>
<view wx:if="{{item.canReceive}}" class="receive-btn" data-id="{{item.id}}" bind:tap="handleReceive">{{item.isRetry ? '重试' : '领取'}}</view>
<text wx:else class="state-text">{{item.stateText}}</text>
</view>
</view>
<view wx:if="{{!hasMore}}" class="no-more">没有更多数据了</view>
<view wx:if="{{loading}}" class="loading-tip">加载中...</view>
</scroll-view>
</view>
<view wx:else class="no-data">暂无记录</view>
</view>