feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
.guide-popup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
|
||||
.guide-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(10, 13, 24, 0.42);
|
||||
backdrop-filter: blur(10rpx);
|
||||
opacity: 0;
|
||||
transition: opacity 0.28s ease;
|
||||
}
|
||||
|
||||
.guide-shell {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: translateY(calc(100% + 48rpx));
|
||||
transition: transform 0.28s ease;
|
||||
will-change: transform;
|
||||
pointer-events: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.guide-drag-bar {
|
||||
width: 88rpx;
|
||||
height: 8rpx;
|
||||
margin: 0 auto 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.guide-avatar-wrap {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -40rpx;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
padding: 8rpx;
|
||||
border-radius: 50%;
|
||||
background: #edf0f7;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 18rpx 42rpx rgba(11, 18, 35, 0.14);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.guide-avatar {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.18);
|
||||
background: #f6f7fb;
|
||||
}
|
||||
|
||||
.guide-avatar-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #ffd7dc 0%, #ff8ba2 100%);
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.guide-panel {
|
||||
border-radius: 28rpx;
|
||||
padding: 82rpx 24rpx 28rpx;
|
||||
background: linear-gradient(180deg, #edf0f7 0%, #e7ebf4 100%);
|
||||
box-shadow: 0 28rpx 70rpx rgba(11, 18, 35, 0.18);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-topbar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.guide-name {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #2a2f3a;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.guide-entry {
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: -30%;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
color: #2f3442;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.guide-entry-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
|
||||
.guide-entry-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.guide-balance-card {
|
||||
margin-top: 26rpx;
|
||||
padding: 22rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.44);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.guide-balance-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.guide-balance-label {
|
||||
color: #5f6675;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.guide-balance-value {
|
||||
margin-top: 10rpx;
|
||||
color: #ff285d;
|
||||
font-size: 54rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.guide-balance-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guide-pill {
|
||||
min-width: 120rpx;
|
||||
padding: 0 22rpx;
|
||||
height: 58rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
color: #ff4774;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-pill-primary {
|
||||
background: linear-gradient(135deg, #ff5d87 0%, #ff3365 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
margin-top: 22rpx;
|
||||
padding: 18rpx 12rpx 4rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.32);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
row-gap: 28rpx;
|
||||
}
|
||||
|
||||
.guide-grid-item {
|
||||
min-height: 126rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.guide-grid-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.guide-grid-label {
|
||||
margin-top: 16rpx;
|
||||
color: #2f3442;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.is-visible {
|
||||
.guide-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.guide-shell {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
type GuideMenuItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
interface GuidePopupProperties {
|
||||
show: boolean;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
images: string[];
|
||||
amount: string;
|
||||
}
|
||||
|
||||
interface GuidePopupData {
|
||||
mounted: boolean;
|
||||
visible: boolean;
|
||||
displayName: string;
|
||||
displaySubtitle: string;
|
||||
avatarUrl: string;
|
||||
avatarLetter: string;
|
||||
balanceLabel: string;
|
||||
withdrawText: string;
|
||||
incomeText: string;
|
||||
menuItems: GuideMenuItem[];
|
||||
}
|
||||
|
||||
interface GuidePopupInstance
|
||||
extends WechatMiniprogram.Component.TrivialInstance {
|
||||
data: GuidePopupData;
|
||||
properties: GuidePopupProperties;
|
||||
_hideTimer?: number | null;
|
||||
}
|
||||
|
||||
const DEFAULT_NAME = "当前用户";
|
||||
const DEFAULT_ENTRY = "个人中心";
|
||||
const DEFAULT_BALANCE_LABEL = "可提现金额";
|
||||
const DEFAULT_WITHDRAW_TEXT = "立即提现";
|
||||
const DEFAULT_INCOME_TEXT = "收益明细";
|
||||
const HIDE_DELAY = 280;
|
||||
|
||||
const DEFAULT_MENU_ITEMS: GuideMenuItem[] = [
|
||||
{
|
||||
key: "orders",
|
||||
label: "我的订单",
|
||||
icon: "./icons/orders.svg",
|
||||
},
|
||||
{
|
||||
key: "prize-records",
|
||||
label: "中奖记录",
|
||||
icon: "./icons/gift.svg",
|
||||
},
|
||||
{
|
||||
key: "coupons",
|
||||
label: "我的优惠券",
|
||||
icon: "./icons/coupon.svg",
|
||||
},
|
||||
{
|
||||
key: "comment-board",
|
||||
label: "评论记录板",
|
||||
icon: "./icons/comment.svg",
|
||||
},
|
||||
{
|
||||
key: "nickname",
|
||||
label: "修改昵称",
|
||||
icon: "./icons/edit.svg",
|
||||
},
|
||||
{
|
||||
key: "complaint",
|
||||
label: "投诉",
|
||||
icon: "./icons/warning.svg",
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeImages(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
|
||||
return value.filter(
|
||||
(item): item is string => typeof item === "string" && item.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function syncViewModel(
|
||||
instance: GuidePopupInstance,
|
||||
titleText: string,
|
||||
subtitleText: string,
|
||||
imageList: string[],
|
||||
) {
|
||||
const name = titleText || wx.getStorageSync("userName") || DEFAULT_NAME;
|
||||
const subtitle = subtitleText || DEFAULT_ENTRY;
|
||||
const avatarUrl = imageList.length ? imageList[0] : "";
|
||||
const avatarLetter = String(name).trim().slice(0, 1).toUpperCase() || "U";
|
||||
|
||||
instance.setData({
|
||||
displayName: name,
|
||||
displaySubtitle: subtitle,
|
||||
avatarUrl,
|
||||
avatarLetter,
|
||||
});
|
||||
}
|
||||
|
||||
function clearHideTimer(instance: GuidePopupInstance) {
|
||||
if (!instance._hideTimer) return;
|
||||
|
||||
clearTimeout(instance._hideTimer);
|
||||
instance._hideTimer = null;
|
||||
}
|
||||
|
||||
function togglePopup(instance: GuidePopupInstance, show: boolean) {
|
||||
clearHideTimer(instance);
|
||||
|
||||
if (show) {
|
||||
if (instance.data.mounted) {
|
||||
wx.nextTick(() => {
|
||||
instance.setData({ visible: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
instance.setData(
|
||||
{
|
||||
mounted: true,
|
||||
visible: false,
|
||||
},
|
||||
() => {
|
||||
wx.nextTick(() => {
|
||||
instance.setData({ visible: true });
|
||||
});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!instance.data.mounted) return;
|
||||
|
||||
instance.setData({ visible: false });
|
||||
instance._hideTimer = setTimeout(() => {
|
||||
instance.setData({ mounted: false });
|
||||
instance._hideTimer = null;
|
||||
}, HIDE_DELAY);
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer(this: GuidePopupInstance, nextValue: boolean) {
|
||||
togglePopup(this, !!nextValue);
|
||||
},
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer(this: GuidePopupInstance, nextValue: string) {
|
||||
syncViewModel(
|
||||
this,
|
||||
nextValue || "",
|
||||
this.properties.subtitle || "",
|
||||
normalizeImages(this.properties.images),
|
||||
);
|
||||
},
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer(this: GuidePopupInstance, nextValue: string) {
|
||||
syncViewModel(
|
||||
this,
|
||||
this.properties.title || "",
|
||||
nextValue || "",
|
||||
normalizeImages(this.properties.images),
|
||||
);
|
||||
},
|
||||
},
|
||||
images: {
|
||||
type: Array,
|
||||
value: [],
|
||||
observer(this: GuidePopupInstance, nextValue: unknown) {
|
||||
syncViewModel(
|
||||
this,
|
||||
this.properties.title || "",
|
||||
this.properties.subtitle || "",
|
||||
normalizeImages(nextValue),
|
||||
);
|
||||
},
|
||||
},
|
||||
amount: {
|
||||
type: String,
|
||||
value: "0.00",
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
mounted: false,
|
||||
visible: false,
|
||||
displayName: DEFAULT_NAME,
|
||||
displaySubtitle: DEFAULT_ENTRY,
|
||||
avatarUrl: "",
|
||||
avatarLetter: "U",
|
||||
balanceLabel: DEFAULT_BALANCE_LABEL,
|
||||
withdrawText: DEFAULT_WITHDRAW_TEXT,
|
||||
incomeText: DEFAULT_INCOME_TEXT,
|
||||
menuItems: DEFAULT_MENU_ITEMS,
|
||||
} as GuidePopupData,
|
||||
|
||||
lifetimes: {
|
||||
attached(this: GuidePopupInstance) {
|
||||
syncViewModel(
|
||||
this,
|
||||
this.properties.title || "",
|
||||
this.properties.subtitle || "",
|
||||
normalizeImages(this.properties.images),
|
||||
);
|
||||
togglePopup(this, !!this.properties.show);
|
||||
},
|
||||
|
||||
detached(this: GuidePopupInstance) {
|
||||
clearHideTimer(this);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
noop() {},
|
||||
|
||||
handleClose(this: GuidePopupInstance) {
|
||||
this.triggerEvent("close");
|
||||
},
|
||||
|
||||
handlePreviewAvatar(this: GuidePopupInstance) {
|
||||
const images = normalizeImages(this.properties.images);
|
||||
if (!images.length) return;
|
||||
|
||||
wx.previewImage({
|
||||
current: images[0],
|
||||
urls: images,
|
||||
});
|
||||
},
|
||||
|
||||
handleQuickAction(
|
||||
this: GuidePopupInstance,
|
||||
e: WechatMiniprogram.CustomEvent,
|
||||
) {
|
||||
const key = (e.currentTarget.dataset as { key?: string }).key || "";
|
||||
if (!key) return;
|
||||
|
||||
this.triggerEvent("action", { key });
|
||||
},
|
||||
|
||||
handleMenuTap(this: GuidePopupInstance, e: WechatMiniprogram.CustomEvent) {
|
||||
const key = (e.currentTarget.dataset as { key?: string }).key || "";
|
||||
if (!key) return;
|
||||
|
||||
this.triggerEvent("action", { key });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
<view wx:if="{{mounted}}" class="guide-popup {{visible ? 'is-visible' : ''}}">
|
||||
<view class="guide-mask" bindtap="handleClose"></view>
|
||||
|
||||
<view class="guide-shell" catchtap="noop" catchtouchmove="noop">
|
||||
<view class="guide-drag-bar"></view>
|
||||
|
||||
<view class="guide-avatar-wrap">
|
||||
<image
|
||||
wx:if="{{avatarUrl}}"
|
||||
class="guide-avatar"
|
||||
src="{{avatarUrl}}"
|
||||
mode="aspectFill"
|
||||
bindtap="handlePreviewAvatar"
|
||||
/>
|
||||
<view wx:else class="guide-avatar guide-avatar-fallback">{{avatarLetter}}</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-panel">
|
||||
<view class="guide-topbar">
|
||||
<view class="guide-name">{{displayName}}</view>
|
||||
<view class="guide-entry">
|
||||
<image class="guide-entry-icon" src="./icons/user.svg" mode="aspectFit" />
|
||||
<text class="guide-entry-text">{{displaySubtitle}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-balance-card">
|
||||
<view class="guide-balance-main">
|
||||
<view class="guide-balance-label">{{balanceLabel}}</view>
|
||||
<view class="guide-balance-value">¥{{amount}}</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-balance-actions">
|
||||
<view class="guide-pill guide-pill-primary" data-key="withdraw" bindtap="handleQuickAction">
|
||||
{{withdrawText}}
|
||||
</view>
|
||||
<view class="guide-pill" data-key="income-detail" bindtap="handleQuickAction">
|
||||
{{incomeText}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-grid">
|
||||
<view
|
||||
wx:for="{{menuItems}}"
|
||||
wx:key="key"
|
||||
class="guide-grid-item"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="handleMenuTap"
|
||||
>
|
||||
<image class="guide-grid-icon" src="{{item.icon}}" mode="aspectFit" />
|
||||
<text class="guide-grid-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 24H76V60H44L28 74V60H20V24Z" stroke="#303644" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M34 38H62" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M34 50H54" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 30C29 30 34 25 34 18H74V34C67 34 62 39 62 46C62 53 67 58 74 58V74H34C34 67 29 62 22 62V30Z" stroke="#303644" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M46 28V64" stroke="#303644" stroke-width="6" stroke-dasharray="8 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 68L28 52L58 22C61.3 18.7 66.7 18.7 70 22C73.3 25.3 73.3 30.7 70 34L40 64L24 68Z" stroke="#303644" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M54 26L66 38" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M24 76H72" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="36" width="56" height="40" rx="8" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M48 36V76" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M20 50H76" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M34 34C28 34 24 30 24 24C24 19 28 16 32 16C40 16 46 27 48 34" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 34C68 34 72 30 72 24C72 19 68 16 64 16C56 16 50 27 48 34" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 571 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="22" y="18" width="44" height="58" rx="10" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M34 34H54" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M34 48H54" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M34 62H48" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M66 58L74 66L86 50" stroke="#303644" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 549 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="36" cy="30" r="12" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M16 70C18.5 57.5 27.5 50 36 50C44.5 50 53.5 57.5 56 70" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 32H82" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 50H76" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 460 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M48 18L76 70H20L48 18Z" stroke="#FF5277" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M48 36V52" stroke="#FF5277" stroke-width="6" stroke-linecap="round"/>
|
||||
<circle cx="48" cy="62" r="3.5" fill="#FF5277"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |