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,6 @@
{
"component": true,
"usingComponents": {
"popup": "/components/popup/popup"
}
}
+217
View File
@@ -0,0 +1,217 @@
/* components/banner/banner.wxss */
.banner-wrap {
width: 100%;
position: absolute;
top: 7%;
left: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 9999;
padding: 30rpx;
box-sizing: border-box;
}
.banner__avatar {
border-radius: 50%;
margin-right: 20rpx;
}
.banner__card {
border-radius: 25rpx;
padding: 18rpx;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.5);
margin-bottom: 20rpx;
display: inline-block;
}
.banner__icon {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
.banner__avatar {
width: 80rpx;
height: 80rpx;
}
.banner-id {
font-size: 21rpx;
}
.banner-box {
display: flex;
align-items: center;
}
.banner__title {
max-width: 250rpx;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.banner__count {
color: #fff;
margin-top: 10rpx;
}
.popup-header {
text-align: center;
font-weight: 700;
color: #000;
font-size: 32rpx;
}
.popup-text {
color: #000;
font-size: 28rpx;
}
.report-options {
width: 100%;
margin: 24rpx 0;
}
.options-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 24rpx;
}
.option-item {
display: flex;
align-items: center;
margin-right: 30rpx;
margin-bottom: 20rpx;
}
.option-item radio {
margin-right: 10rpx;
}
.other-input {
margin: 24rpx 0;
}
.other-input-field {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
box-sizing: border-box;
}
.upload-section {
margin-bottom: 24rpx;
}
.upload-title {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.image-wrapper {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
overflow: hidden;
}
.uploaded-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.delete-image {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 32rpx;
height: 32rpx;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
}
.upload-btn {
width: 160rpx;
height: 160rpx;
border: 2rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f8f8f8;
}
.upload-icon {
font-size: 48rpx;
color: #999;
margin-bottom: 8rpx;
}
.upload-text {
font-size: 24rpx;
color: #999;
}
.description-section {
margin-bottom: 32rpx;
}
.description-input {
width: 100%;
height: 160rpx;
padding: 16rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
box-sizing: border-box;
margin-bottom: 8rpx;
}
.char-count {
font-size: 24rpx;
color: #999;
text-align: right;
display: block;
}
.submit-section {
padding: 0 24rpx;
}
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #ff4d4f;
color: #fff;
font-size: 32rpx;
border-radius: 44rpx;
border: none;
}
.submit-btn:active {
background: #ff7875;
}
.submit-btn[disabled] {
background: #ccc;
color: #999;
opacity: 0.7;
}
+281
View File
@@ -0,0 +1,281 @@
import { request } from "../../utils/request";
// components/banner/banner.ts
interface BannerData {
avatarUrl: string;
nickName: string;
userId: string;
showPopup: boolean;
reportOptions: Array<{
id: number;
text: string;
checked: boolean;
}>;
showOtherInput: boolean;
otherReason: string;
uploadedImages: string[];
description: string;
state: number;
isSubmitEnabled: boolean;
}
interface BannerProperties {
roomInfo: {
iconUrl?: string;
name?: string;
mumId?: string;
state: number;
};
}
interface BannerComponent extends WechatMiniprogram.Component.TrivialInstance {
data: BannerData;
properties: BannerProperties;
}
const state: Record<number, string> = {
1: "直播中",
2: "直播结束",
};
Component({
/**
* 组件的属性列表
*/
properties: {
roomInfo: {
type: Object,
value: {
iconUrl: "",
nickname: "",
mumId: "",
state: null
},
observer: function (
this: BannerComponent,
newVal: BannerProperties["roomInfo"]
) {
this.updateRoomInfo(newVal);
},
},
watchNum:{
type:Number,
data:0
}
},
/**
* 组件的初始数据
*/
data: {
avatarUrl: "",
nickName: "",
userId: "",
state: "",
showPopup: false,
reportOptions: [
{ id: 1, text: "价格虚假", checked: false },
{ id: 2, text: "品牌虚假", checked: false },
{ id: 3, text: "色情低俗", checked: false },
{ id: 4, text: "虚假活动", checked: false },
{ id: 5, text: "不良行为", checked: false },
{ id: 6, text: "商品信息虚假", checked: false },
{ id: 7, text: "出售违禁品", checked: false },
{ id: 8, text: "其他", checked: false },
],
showOtherInput: false,
otherReason: "",
uploadedImages: [],
description: "",
isSubmitEnabled: false,
},
lifetimes: {
attached(this: BannerComponent) {
this.updateRoomInfo(this.properties.roomInfo);
},
},
/**
* 组件的方法列表
*/
methods: {
updateRoomInfo(
this: BannerComponent,
roomInfo?: BannerProperties["roomInfo"]
) {
const info = roomInfo || this.properties.roomInfo;
this.setData({
avatarUrl: info?.iconUrl || "",
nickName: info?.name || "",
userId: info?.mumId || "",
state: state[info?.state ?? 2],
});
},
handOpen(this: WechatMiniprogram.Component.TrivialInstance) {
this.setData({
showPopup: true,
isSubmitEnabled: false,
});
},
closePopup(this: WechatMiniprogram.Component.TrivialInstance) {
this.setData({
showPopup: false,
});
},
onOptionChange(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
const { value } = e.detail;
const reportOptions = this.data.reportOptions.map((item: any) => ({
...item,
checked: value.includes(item.id.toString()),
}));
const showOtherInput =
reportOptions.find((item: any) => item.id === 8)?.checked || false;
this.setData({
reportOptions,
showOtherInput,
}, () => {
this.updateSubmitEnabled();
});
},
onOtherInput(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
this.setData({
otherReason: e.detail.value,
}, () => {
this.updateSubmitEnabled();
});
},
deleteImage(
this: WechatMiniprogram.Component.TrivialInstance,
e: WechatMiniprogram.TouchEvent
) {
const { index } = e.currentTarget.dataset;
wx.showModal({
title: "提示",
content: "确定要删除这张图片吗?",
success: (res) => {
if (res.confirm) {
const uploadedImages = [...this.data.uploadedImages];
uploadedImages.splice(index, 1);
this.setData({ uploadedImages });
}
},
});
},
chooseImage(this: WechatMiniprogram.Component.TrivialInstance) {
wx.chooseImage({
count: 3 - this.data.uploadedImages.length,
success: (res) => {
this.setData({
uploadedImages: [...this.data.uploadedImages, ...res.tempFilePaths],
});
},
});
},
previewImage(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
const { src } = e.currentTarget.dataset;
wx.previewImage({
current: src,
urls: this.data.uploadedImages,
});
},
onDescriptionInput(
this: WechatMiniprogram.Component.TrivialInstance,
e: any
) {
this.setData({
description: e.detail.value,
}, () => {
this.updateSubmitEnabled();
});
},
updateSubmitEnabled(this: BannerComponent) {
const selectedOptions = this.data.reportOptions.filter(
(item: any) => item.checked
);
const hasSelectedOptions = selectedOptions.length > 0;
const isOtherSelected = selectedOptions.some((item: any) => item.id === 8);
const otherReasonValid = !isOtherSelected || (this.data.otherReason && this.data.otherReason.trim().length > 0);
const hasDescription = this.data.description && this.data.description.trim().length > 0;
this.setData({
isSubmitEnabled: hasSelectedOptions && otherReasonValid && hasDescription,
});
},
async submitReport(this: BannerComponent) {
if (!this.data.isSubmitEnabled) {
return;
}
const selectedOptions = this.data.reportOptions.filter(
(item: any) => item.checked
);
if (selectedOptions.length === 0) {
wx.showToast({
title: "请选择举报原因",
icon: "none",
});
return;
}
if (
selectedOptions.some((item: any) => item.id === 8) &&
!this.data.otherReason
) {
wx.showToast({
title: "请填写其他举报原因",
icon: "none",
});
return;
}
// 提交举报信息
const reportData = {
reasons: selectedOptions,
otherReason: this.data.otherReason,
images: this.data.uploadedImages,
description: this.data.description,
userId: this.data.userId,
roomId: wx.getStorageSync("joinedGroupId"),
};
console.log("举报信息:", reportData);
try {
await request({
options: {
url: `/app/video-live/feedback`,
data: reportData,
},
});
wx.showToast({
title: "举报成功",
icon: "success",
});
// 提交成功后关闭弹窗
this.setData({
showPopup: false,
reportOptions: this.data.reportOptions.map((item: any) => ({
...item,
checked: false,
})),
showOtherInput: false,
otherReason: "",
uploadedImages: [],
description: "",
isSubmitEnabled: false,
});
} catch (error) {
this.handleError(error);
}
},
},
});
+83
View File
@@ -0,0 +1,83 @@
<!--components/banner/banner.wxml-->
<view class="banner-wrap">
<view>
<view class="banner__card banner-box">
<image class="banner__avatar" src="{{avatarUrl}}" mode="aspectFill" />
<view>
<view class="banner__title text-overflow">{{nickName}}</view>
<view class="banner-box banner__count">
<image class="banner__icon" src="https://img.alicdn.com/imgextra/i1/O1CN01GC0HVZ1kWz8bvu0KX_!!6000000004692-2-tps-36-36.png" mode="widthFix" />
<text>{{watchNum}}</text>
</view>
</view>
</view>
<view class="banner__card">{{state}}</view>
</view>
<view>
<view class="banner__card banner-box" bind:tap="handOpen">
<image class="banner__icon" src="/assets/img/p.svg" mode="widthFix"/>
<text>投诉</text>
</view>
<view class="banner-id">用户ID:{{userId}}</view>
</view>
</view>
<popup visible="{{showPopup}}" bind:close="closePopup">
<view slot="header" class="popup-header">
<text>举报本场直播</text>
</view>
<view class="popup-content">
<radio-group class="report-options" bindchange="onOptionChange">
<view class="options-row">
<view class="option-item" wx:for="{{reportOptions}}" wx:for-item="item" wx:key="id">
<radio value="{{item.id}}" checked="{{item.checked}}"/>
<text class="popup-text">{{item.text}}</text>
</view>
</view>
</radio-group>
<view class="other-input" wx:if="{{showOtherInput}}">
<input
class="other-input-field popup-text"
type="text"
placeholder="请输入其他举报原因"
value="{{otherReason}}"
bindinput="onOtherInput"
/>
</view>
<view class="upload-section">
<view class="upload-title popup-text">上传图片证据(最多3张)</view>
<view class="image-list">
<block wx:for="{{uploadedImages}}" wx:key="*this">
<view class="image-wrapper">
<image class="uploaded-image" src="{{item}}" mode="aspectFill" bindtap="previewImage" data-src="{{item}}"/>
<view class="delete-image" catchtap="deleteImage" data-index="{{index}}">×</view>
</view>
</block>
<view class="upload-btn" wx:if="{{uploadedImages.length < 3}}" bindtap="chooseImage">
<view class="upload-icon">+</view>
<text class="upload-text">上传图片</text>
</view>
</view>
</view>
<view class="description-section">
<textarea
class="description-input popup-text"
placeholder="请补充说明(选填)"
value="{{description}}"
bindinput="onDescriptionInput"
maxlength="200"
/>
<text class="char-count popup-text">{{description.length}}/200</text>
</view>
<view class="submit-section">
<button class="submit-btn" disabled="{{!isSubmitEnabled}}" bindtap="submitReport">提交举报</button>
</view>
</view>
</popup>