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,5 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}
@@ -0,0 +1,67 @@
.page {
min-height: 100vh;
background: #f5f5f5;
padding: 20rpx 30rpx 60rpx;
box-sizing: border-box;
overflow-x: hidden;
}
.form-section {
background: #ffffff; border-radius: 24rpx; padding: 30rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}
.form-group { margin-bottom: 30rpx; }
.form-label { font-size: 28rpx; font-weight: 600; color: #333; margin-bottom: 14rpx; }
.required { color: #FF3B30; }
.form-input {
width: 100%; height: 80rpx; background: #f7f8fa;
border-radius: 14rpx; padding: 0 24rpx; font-size: 28rpx; box-sizing: border-box;
}
.form-picker {
display: flex; align-items: center; justify-content: space-between;
width: 100%; height: 80rpx; background: #f7f8fa;
border-radius: 14rpx; padding: 0 24rpx; font-size: 28rpx;
box-sizing: border-box; color: #333;
}
.form-picker.placeholder { color: #bbb; }
.picker-arrow { font-size: 22rpx; color: #999; }
.form-textarea {
width: 100%; height: 140rpx; background: #f7f8fa;
border-radius: 14rpx; padding: 20rpx 24rpx; font-size: 28rpx; box-sizing: border-box;
}
.form-row { display: flex; gap: 20rpx; }
.form-row .half { flex: 1; }
.slot-group { display: flex; gap: 16rpx; }
.slot-chip {
flex: 1; text-align: center; height: 72rpx; line-height: 72rpx;
background: #f7f8fa; border-radius: 14rpx; font-size: 26rpx;
color: #666; border: 2rpx solid transparent;
}
.slot-chip.active {
background: #e8f8ee; color: #07C160;
border-color: #07C160; font-weight: 600;
}
.btn-area { margin-top: 40rpx; }
.save-btn {
width: 100%; height: 90rpx; line-height: 90rpx;
background: linear-gradient(135deg, #07C160, #06AD56);
color: #fff; font-size: 32rpx; font-weight: 600;
border-radius: 18rpx; border: none; margin-bottom: 20rpx;
}
.cancel-btn {
width: 100%; height: 90rpx; line-height: 90rpx;
background: #fff; color: #FF3B30; font-size: 32rpx;
border-radius: 18rpx; border: 2rpx solid #FF3B30;
}
@@ -0,0 +1,170 @@
interface MedicineForm {
name: string;
category: string;
dosage: string;
frequency: string;
slots: string[];
quantity: number;
unit: string;
expiryDate: string;
notes: string;
}
const STORAGE_KEY = 'medicine_box_list';
const FREQ_SLOTS: Record<string, string[]> = {
'每日1次': ['早'],
'每日2次': ['早', '晚'],
'每日3次': ['早', '中', '晚'],
'每日4次': ['早', '中', '晚', '睡前'],
'每周1次': ['自定义'],
'按需服用': ['按需'],
};
Page({
data: {
editId: '',
categories: ['抗生素', '解热镇痛', '心血管', '降糖药', '消化系统', '抗过敏', '维生素', '中成药', '外用', '其他'],
frequencies: ['每日1次', '每日2次', '每日3次', '每日4次', '每周1次', '按需服用'],
units: ['盒', '瓶', '片', '粒', '支', '包', '袋'],
timeSlots: ['早', '中', '晚', '睡前', '按需'],
categoryIndex: -1,
freqIndex: -1,
unitIndex: -1,
today: '',
form: {
name: '',
category: '',
dosage: '',
frequency: '',
slots: [] as string[],
quantity: 1,
unit: '',
expiryDate: '',
notes: '',
} as MedicineForm,
},
onLoad(options: Record<string, string>) {
const today = new Date();
this.setData({
today: `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`,
editId: options.id || '',
});
if (options.id) {
const list: any[] = wx.getStorageSync(STORAGE_KEY) || [];
const item = list.find((m: any) => m.id === options.id);
if (item) {
const form = {
name: item.name,
category: item.category,
dosage: item.dosage,
frequency: item.frequency,
slots: item.slots || [],
quantity: item.quantity,
unit: item.unit,
expiryDate: item.expiryDate,
notes: item.notes || '',
};
this.setData({
form,
categoryIndex: this.data.categories.indexOf(item.category),
freqIndex: this.data.frequencies.indexOf(item.frequency),
unitIndex: this.data.units.indexOf(item.unit),
});
}
}
},
onFieldChange(e: WechatMiniprogram.Input) {
const { field } = e.currentTarget.dataset;
this.setData({ [`form.${field}`]: e.detail.value });
},
onCategoryChange(e: WechatMiniprogram.PickerChange) {
const idx = Number(e.detail.value);
this.setData({ categoryIndex: idx, 'form.category': this.data.categories[idx] });
},
onFreqChange(e: WechatMiniprogram.PickerChange) {
const idx = Number(e.detail.value);
const freq = this.data.frequencies[idx];
const slots = FREQ_SLOTS[freq] || ['早', '中', '晚'];
this.setData({ freqIndex: idx, 'form.frequency': freq, 'form.slots': slots });
},
onUnitChange(e: WechatMiniprogram.PickerChange) {
const idx = Number(e.detail.value);
this.setData({ unitIndex: idx, 'form.unit': this.data.units[idx] });
},
onDateChange(e: WechatMiniprogram.PickerChange) {
this.setData({ 'form.expiryDate': e.detail.value });
},
toggleSlot(e: WechatMiniprogram.TouchEvent) {
const { slot } = e.currentTarget.dataset;
const slots = [...this.data.form.slots];
const idx = slots.indexOf(slot);
if (idx > -1) {
slots.splice(idx, 1);
} else {
slots.push(slot);
}
this.setData({ 'form.slots': slots });
},
save() {
const { form, editId } = this.data;
if (!form.name.trim()) {
wx.showToast({ title: '请输入药品名称', icon: 'none' });
return;
}
if (!form.category) {
wx.showToast({ title: '请选择药品分类', icon: 'none' });
return;
}
const list: any[] = wx.getStorageSync(STORAGE_KEY) || [];
if (editId) {
const idx = list.findIndex((m: any) => m.id === editId);
if (idx > -1) {
list[idx] = { ...list[idx], ...form };
}
} else {
list.unshift({
id: Date.now().toString(),
...form,
addedAt: new Date().toISOString().split('T')[0],
});
}
wx.setStorageSync(STORAGE_KEY, list);
wx.showToast({ title: editId ? '已更新' : '已添加', icon: 'success' });
setTimeout(() => wx.navigateBack(), 800);
},
cancel() {
const { editId } = this.data;
if (!editId) {
wx.navigateBack();
return;
}
wx.showModal({
title: '确认删除',
content: '将删除此药品及打卡记录',
success: (res) => {
if (res.confirm) {
const list: any[] = wx.getStorageSync(STORAGE_KEY) || [];
wx.setStorageSync(STORAGE_KEY, list.filter((m: any) => m.id !== editId));
const checkins = wx.getStorageSync('checkin_records') || {};
delete checkins[editId];
wx.setStorageSync('checkin_records', checkins);
wx.showToast({ title: '已删除', icon: 'success' });
setTimeout(() => wx.navigateBack(), 800);
}
},
});
},
});
@@ -0,0 +1,79 @@
<view class="page">
<navigation-bar title="{{editId ? '编辑药品' : '添加药品'}}" back="{{true}}" color="black" background="#ffffff" />
<view class="form-section">
<view class="form-group">
<view class="form-label">药品名称 <text class="required">*</text></view>
<input class="form-input" placeholder="请输入药品名称" value="{{form.name}}" bindinput="onFieldChange" data-field="name" />
</view>
<view class="form-group">
<view class="form-label">药品分类</view>
<picker mode="selector" range="{{categories}}" value="{{categoryIndex}}" bindchange="onCategoryChange">
<view class="form-picker {{form.category ? '' : 'placeholder'}}">
{{form.category || '请选择分类'}}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="form-row">
<view class="form-group half">
<view class="form-label">单次用量</view>
<input class="form-input" placeholder="如:1片" value="{{form.dosage}}" bindinput="onFieldChange" data-field="dosage" />
</view>
<view class="form-group half">
<view class="form-label">服用频次</view>
<picker mode="selector" range="{{frequencies}}" value="{{freqIndex}}" bindchange="onFreqChange">
<view class="form-picker {{form.frequency ? '' : 'placeholder'}}">
{{form.frequency || '请选择'}}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
</view>
<view class="form-group">
<view class="form-label">服用时段</view>
<view class="slot-group">
<view wx:for="{{timeSlots}}" wx:key="*this" class="slot-chip {{form.slots.indexOf(item) > -1 ? 'active' : ''}}" data-slot="{{item}}" bindtap="toggleSlot">{{item}}</view>
</view>
</view>
<view class="form-row">
<view class="form-group half">
<view class="form-label">库存数量</view>
<input class="form-input" type="number" placeholder="数量" value="{{form.quantity}}" bindinput="onFieldChange" data-field="quantity" />
</view>
<view class="form-group half">
<view class="form-label">单位</view>
<picker mode="selector" range="{{units}}" value="{{unitIndex}}" bindchange="onUnitChange">
<view class="form-picker {{form.unit ? '' : 'placeholder'}}">
{{form.unit || '请选择'}}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
</view>
<view class="form-group">
<view class="form-label">有效期至</view>
<picker mode="date" value="{{form.expiryDate}}" start="{{today}}" bindchange="onDateChange">
<view class="form-picker {{form.expiryDate ? '' : 'placeholder'}}">
{{form.expiryDate || '请选择有效期'}}
<text class="picker-arrow">📅</text>
</view>
</picker>
</view>
<view class="form-group">
<view class="form-label">备注</view>
<textarea class="form-textarea" placeholder="添加备注信息..." value="{{form.notes}}" bindinput="onFieldChange" data-field="notes" />
</view>
</view>
<view class="btn-area">
<button class="save-btn" bindtap="save">保存药品</button>
<button class="cancel-btn" bindtap="cancel" wx:if="{{editId}}">删除此药品</button>
</view>
</view>